gethired

Build log

GetHired is a job board run as a live auction. Every profile opens at $1, being listed is free, and you move up by paying more than whoever is above you. The whole pitch is that the numbers are real — so this log is where the things that went wrong get written down rather than tidied away.

Most build-in-public writing is a highlight reel. This is the opposite: each entry is one thing that broke, with the actual error string and what it cost. If you are building something that takes money, the failures are the only part worth your time — the successes are just the docs working.

Candidates
9
Companies
1
Open jobs
5

Read from the board just now. Nothing on this page is a projection.

What has happened so far

  1. 26 August 2026

    Opened the site to AI readers, properly

    Named twenty-three crawlers explicitly rather than relying on the wildcard, opened the two public JSON endpoints that answer questions better than the HTML does, and published a plain-text map of the site so an agent can read it in one request instead of crawling a dozen pages.

  2. 26 August 2026

    A guard raised two false alarms on its first real run

    We built a checker so the payment failure could never recur. Its first run against the live system reported two catastrophes, and both were its own bugs. A guard that cries wolf is worse than no guard, because the correct response to one you cannot trust is to switch it off.

    The full write-up →
  3. 26 August 2026

    Nobody who joined could ever leave

    There was no way to remove a profile — not for us, not for the people on it. Somebody puts their name on a public hiring board, changes their mind, and has no exit. Now there is one: a hide, not a delete, so the record of a payment survives and a wrong removal is one click from undone.

  4. 26 August 2026

    A payment cleared and the board never moved

    The single worst failure a paid product can have, found in testing rather than by a customer. One mismatched signing secret meant every confirmation was refused as unsigned, and the refund safeguard could not catch it because it lives inside the settlement that never ran.

    The full write-up →
  5. 25 August 2026

    An afternoon lost to copying secrets between dashboards

    Paste, redeploy, still failing, repeat. All of it avoidable: the API returns the value and the CLI accepts it on standard input. The reason we did it the slow way is the more useful lesson — a safety rule written for one situation, applied to a different one, stops being safety and becomes cost.

    The full write-up →
  6. 24 August 2026

    Credentials reached a git remote in thirteen minutes

    A file of pulled production secrets was swept up by an auto-commit hook and pushed. Three separate protections existed and all three missed it, for the same reason: each was watching a path that was not the one used. The fix was a commit hook that reads content, because an ignore rule is a convention and a hook is a wall.

The write-ups

Take the code

Real files from this codebase, each with the reason it looks the way it does. A snippet without its reason gets pasted in and then quietly broken by the next edit.

Verify before you trust the body

The first six lines of any payment webhook.

A forged event on a paid product hands out the thing money buys, for free. This is the highest-value forgery target you will ever deploy, and the check has to happen before anything reads the payload.

const body = await request.text();          // raw, not parsed
const signature = request.headers.get("stripe-signature");
if (!signature) return json({ error: "Missing signature" }, 400);

let event: Stripe.Event;
try {
  event = stripe.webhooks.constructEvent(body, signature, secret);
} catch {
  // Deliberately vague: a forger learns nothing from the refusal.
  return json({ error: "Invalid signature" }, 400);
}

Settlement that survives a retry

Claim the payment before acting on it.

Stripe retries. Without this, a retry grants the same thing twice — a second position, a second credit, a second refund. The conditional UPDATE is the lock: exactly one caller can move the row out of 'pending'.

const claimed = await sql`
  UPDATE payment_intents SET status = 'settling'
  WHERE id = ${id} AND status = 'pending'
  RETURNING *`;

// Lost the race — another delivery of the same event is already handling it.
// Return 2xx anyway, or Stripe keeps retrying a payment already applied.
if (!claimed.length) return json({ received: true, skipped: "already settling" });

Refuse a live key on a public preview

One function, checked before any checkout is created.

A staging site with a live key charges real cards by accident, and staging is the environment you hand to strangers for feedback. Checks the key prefix only — never its value.

export function livePaymentsBlocked(): boolean {
  if (!isPreviewDeployment()) return false;
  const key = process.env.STRIPE_SECRET_KEY;
  if (!key) return false;   // "not configured" is a different message
  return !key.startsWith("sk_test_") && !key.startsWith("rk_test_");
}

Moderation that is undoable

Take something off the site without deleting it.

Somebody paid for that row. Deleting it erases the record of a real payment, and an admin who removes the wrong person at 2am needs one click back. A tombstone, not a grave — plus four gates, all re-checked on the server, because a disabled button is not a permission check.

ALTER TABLE profiles ADD COLUMN IF NOT EXISTS hidden_at bigint;

-- Then EVERY read filters it. Board queries are the obvious ones; the ones
-- that bite are lookups by id, by cookie, by public code — anything that
-- reaches a profile without going through a ranked list.
WHERE ... AND hidden_at IS NULL

All of it is running on the board itself, which is the only test that counts.