Skip to content
DDS Vibe Academy Free · No signup · No paywall
Class 92 · Advanced Built with Claude Fable 5.1 September 13, 2026

$25 in.
Live in sixty seconds.

Nobody is awake. Every guarantee has to refuse on its own.

A stranger buys a $25 listing, types their shop into a form, and their page is readable in about a minute. No queue, no moderator, no approval. This class is the machinery underneath a directory that does that for real — the payment webhook and the signature that has to be checked against raw bytes, the entitlement that survives a failed write, the geocoding step that refuses rather than guesses — and the harder half: the checks in that same system that looked like protection and turned out to be incapable of failing. Seven projects, every prompt written out.

565 shops listed ~60s payment to live 10 steps, 0 humans 3 cron jobs, 0 alerts 7 projects to build ~75 min read

Quick answer

A self-service directory is a machine that converts money into published content while you sleep, and everything hard about it follows from that sentence. The payment is the easy half: a webhook, a signature verified against the exact raw bytes that were sent, and a record of a right to publish rather than the publication itself — which is what lets a failed submission be retried instead of refunded. The hard half is that no person is going to catch anything. Every guarantee you would normally get from someone reading a queue has to become a check that can refuse, and refusing is harder to write than warning. Then the real problem arrives: your checks quietly stop being checks. In the system measured for this class, a duplicate query ran against one collection while the page was assembled from two, a health endpoint reported a six-day-old reading as current, and a build step carried a flag that let it fail without failing the build. Each looked like protection the day it was written. A check that cannot fail is not a check.

Key takeaways

  1. Verify the payment yourself. Never trust the browser's word that money changed hands. Take the webhook, check the signature against the raw bytes, and record the entitlement in your own store.
  2. Record a right, not a result. The webhook writes "this order may publish one listing." The listing is written later. Three of the four ways this can go wrong become recoverable the moment you split them.
  3. Make it refuse. A validator that warns is documentation. Give every refusal a sentence the submitter can act on, and the support load goes away with it.
  4. Write checks that are able to fail. Change the threshold until the check breaks. If you cannot make it fail, you have a report.
  5. A 200 means the handler ran, not that it worked. A scheduled job that returns success for "did nothing" is indistinguishable from one that is dead.
  6. Anything refreshed by the thing it watches is not a monitor.
  7. Decide what the money buys. On a paid path the payment is the moderation, and you should be able to say that out loud. If you cannot, you need a human somewhere.
SELF-SERVICE · MODULE 00

Ten steps, no humans

The reference system for this class is the Bay State Treasure Gazette, a directory of 565 Massachusetts shops. A shop owner buys a one-year listing for $25, receives an order number by email, fills in a form, and their page is live. The copy on the form says “usually within a minute” and that is not marketing — there is nothing in the path that waits for a person.

$25 IN. SIXTY SECONDS LATER, A PAGE.the paid path - no human approves anything on this line01order paid02webhook03HMAC04entitlement05form06match07validate08geocode09dedupe10publishedgates that can refuse are marked in ink; the rest are conveyor beltAND THE FOUR WAYS A PAID ORDER NEVER BECOMES A PAGEwebhook never arrivesbuyer sees 'order not found'THE BUYERbuyer never returnsthey paid; nothing existsNOBODYpublish transaction throws500, entitlement not consumedLOGS ONLYgeocode refuses400 with an actionable sentenceTHE BUYER'who finds out' is the column that matters. Two of the four rows say nobody.
The paid path. Three steps can refuse; the other seven are conveyor belt. The column that matters in the lower table is the last one — two of the four failure cases say nobody.

What carries identity across the payment boundary

Two values, both already on the buyer's receipt: the order number, normalised, and the email used on the order. Neither is secret. That is worth sitting with, because it is the whole authentication model of the paid path, and the code that implements it says so out loud:

The submission endpoint is public and gated only by an order name, which is
short and guessable in principle. Defences, in order:
  - the order must exist as a PAID entitlement we recorded ourselves
  - the submitter's email must match the order's email
  - each unit redeems exactly once, enforced by a transaction
  - rate limited per IP
Guessing a valid order name without also knowing the buyer's email is the
bar an attacker has to clear.

Write the security posture down, in the file

That comment is doing more work than it looks. A self-service endpoint accrues defences one incident at a time, and six months later nobody can say what the actual bar is. Stating it as a ranked list, in the file that implements it, means the next person to touch it — human or agent — can tell the difference between a defence and a decoration.

Run the gate chain yourself

Six gates, in order. For each submission, decide which one stops it — or whether it goes all the way through.

Interactive · Gate Runnercandidate 1 of 6

Which gate stops it?

Six real gates from the paid submission path, in the order they run. Pick the first one that refuses each candidate.

A shop owner with a valid order and a full street address

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

The last candidate is the lesson

The duplicate check is not missing. It is written, it runs on every submission, and it is correct about the collection it queries. It simply queries one of the two collections the page is assembled from. A shop already in the directory can pay and appear twice, with two cards, two map pins and two detail pages.

The client-side merge does not save it either. It is documented as resolving identifier collisions — and it operates on two id namespaces that cannot produce a collision. It is a true statement about a case that can never arise. Hold onto that sentence; module 03 is built on it.

SELF-SERVICE · MODULE 01

Record a right, not a result

The instinct when a payment lands is to create the thing that was bought. Resist it. At the moment the money clears, the buyer has not told you what to publish yet — they have only told you they intend to. So the webhook writes an entitlement: a small document saying this order may publish one listing, valid until a date, redeemed zero times so far.

The listing is written later, from the form, against that entitlement. Splitting the two is the single highest-leverage decision in the whole build, and it buys three things at once.

What the split buys

  1. Idempotency for free. Key the document by the normalised order number and a webhook retry merges into the same document instead of creating a second one. Payment providers retry. Yours will.
  2. A thing to validate against. The form has something concrete to check — exists, matches, unredeemed — rather than having to re-interrogate the payment provider on every submission, which would mean holding a long-lived credential for it.
  3. Recoverable failure. If the publish transaction throws, the entitlement is not consumed. The buyer retries and it works. Nothing was charged twice and nothing needs a refund.

The signature, and the mistake everybody makes once

The payment provider signs the raw request body and sends the digest in a header. Verification must run against those exact bytes. The usual framework setup parses JSON for every route, and if you verify against a re-serialised object, key order and whitespace have already changed and every signature fails. The fix is to mount the raw body parser on that one path and nowhere else.

The provider signs the RAW request body with the webhook's shared secret and
sends the base64 HMAC-SHA256 in a header. This MUST run against the raw bytes
— if the framework has already parsed the JSON and you re-stringify it, key
order and whitespace change and every signature fails.

Match on the SKU, not the product title

An order can contain anything. Filter the line items by SKU to count how many listing units were bought — titles get edited by whoever runs the shop, and an A/B test on a product name should not silently stop your webhook working. An order containing no listing items should return 200 with a cheerful "nothing for me here", so the provider stops retrying a t-shirt order.

Read the entitlement, field by field

Interactive · Anatomyregion

One entitlement document

The record the webhook writes. Six fields, and every one of them is load-bearing. Click a line to see what it is doing.

entitlement/GAZ-1042

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

The hole the split leaves open, stated plainly

A buyer who pays and never returns to the form has bought nothing, and nothing in the system goes looking for them. No sweep reads unredeemed entitlements, no email chases them, and the seller cannot tell the difference between that and a customer who simply has not got round to it.

This is the largest gap in the "nobody awake" claim, and it belongs in the class as a design question rather than a bug. Closing it needs either a credential for the payment provider so orders can be reconciled against entitlements — reintroducing exactly the long-lived secret the design avoided — or a scheduled nudge over unredeemed entitlements, which needs no new data at all, because the expiry date, the units bought, the units used and the email are already on the document. Project 4 builds the second one.

SELF-SERVICE · MODULE 02

Refuse, don't warn

With nobody reading a queue, validation is the only editorial standard the system has. That changes what validation is for. It is not there to improve a submission; it is there to stop one, and the difference shows up in the code as whether the function returns or throws.

The clearest example in the reference system is geocoding, which most directories treat as enrichment — look up the coordinates, fill them in, carry on. Here it is a hard gate with four distinct refusals:

ConditionWhat the submitter is told
No result at all“We could not find that address.”
Resolves outside the state“That address does not appear to be in Massachusetts.”
Only resolves to a town centre“We could only locate that to the town centre. Please give a full street address with a number.”
Ambiguous between several placesA refusal naming what was ambiguous

Every one of those is a sentence a shop owner can act on without emailing anybody. That is not politeness — on a self-service path, an unactionable error message is a support ticket you have chosen to receive.

Why geocoding became a gate

It was not designed that way. It was promoted to a gate after eighteen records were found carrying coordinates produced by Math.random() around a town centroid — wrong by up to roughly 1.7 miles, and different on every run.

Nothing was broken in the sense of throwing. A missing coordinate had a fallback, the fallback produced a number, the number was a plausible latitude, and the map drew a pin. The failure was not that the code stopped working; it was that the code kept working on invented data. That is the failure mode a self-service system produces by default, because there is nobody looking at the map going “that shop is not in the harbour.”

Three validations. One of them is lying.

Each round below reports a passing result. In each, exactly one line is the reason the result cannot be trusted.

Interactive · Huntround 1 of 3

Find the reason the pass means nothing

Three checks that reported success on real runs. For each, pick the line that makes the success meaningless.

Round 1 — coordinates564 of 564 present, 0 nulls
every published row must carry a latitude and longitude

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

Deny-list, allow-list, and which way a rule fails

These are not two styles of the same rule. A deny-list fails open: anything nobody has thought of yet is permitted. An allow-list fails closed: anything nobody has thought of yet is refused. On a path where a person reviews the output, failing open is an inconvenience. On a path where nobody reviews anything, failing open is the whole risk.

SELF-SERVICE · MODULE 03

A check that cannot fail is not a check

This is the module the rest of the class exists to set up. Every defect in the reference system — and there are a lot of them, all found by measurement rather than by anything going visibly wrong — is the same shape. A test whose answer does not depend on the thing it claims to test.

They are hard to spot because they are not bugs. Each one is correct code, doing exactly what it says, returning exactly the value it should. They fail the only test that matters, which is: can I make this thing say no?

A CHECK THAT CANNOT FAIL IS NOT A CHECK.every item measured in one production system, 2026-09-13CAN REFUSECANNOT FAILgeocode4 refusals, each a sentence the shop owner can act onid collision'curated wins' - the two namespaces cannot collideone redemptiona Firestore transaction; a retry cannot double-spendevery sweepreturns 200 for 'did nothing' and 'did it right'prerenderrefuses to start without an empty root divthe health checkrefreshed only by deploys; reports a 6-day-old truthscheduler countfails the build unless exactly 3 jobs existcheck-examplesallowFailure: true - a report wearing a gate's clothes0% traffica candidate is promoted only after the smoke testthe recency testLast-Modified: now makes every dynamic page passthe weekly agentread its own verdicts and refused to apply thema pipe-delimited rowpositional: one dropped cell shifts all the rest
Twelve checks from one production system, measured 2026-09-13. The six on the left can refuse and have. The six on the right are structurally incapable of refusing, and every one of them was written on purpose by someone trying to be careful.

Can this one refuse?

Interactive · Decidercase 1 of 6

Gate, or report?

Six checks from the same codebase. For each: is it capable of refusing, or does it only look like it is?

Geocoding refuses an address it can only place at a town centre. — Is this capable of refusing?

verified by submitting 'Main Street, Boston'

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

The test that finds them

There is one question, and it is mechanical rather than clever:

Change the threshold until it fails

Take the check. Feed it something that should be refused. If you cannot construct an input that makes it say no — not did not, but cannot — then it is not protecting you, and the passing result it has been emitting for months is not evidence of anything.

A metric that returns the same answer for every input cannot see the defect. So the first thing to do with any new check is break it on purpose and watch it complain. If it will not complain, delete it or fix it, because leaving it there is worse than having nothing: nothing does not give you confidence.

This applies to the checks you write about your own work too

While preparing this class, a build-time gate was added to a page to guarantee a product name appeared often enough in the visible copy. It counted with a case-sensitive match. The copy it was written to protect was in capitals. The gate counted ten, needed twelve, and failed a page that was already correct — it could not see its own fix.

The rule written down after that one was: a text-matching check must scan only the region where the matched thing can actually do harm. The very next check written after it — on this page — searched the whole document for a style declaration that breaks the contents rail, and fired on the comment explaining that the declaration is deliberately absent. It matched its own documentation.

Nobody is immune to this, including immediately after writing down the rule. The remedy is not care; it is the mechanical test above, applied to your own instruments with the same suspicion you would apply to somebody else's.

And the opposite, which happened while this page was being built

The figures on this page are generated by a script whose gate computes every text block's edge span and refuses to write the file if two overlap or one leaves the canvas. On the first run it refused: one label was landing at x = −11.

It was not an overlap. One call had been written with eight arguments instead of nine, so a text anchor bound to the font-weight parameter and a label name bound to the anchor. A gate built to catch collisions caught an argument-order mistake — because the nonsense anchor fell through to the centred branch and produced an impossible coordinate.

That is luck, and it is worth being honest about: the same mistake forty pixels further right would have passed the gate cleanly and shipped text-anchor="l09-07" into the file. Invalid, silently ignored by every browser, visibly wrong on screen, and complained about by nothing. So the fix was not only the missing argument. It was a check at the layer where the mistake actually happens: the drawing function now refuses an anchor that is not one of the three legal values, and says it is probably an argument-order slip. Put the check where the error is, not only where the symptom surfaces.

SELF-SERVICE · MODULE 04

Nobody is watching the watchers either

Three scheduled jobs keep the reference directory honest. One expires listings whose year is up, one sends renewal notices, one publishes and retires advertisements. All three have run every day without a miss, which is measurable and true and almost beside the point.

All three return HTTP 200 for “I did nothing” and for “I did the right thing.” The scheduler records the status code. Nothing reads the response body. A sweep whose query silently matched zero documents — a renamed field, a changed status string, a collection pointed at the wrong database — would log a contented 200 every morning for a year.

jobs running3 of 3unbroken daily 200
that alert on failure0nothing reads the body
with a backup0no export, anywhere
monitor freshness6 daysreported as 3.5 hours

The freshness check has no freshness check

There is a monitor. Somebody spotted that the pipeline asserted the jobs existed rather than that they had run, and built a proper fix: a step that reads each job's real last-attempt time and posts it to a health endpoint. That fix works. It also runs inside the deploy pipeline, so it only refreshes when somebody pushes code.

THE FRESHNESS CHECK HAS NO FRESHNESS CHECK.GET /health on the live API, read 2026-09-13the endpoint says:"checkedAt": "2026-09-07", "ageHours": 3.5, "ok": trueboth statements were true on 2026-09-07. neither is evidence today.09-07last deploy09-0809-1009-1209-13todaysix days in which /health could not have knownA monitor refreshed by the thing it monitors is not a monitor.
Read live on 2026-09-13. The endpoint reports ok: true and an age of 3.5 hours on the strength of a reading taken on the 7th. Both statements were true when they were written. Neither is evidence today.

The jobs happened to be healthy — that was checked directly, against the scheduler. But /health could not have known that, and would have said exactly the same thing if all three had been dead since the 8th.

The rule, and it generalises past monitoring

A monitor refreshed by the thing it monitors is not a monitor. If a deploy is what updates your view of production, then your view of production is a photograph of the last deploy. The runbook for this system half-knows it — it says “if the scheduler block is null, push a deploy to refresh it.” It handles missing. It does not handle stale, and stale is the dangerous one, because missing looks broken and stale looks fine.

The fix is not a better monitor. It is that the reading must carry its own timestamp, and the consumer must refuse a reading that is too old instead of rendering it. A number without an age is not a measurement.

Six things it would be reasonable to assume

Interactive · Ledgerclaim 1 of 6

Six assumptions, six measurements

Each row pairs something a careful person would assume about this system with what was actually measured on 2026-09-13.

The directory a reader sees is one dataset

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

And there are no backups

No export job, no scheduled snapshot, no lifecycle rule — measured, not assumed. The database holds every paid entitlement and every published listing in the system. Nothing tells you a backup did not happen when nothing is trying to take one. It is the quietest failure on the list because it produces no signal at all, right up until it produces the only signal that matters.

SELF-SERVICE · MODULE 05

The money is the moderation

A directory that lets strangers publish has to answer one question before it writes a line of code: what stands between a submission and the public page? The reference system has three different answers depending on who is paying, and the differences are the most teachable thing in the build.

PathGateHuman before publication?
Paid listingMoneyNo. Validate, geocode, dedupe, publish — one transaction, readable in about a minute
Paid advertisementMoneySoft. Copy lands pending; a person approves it, or forty-eight hours pass and it publishes itself
Letter to the editorNothingYes, hard. A person must approve. There is no auto-publish timer
Event listingNothingYes, hard. Same

Read down that table and the policy states itself: the thing a stranger pays for publishes itself with no human in the loop; the things a stranger can post for free do not publish without one; and the paid thing in between publishes itself after forty-eight unattended hours.

The money is the moderation. Not as a slogan — as the actual security model, stated in the code and defensible in a sentence. A card that clears carries a name, an address and a chargeback risk, which is a meaningfully higher bar than a form anyone can submit. It is not a quality check and nobody should describe it as one.

The moderation file says the quiet part in capitals

The free paths have one rule written at the top of the module that handles them: nothing validated here is ever published by validating. Passing validation moves a submission to pending, never to published. That single sentence removes an entire class of accident — the refactor six months later where a well-meaning change makes a validator's success path also the publish path.

Rejection is a write, never a delete

When a letter is rejected it is marked rejected. It is not removed. That keeps a moderation decision reviewable, and it keeps a spam pattern visible in aggregate — which is the only way you ever learn what is actually being attempted against you. A deleted submission teaches you nothing and cannot be appealed.

What you owe the public, and what it costs

A paid directory publishing real, named businesses takes on obligations that a personal project does not, and they need answering before launch rather than after the first complaint:

  1. A takedown path you can actually execute. Not a promise — a button. In the reference system it is a single administrative endpoint that unpublishes by id, and the fact that it exists and is one call is the reason the rest of the design is defensible.
  2. A visible provenance distinction. A row a business paid to place and a row your research produced are not the same kind of claim, and the page should not present them as though they are.
  3. An answer to “this shop closed.” Directories rot. Deciding in advance who can report it and what happens next is cheaper than deciding during the email.
  4. Refunds by a human. Automate the publication; do not automate the money going back. That one stays manual on purpose.
SELF-SERVICE · MODULE 06

Build it yourself — seven projects

Each one is a working piece of the system above, reduced to what a person can build alone, for their own niche and their own region. They ladder: project 1 is an evening and project 7 assumes the rest. None of them needs the Places API.

1 · The receipt

A signed payment webhook whose signature is verified against the raw bytes, recorded as an entitlement your own system owns. The whole class in one evening.

difficulty 1/5~1 evening12 steps

2 · The claim

A form that redeems an entitlement exactly once, under a transaction, so two simultaneous submissions cannot both win.

difficulty 2/5~3 hours10 steps

3 · The address is real

Geocoding as a hard gate with four named refusals, each returning a sentence the submitter can act on without emailing you.

difficulty 3/5~4 hours14 steps

4 · The clock

Expiry, renewal notices, and a sweep over unredeemed entitlements — the one the reference system still does not have.

difficulty 3/5~4 hours12 steps

5 · The unpaid door

A free submission path that cannot publish itself. Validation moves it to pending, never to live, and rejection is a write rather than a delete.

difficulty 3/5~4 hours13 steps

6 · The unattended publisher

Scheduled jobs that report what they actually did, a health reading that carries its own age, and a consumer that refuses a stale one.

difficulty 4/5~6 hours16 steps

7 · The capstone

The whole loop, proven on every deploy: a smoke test that buys, submits, publishes, verifies and purges against a candidate before any traffic reaches it.

difficulty 5/5~1 weekendassumes 1–6

Project 1, in full

The first four steps exactly as you would type them. The pattern holds for all seven: a prompt, a thing to look for, and a gate before you move on.

1.  Create the project folder and connect it as a workspace.

2.  Create AGENTS.md at the root: what you are building, your payment
    provider, your datastore, and an empty "Known traps" heading you
    will fill in as you go.

3.  Prompt, verbatim:
    "Create a webhook endpoint that receives order-paid events from
     [provider]. Mount a raw body parser on THIS ROUTE ONLY. Verify the
     HMAC over the raw bytes against the shared secret. Return 401 on
     mismatch. Do not parse the body before verifying."

    - you should see:  a route that reads Buffer, not a parsed object
    - if it verifies against JSON.stringify(req.body), it is wrong and
      will fail every real signature - say so and ask again

4.  GATE: send a request with a deliberately corrupted signature and
    watch it return 401. Then send a valid one and watch it return 200.
    Do not continue until you have seen BOTH. A verifier you have only
    ever seen succeed is not a verifier.

Generate your own starting spec

Fill this in for your own stack and download the result. It is the file to hand an agent at the start of project 1.

Interactive · Forgelive

Your entitlement webhook spec

Five answers produce a starter specification for your own directory. Nothing is sent anywhere — the file is built in this page.


Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

Bottom line

A self-service directory is not a form with a payment attached. It is a machine that converts money into published content while nobody is looking, and every design decision follows from the second half of that sentence. Verify the payment yourself. Record a right rather than a result, so a failure is recoverable instead of refundable. Make validation refuse, in a sentence the submitter can act on. Decide out loud what the money buys, because on the paid path the money is the moderation.

And then do the thing almost nobody does: go back through every check you wrote and try to make each one say no. The reference system for this class is real, it works, it takes real money from real shop owners and publishes their pages in about a minute — and half its safeguards turned out to be structurally incapable of refusing anything. Not broken. Not sloppy. Written carefully, by someone trying, and incapable all the same. A check that cannot fail is not a check, and the only way to find out which kind you have is to break it on purpose.

Confidence

Measured
The ten-step path, the six gates and their order, the four end states of a paid order, the three moderation postures, the scheduler behaviour and their unbroken daily runs, the stale health reading, the twelve checks in module 03, and the absence of any backup job — all read from the running system and its source on 2026-09-13.
Asserted
That Claude Fable 5.1 built the reference system, driven from a Cowork session and Claude Code. This is the author's own account of his own build. The repository carries no model marker either way, and the archaeology that went looking could not have found one, because neither surface writes a model name into a repository.
Not verified
How many of the 565 listed shops are still trading today; the current search indexing figures for the reference site; and whether any of the abuse paths in module 05 have actually been attempted rather than merely being possible. Each is named where it appears rather than smoothed over.
Deliberately omitted
Revenue, conversion rates and anything else about money beyond the two published list prices. Two open defects in the reference system are also held back until they are fixed — publishing a live weakness in a site this class links to would be a strange way to teach responsibility.

Questions

What does fully automated self-service actually mean here?

It means the thing a stranger pays for publishes itself with no human in the loop. A paid listing goes validate, geocode, dedupe and published in a single transaction, readable within about sixty seconds. The things a stranger can post for free — letters and events — do not publish without a human approving them. And the paid advertisement sits in between: a person can approve the copy, or forty-eight unattended hours pass and it publishes itself. The money is the moderation. That is the entire security model of the paid path, and it is worth stating plainly rather than pretending a machine is reading submissions for quality.

How does a payment become a live page?

A webhook, not polling and not an export. The store fires an order-paid event at one endpoint, which verifies an HMAC computed over the exact raw bytes that were sent. Two values already on the buyer's receipt carry identity across the payment boundary: the normalised order number and the order email. The webhook does not write a listing. It writes an entitlement — a recorded right to publish one listing — and the listing itself is written later from the form. That separation is what makes the failure modes survivable, because a form that fails can be retried against an entitlement that was never consumed.

What stops someone listing a business that does not exist?

Less than you would hope, and being honest about that is the point. Payment is the first barrier: an attacker must clear a valid paid order number plus the buyer's email on that order, with each unit redeemable exactly once under a database transaction and rate limiting per address. Then geocoding is a hard gate — it refuses an address it cannot find, an address outside the state, and an address it can only resolve to a town centre. What none of that establishes is that a real shop stands at a real address. A directory that takes money from strangers needs a takedown path it can actually execute, and the class treats that as a design requirement rather than an afterthought.

Why record an entitlement instead of writing the listing at checkout?

Because the buyer has not told you what to publish yet. Splitting the purchase from the publication gives you an idempotent webhook — a retry merges into the same document keyed by the order number — and it gives the form a thing to validate against. It also turns three of the four failure modes into recoverable states: if the publish transaction throws, the entitlement is not consumed and a retry works. The cost of the split is the fourth mode, which is genuinely open: a buyer who pays and never returns to the form has bought nothing, and nothing in the system currently goes looking for them.

What is the hardest part of building a self-service directory?

Not the form and not the payment. It is that nobody is awake. Every guarantee you would normally get from a person reading a queue has to be expressed as something that can refuse on its own, and refusing is harder to write than warning. The second hardest part is that your checks quietly stop being checks. In the system this class measures, a deduplication query ran against one collection while the directory was assembled from two, a health endpoint reported a six-day-old reading as current, and a continuous-integration step carried a flag that let it fail without failing the build. Every one of those looked like protection on the day it was written.

What is a check that cannot fail?

A test whose answer does not depend on the thing it claims to test. Three measured examples from one codebase. A merge function documented as resolving identifier collisions, operating on two namespaces that cannot produce a collision — a true statement about a case that can never arise. A recency rule that accepts a page modified within twelve months, evaluated against a header that a dynamic server sets to the current second, so every dynamic page passes regardless of its content. And a scheduled sweep that returns success for did nothing and for did the right thing, so a query silently matching zero rows logs a healthy result forever. The remedy is the same in all three: change the threshold until the check can fail, and if it never can, you have a report rather than a gate.

Do I need the Google Places API to build this?

No, and the class is deliberately built so you do not. Projects one through five use only a payment provider, a database and a geocoder, and the geocoding step has a free tier generous enough that a directory of a few hundred rows never leaves it. Places matters for a different job — confirming that a business you did not create still exists — which is a maintenance problem rather than a build problem. If you do reach for it, understand that the field mask you request selects the billing tier, and the billing tier selects both the rate and the size of the monthly free allowance.

What does something like this cost to run?

At the scale of a few hundred listings and ordinary traffic, close to nothing — the scheduled jobs, the build pipeline and the database all sit inside free allowances, and the front end is static files. The costs that actually bite are not infrastructure. They are the paid API calls you make while researching or verifying records, which is why the reference system runs a hard monthly call cap in code rather than trusting a console quota, and why that cap is the first thing the class teaches you to build.

Which of the seven projects should I start with?

Project one, even if you are experienced, because it is one evening and it teaches the habit everything else depends on: verify a payment yourself before you trust anything the browser told you. It builds the receipt — a signed webhook whose signature is checked against the raw bytes, recorded as an entitlement your own system owns. Projects two through five add the claim, the address gate, the expiry clock and the free-but-moderated door. Project six makes it publish unattended. Project seven is the capstone that proves the whole loop on every deploy.

Was this really built by an AI model?

Claude Fable 5.1, driven from two surfaces — a Cowork session and Claude Code — with a human setting the standing rules, auditing the reports and making every decision about money and publication. That division is not incidental to the result. The most instructive artifact in the whole archive is a scheduled agent that ran a confirmation script, read its own four passing verdicts, found that the rule behind them could not actually fail, and declined to publish anything on the strength of them. It wrote down why instead. An agent that can refuse its own instructions is worth more than one that completes them.

DDS Vibe Academy · Class 92 · Free, no signup All classes · Back to top