Best practices
Everything here is written for one specific thing: a public leaderboard where position is bought, settled by a card payment, under traffic you did not plan for.
Read them in order the first time. They build on each other — the auction rule you pick in 01 determines the schema in 03, and the schema is what makes the webhook in 02 safe.
| # | File | What it covers |
|---|---|---|
| 1 | Auction mechanics | Cumulative vs highest-bid, why you must never promise a rank at checkout, total sort orders, variants |
| 2 | Payments and webhooks | Signature verification, double idempotency, status codes, chargebacks, payout holds, refund policy |
| 3 | Data model and concurrency | Ledger-first schema, atomic increments, keyset pagination, click buffering, reconciliation |
| 4 | Surviving the spike | Caching the one query, keeping payments alive under load, realtime without regret, analytics that hold |
| 5 | Abuse and moderation | Post-payment bait-and-switch, SSRF, XSS, takedown policy, impersonation, card testing |
| 6 | Legal and trust | Paid-placement disclosure, when a variant becomes gambling, EU/UK withdrawal rights, the four pages to publish |
| 7 | Launch and distribution | Seeding, per-listing OG images, building in public, why cloning the domain fails |
| 8 | After the spike | Decay, telling bidders the truth, three honest endgames, obligations that outlive the hype |
The short version
If you read nothing else:
- The webhook is the only thing that grants rank.
success_urlis a redirect, not a payment. - Never promise a position at checkout. Sell a contribution; let the sort place it once the money is real.
- Idempotency at two levels — event id and PaymentIntent id — enforced by
unique constraints, not by a
SELECTfirst. - Money is integer cents in an append-only ledger. Totals are derived and reconcilable.
- One atomic
UPDATE ... SET total = total + n. Never read-modify-write. - Total, stable sort order.
total_cents DESC, first_paid_at ASC, id ASC, paginated by keyset with a seek predicate. - Lock the URL after payment. Otherwise you sell the top of a viral page to a phishing kit.
- Cache the board hard, and keep the payment path off the read path's resources.
- Publish rules, refunds, takedowns and who you are before taking a dollar.
- Traffic is the product. Cloning the code copies the cheap half.
Reference implementation
The patterns above, as code you can read in ten minutes:
reference/schema.sql— the three tables,credit_bid(), click buffering, and the board queries.reference/create-checkout.ts— amount validation, URL normalisation, and the rank promise deliberately not made.reference/stripe-webhook.ts— signature verification, event routing, status codes.reference/pre-launch-checklist.md— the list to run through the night before.
They are illustrative, not a framework. Copy the ideas, not the imports.
Anti-patterns
Every one of these has shipped, in public, on a board taking real money.
| Anti-pattern | What happens | Fix |
|---|---|---|
Granting rank on success_url |
Free ranks for anyone who reads a URL | 02 |
| Parsing the body before signature verification | Every webhook fails in prod, all of them pass locally | 02 |
SELECT then UPDATE on the total |
Silently lost payments under exactly the load you wanted | 03 |
| Enforcing "must beat #1" at checkout | Refund-or-lie, during your best hour | 01 |
ORDER BY total DESC with no tiebreak |
Board reshuffles on refresh; pagination duplicates and skips | 01 |
LIMIT/OFFSET pagination |
Listings appear twice or vanish while the board moves | 03 |
| Keyset pagination with no seek predicate | Correct rows, but deep pages scan the whole index — 769 buffers vs 7, measured | 03 |
| Editable URL after payment | You sold the top of a viral page to a phishing kit | 05 |
Re-payment resets status to active |
Money buys its way past your own moderation | 03 |
click_count = click_count + 1 per redirect |
The hottest row becomes a global lock | 03 |
| Server-side favicon fetch with no IP validation | An HTTP client inside your VPC, for $5 | 05 |
| Floats for money | 10.10 * 100 === 1009.9999999999999 |
01 |
| Unrecognisable statement descriptor | The most common — and most preventable — dispute reason | 02 |
| No refund policy until the first refund | You improvise it under pressure, in public | 02 |
| Revenue sharing promised in launch week | Your funds are under review and you cannot pay | 02 |
| Fabricated seed bids or inflated click counts | Fraud, trivially caught by comparing board to ledger | 07 |
| Taking the board down after the hype | A wave of chargebacks and no credibility for the next launch | 08 |
| Shipping the same board on a new domain | Clone #40's traffic curve is flat | 07 |
Boilerplates and tooling
- Outbid boilerplate — extracted from a production board and trimmed to a starting point.
- Bidkit — hosted "launch a bidding platform" tooling.
- supastarter — the Next.js + Postgres boilerplate outbid.lol itself was built on.
- Stripe Checkout + webhook docs — read the idempotency and signature-verification sections specifically.
- Stripe Tax — destination-based VAT on digital services is not a thing you want to hand-roll.
Before reaching for a boilerplate, read 2. Payments — most of these ship the happy path and leave idempotency, dispute evidence and URL locking to you.
Writeups and analysis
- Inside outbid.lol: the pay-to-rank board taking over tech — the most technical account: concurrency handling, webhook flow, the three-hour build.
- Why the pay-to-rank board went viral — why clones with no audience earn nothing.
- The .lol bidding directory frenzy of August 2026 — taxonomy of the variants.
- A dead-simple website that made $100K in under 48 hours — the timeline as it happened.
- $139,041 in 65 hours — the numbers.
- Hacker News discussion
- How outbid.lol works, and why the price only goes up
- Is it legit, and is a spot worth paying for? — the bidder's side, which is worth reading if you are selling to them.