GAVEL: every bid is a hold, the hammer is the payment. PayPal sandbox only.

GAVEL: every bid is a hold, the hammer is the payment.

An auction where the bid is a PayPal hold, the hammer is the payment, and the record can be checked by anyone.

Inspiration

Picture a charity night. The auctioneer calls 200 dollars for a signed print. A woman near the back has her hand half raised, and she lowers it. Her card would be charged the moment she bids, and she is not sure she wants the print that much. Across the room, a man bids 400 with a grin, wins, and is never heard from again. The organizer spends a week chasing an invoice, with a paper sheet as the only record.

PayPal already has the answer in its payments model. You can authorize an amount, which holds it, and capture later, which takes it. That is the shape of an auction. Hold a ceiling when a bidder joins, let them bid anywhere under it, and at the hammer capture only the winning price while the other holds go back untouched.

A price counts up to $20, then an orange line sweeps across it: Held, not charged.

A price counts up to $20, then the line "Held, not charged" sweeps across it.

What it does

GAVEL turns that idea into a working room. Everything runs in the PayPal sandbox; no real money moves.

  • Hold. A bidder approves a ceiling as a PayPal authorization. It is held, not charged.
  • Bid. A deterministic engine runs an ascending English auction with a minimum increment and a soft close. A bid above the ceiling is refused. Bids come from phones, a QR code or the console. The auctioneer can also speak a bid into the console microphone; it waits for the bidder's confirm.
  • Hammer. The auctioneer holds a button to close the lot. GAVEL captures only the winning price from the winner's hold and voids every other hold. Until that capture, money is held, not charged.
  • Receipt and verify. Every accepted bid goes into a hash-chained ledger that anyone can check in the browser.
  • Voice. The auctioneer speaks a bid into the console microphone (tap to start and stop, or press and hold). It is parsed, validated and then waits for the bidder's confirm ("Did you mean 350 for paddle 12?"). The microphone works when the server has a speech provider, a bidder phone has no microphone, and voice was tested, so a judge can also bid by tap, QR or typed entry.

Four steps, Hold, Bid, Hammer, Receipt, each lit in turn while the real GAVEL screen for that step wipes in beside it.

Hold, bid, hammer, receipt: the four steps, with the real screen for each.

Animated slide: how it works, in four moves: hold, bid, hammer, receipt.

How it works: a ceiling is held, bids come in under it, the hammer captures the winning price, and the receipt closes the lot.

Try it in 60 seconds, no account

  1. Open the judge panel and start a demo room.
  2. Join the room as a bidder from the hub and register a sandbox hold.
  3. Bid, then press and hold the hammer on the console.
  4. Read the receipt, then open the audit page and verify the ledger chain in your browser.
  5. The scripted demo plays a recorded room in about a minute.
  6. Prefer HTTP? Use the shared judge key in the Devpost testing instructions (optional, the steps above need no key) and follow Call the API as a judge: open a room, bid, hammer and read the ledger with curl. The live Try it panel is on that page, the API reference lists every endpoint with code samples, and the docs cover the rest.

Animated slide: the real room at /try and the scripted demo.

Two ways in for a judge, no login.

Animated slide: do not trust, verify. Every accepted bid goes into a hash-chained ledger that anyone can re-check.

Do not trust, verify: every accepted bid goes into a hash-chained ledger, and anyone can re-check the public export. Hashes shown are shortened examples.

How we built it

GAVEL is a TypeScript monorepo (Node 22, npm workspaces) started on 4 Nov 2026, with a Python reference implementation used for differential tests of the engine and the parser.

  • A deterministic engine. packages/engine holds the rules, commands, events and the ledger, with no Node built-ins, so the same events always give the same result. Money is integer cents.
  • A tamper-evident ledger. Each entry carries the hash of the one before it; an edited entry is marked as edited.
  • PayPal holds with a journaled settlement. packages/payments uses Orders v2 with intent AUTHORIZE and Payments v2 for authorize, capture, void and reauthorize, JS SDK v6 and signature-verified webhooks. The settlement saga writes a journal. If the process dies mid-settlement, it asks PayPal what is true and continues instead of repeating. An unknown answer stops for review.
  • Voice with a confirm step. Audio goes to Deepgram (nova-3) with Groq (whisper-large-v3-turbo) as failover, then a rule-based parser and a validator.
  • Cloudflare Workers and Render. The web app and docs site run on Cloudflare with security headers. The API and WebSocket host is one always-on Node service on Render with a 1 GB disk for the ledger and settlement journals.
  • An AG Grid ledger lab. The audit ledger, console bid history and holds are AG Grid Community tables with filters, CSV export and an in-browser chain check.
  • A judge API. Per-judge keys open a sandbox room over HTTP. The OpenAPI 3.1 spec is linted in CI and checked against the router. Beta SDKs generated from it with APIMatic are a GitHub release, sandbox only; there is no hosted developer portal.
  • Open evidence. Five Hugging Face datasets and a read-only Evidence Explorer, each labelled for what it measured, with sha256 manifests. Browse them in the Hugging Face collection or the Evidence Explorer.
  • Hardening. A 24-item security review (all eight P0 items pass, residuals named for the rest) and red team and blue team rounds.

A line diagram: Cloudflare Workers, Render, PayPal, AG Grid and Hugging Face joined by thin lines with a packet moving along each.

The stack: Cloudflare Workers, Render, PayPal sandbox, AG Grid and Hugging Face.

Animated slide: the architecture from the browser through a Cloudflare Worker and the Render API to PayPal sandbox and the hash-chained ledger.

One bid, four hops through the stack.

Evidence before claims

We kept a gate file (docs/GATES.md) and let nothing count until a real run backed it. Status today:

  • G0, G1, G2, G3 and G4 pass on the real PayPal sandbox: app and buyers, partial capture with final_capture, wallet and guest-card holds, and six real webhooks verified.
  • G3 passes: a hold stayed capturable for 5 days. A real sandbox probe started 7 Nov 2026 03:06 UTC (wallet buyer, partial capture of 1.00). After 3.5, 4 and 5 days a 1.00 capture was accepted and a reauthorize was accepted each time; a reauthorize on day 0 was refused as too soon (422 TOO_SOON). Run: https://github.com/harshbhx/gavel-auction/actions/runs/43174780092
  • G5: tested, with a confirm step on every voice bid. Voice bids were tested on synthetic speech clips, not by a person in a room (Deepgram 11 of 11 and Groq 9 of 11 scripted bids parsed exactly), and the confirm prompt protects every voice bid.
  • The 30-day settlement run on a fake clock is labelled simulated. The real versus simulated page says which part is which.

Animated slide: open datasets and gate status, G0, G1, G2, G3 and G4 pass, G5 tested.

Open data and the gate status.

AI in the product

Runtime AI. AI is used for voice bidding. Audio from the auctioneer's console microphone goes to speech-to-text: Deepgram (model nova-3) with Groq (model whisper-large-v3-turbo) as failover. A rule-based parser turns the transcript into a structured bid, and a deterministic validator checks it against the auction rules. The model never decides a winner and never moves money, and every spoken bid waits for a confirm.

AI tools used while building. AI coding assistants, for coding help and documentation drafts.

PayPal, Cloudflare Workers, Render, Hugging Face and AG Grid in large type, then the rest of the stack in small type.

The partners in large type, the rest of the stack below.

Built with the best: PayPal for payments, Cloudflare for web and docs, Render for API hosting, AG Grid for data tables, Hugging Face for open datasets.

Who does what: PayPal payments, Cloudflare web and docs, Render API hosting, AG Grid data tables, Hugging Face open datasets.

Challenges we ran into

Correctness costs more than the happy path. Findings that mattered lists the red-team bugs it found, with fixes and tests.

  • Holds must be idempotent. A retry or a double tap must not make a second hold. Webhooks also arrive twice or out of order, so handlers de-duplicate and the saga never trusts one event alone. Real events carried fields our simulator lacked, so we test against six sanitized real fixtures.
  • Void on loss, and what "released" means. After a final capture, a follow-up void answers 422 PREVIOUSLY_CAPTURED and the authorization reads CAPTURED. We treat that as the remainder being released, and we say plainly that it is inferred, not read from a buyer balance.
  • The soft close. Two bids landing together must get one ordered answer: one accepted, one refused as not higher.
  • How long a hold lasts. PayPal's pages disagree (3 days versus 29, with one reauthorization). Our 5-day sandbox probe passed: a hold was still capturable at 3.5, 4 and 5 days and reauthorize was accepted. The probe used a wallet buyer, so demo holds are still made shortly before the capture.
  • Voice in noise. Speech-to-text gets less exact when the room is noisy, so a spoken bid never counts until the bidder confirms it, and every voice bid shows what was heard before it counts. We tested with 120 noisy clips.
  • Flaky CI and honest numbers. Browser and sandbox runs flake, so we separated a real failure from a flaky one before trusting a green run, and every figure here names what it measured. The sandbox also needs US accounts and USD.

Two $25 bids from paddle 12 and paddle 7 arrive together. One is accepted as ledger entry 41, the other is refused as not higher.

Two bids arrive together: one is accepted, one is refused as not higher.

Accomplishments that we're proud of

  • A real server process was killed after the capture request and before the response. On restart with the same data folder it settled with one capture of 12500 equal to the hammer price, two losers voided, a journal equal to PayPal and a ledger of 19 entries that verifies. That ran on a CI runner, not on the deployed host (evidence).
  • Live speech through the audio socket: both providers answered 24 of 24 test clips; no bid was accepted without a confirm (run, evidence).
  • The hosted judge flow passed all 17 steps on the live domains (evidence).
  • PageSpeed Insights scored the landing page 100 for Performance, Accessibility, Best Practices and SEO on both mobile and desktop (lab data, 10 Nov 2026, landing page only, not the whole app).
  • More than 4,500 test cases run in CI, and all eight P0 security items pass (security review).
  • Audit waves found and closed real bugs: as of 9 Nov 2026, 172 deep-dig issues and 597 red-team issues are closed on GitHub (the deep-dig round was still running).
  • Every change went through a pull request and CI: 727 merged pull requests and 1,528 issues closed as completed, as of 11 Nov 2026.

Three results in large type: kill survived, voice 24 of 24 (confirm before every bid), chain verifies.

Kill survived, voice 24 of 24 clips, chain verifies.

What we learned

  • A hold is a better auction primitive than a charge: a refused bid is a fact, not a guess.
  • During settlement, never trust one source of truth. Ask PayPal, journal every step, and treat "unknown" as a stop.
  • Label every number with what it measured. We kept real sandbox and simulated results apart.

The same event arrives twice. It is counted once as entry 41 and the duplicate is struck out.

The same event arrives twice and is counted once.

What's next for GAVEL

  • Set the reauthorize behaviour in the app from the G3 result.
  • Test the console microphone with a person in a real room and in recorded noise.
  • More payment methods through the same hold and capture shape.
  • An organizer dashboard: lots, bidders, settlement and the ledger in one view.
  • Repeat the process kill on the deployed host.

Three lines: the G3 follow-up, real-room voice, and a kill on the deployed host.

Next: reauthorize behaviour from G3, real-room voice, a kill on the deployed host.

Read more and proof

Team

Two builders.

  • Harsh Bhanushali (harshbhx): the auction engine, PayPal payments and settlement, voice, evidence, and operations.
  • Dhairya Patel (DhairyaPatel-Devloper): the web UI, accessibility, UX polish, the end-to-end scenario runner, and deploys on Render.

Try it

Animated closing slide: the logo assembles, the links click in, then the credits: Harsh Bhanushali and Dhairya Patel.

Closing slide: web app, docs, source and the two builders.

PayPal sandbox only. Bids are held, not charged, and no real money moves anywhere in this project.

Built With

Share this project:

Updates

Submission history