Engineering an internet experiment

Building isopod.lol without selling the same plate twice.

A practical backend playbook for a tiny pay-to-own advertising auction using PayPal, Cloudflare Workers, D1, Turnstile, verified webhooks, moderation, and private owner links.

See isopod.lol live →

Why an isopod?

outbid.lol was the main inspiration. Its core idea is extremely legible: people pay for visible position, and money determines who sits above whom. That simplicity caused a wave of generic pay-to-rank boards.

I like isopods, and the name comes from a tiny meme format I already loved: a plain photo of an isopod under a speech or thought bubble saying what the isopod wants. The joke is that this unassuming little creature has oddly specific feelings.

That thought bubble became the product mechanic. Instead of shipping another table where bid number 12 sits above bid number 13, the advertising inventory lives on one glossy isopod. Whoever owns the head becomes what the mascot is thinking about. The other seven brands sit across its shell.

The result is isopod.lol: exactly eight plates on a mascot. The first verified buyer starts a 14-day season. Somebody can steal an occupied plate for the last completed price plus $10. At closing, the final owners remain for 90 days.

This is not a success story yet. The experiment may earn attention and money. It may also become one well-engineered isopod waiting patiently. The useful thing to publish now is the infrastructure and the mistakes found before launch.

Start with product invariants

Write the rules before wiring checkout. For this project, the invariants are:

Those rules become database constraints and transitions, not comments near a button.

The small stack

Cloudflare WorkersPublic site, API, cron reconciliation, management pages, moderation host.
Cloudflare D1Seasons, plate versions, reservations, immutable payments, revisions, webhook receipts.
PayPal Orders v2Create order, buyer approval, server capture, refunds and reversals.
TurnstileProtects reservation creation and appears only when checkout is requested.
Email ServiceSends a private current-owner management link without user accounts.
Cloudflare AccessProtects Ribbon, then the Worker verifies the signed Access identity again.

An approved order is not a paid order

This distinction is the center of the implementation. PayPal can redirect a buyer after approval, but ownership changes only after the server sees and validates a completed capture.

creating
  → awaiting_approval
  → capture_started
  → capture_pending | capture_unknown
  → paid | failed | reversed

Preventing two buyers from winning one version

A frontend-disabled button solves nothing. Two browsers, a webhook retry, and a slow network do not share that button.

Each plate has a monotonically increasing version. Checkout submits the version it displayed. D1 attempts one short reservation for one (season, plate, version). A trigger claims the plate only if the version still matches and the current reservation is absent or safely expired.

When a verified capture arrives, an immutable transaction insert triggers one atomic transition:

  1. validate the reservation, amount, capture, and plate version;
  2. mark the reservation paid;
  3. store the paid amount;
  4. increment the plate version;
  5. preserve the previous transaction ID;
  6. transfer current ownership;
  7. create moderation and activity records;
  8. start the season if it was waiting for its first buyer.

If any validation fails, the statement aborts. The browser never gets to improvise.

Assume the happy path will disappear

A real test produced exactly this failure: PayPal showed a completed purchase, the page stayed pending, Ribbon had nothing to review, and the plate remained locked. The payment was real in PayPal Sandbox. The local finalization had not happened.

The safe response was not to release the plate. Releasing can charge one person and sell the same version to another.

The recovery design uses several converging signals:

The inconvenient rule: when payment state is unknown, preserve the lock until the provider gives a definitive answer.

A refund is a rollback through ownership history

The first full-refund test exposed a provider-shape mistake. A PayPal refund webhook carries the refund ID as its resource ID. The original capture was linked through the resource's rel="up" URL, not the related-ID field used by other payment events. The webhook was valid and marked processed, but the adjustment could not find its transaction. Nothing rolled back.

Refund handling now treats transactions as a linked ownership history rather than deleting a row or subtracting money from the current price.

full refund or final reversal
  → verify and store the webhook once
  → resolve the original capture
  → link the adjustment to its transaction
  → mark that payment reversed
  → if it is still current, restore the latest previous paid transaction
  → restore its paid price and management authority
  → invalidate the refunded owner's management link
  → hide reversed events from the public feed
  → purge the auction cache

A partial refund is recorded but does not transfer ownership until accumulated refunds reach the full captured amount. A refund of an older, non-current transaction cannot overwrite a newer owner. The public ticker reads only activity backed by payments that remain valid; the full event history stays in D1 for private auditing.

Moderation state travels with the restored transaction. Approved content can return immediately. A rejected prior submission is put back into Ribbon as a real review item. Its public label remains Pending review until approval updates that same transaction's activity label.

The clean test: a fresh $49 Sandbox owner replaced the approved $39 owner. The $49 payment was then fully refunded from PayPal. Without replaying a webhook or editing D1, the signed refund event was linked to the right capture, the $49 reservation became reversed, the approved $39 owner returned, the next price returned to $49, and the refunded owner disappeared from the public feed.

Canceling PayPal should not strand inventory

A short reservation prevents double sales, but it creates another failure mode: a buyer can open PayPal, change their mind, return, and see their own plate reported as being in another checkout.

The cancel URL now carries the buyer's signed public order ID. On return, or before the same browser retries that plate, the Worker asks PayPal for the order's current state. It releases the reservation only when local state is still awaiting_approval and PayPal confirms an untouched or voided order. Approved, completed, capture-in-progress, ambiguous, and provider-unreachable states fail closed. If PayPal already has a completed capture, the Worker finalizes it instead of canceling it.

The interface also had to respect plate intent. Clicking the $5 row now selects Plate 06, renders Plate 06 and $5 in the checkout card, preserves policy consent across that rerender, and only then opens PayPal. A real competing buyer gets a clear temporary-hold message rather than a generic error.

Financial ownership is not public approval

Without moderation, the first buyer could instantly publish phishing, hate, impersonation, active SVG content, or a tracking image. A completed payment therefore owns the plate, but the public page shows a safe placeholder until the submitted revision is approved.

Buyers may let the backend fetch and copy their site's declared favicon, or upload a small PNG, JPEG, WebP, SVG, or ICO. The system stores a snapshot rather than hotlinking it. SVGs reject scripts, event handlers, external references, imports, and embedded documents. Descriptions are limited to 150 characters.

Ribbon, the private review dashboard, displays the exact stored logo, destination, description, email, amount, plate, and revision. The operator can approve, reject with a correction reason, refetch an icon, or hide a live placement. Placeholder, review, and activity states all reference the same transaction, so approving one buyer cannot rename another buyer's feed event.

No accounts, but not no recovery

A random management token is generated after payment and emailed to the current owner. D1 stores only its SHA-256 hash. The private link allows details to be submitted or corrected and sends every revision back to moderation.

The link works only while that transaction currently owns the plate and before the final display period ends. Email failure does not roll back payment; a cron retries undelivered management mail with a new token.

Keep abuse from becoming the business model

A build checklist for your version

  1. Define inventory, pricing, tie-breaking, closing, and refund behavior.
  2. Store money as integer minor units.
  3. Version every scarce placement.
  4. Create short server-side reservations.
  5. Use stable provider idempotency keys.
  6. Capture on the server and verify every financial field.
  7. Process signed webhooks idempotently.
  8. Reconcile provider state after ambiguous failures.
  9. Separate payment ownership from moderated public content.
  10. Give buyers a revocable private recovery path.
  11. Model refunds as ownership rollback, not transaction deletion.
  12. Release abandoned checkout only after checking provider state.
  13. Keep private audit history separate from the public activity projection.
  14. Protect expensive writes and bound every fetched/uploaded resource.
  15. Test two simultaneous buyers, duplicate webhooks, abandoned tabs, canceled provider checkout, completed-provider/pending-local state, partial refunds, current-owner refunds, and historical-owner refunds.
Copy the playbook, not the mascot. A reliable state machine is reusable. The orange creature has become emotionally attached to the ribbon. 🎀