Skip to content

PayPal sandbox only. No real money moves.

PayPal hackathon 2026 | documentation, sandbox only

The hammer falls.
Only the winner pays.

Every online auction has one ugly moment: the gavel comes down and the winner has gone quiet. GAVEL asks for the money first. A bid is a PayPal hold, so when the hammer falls we charge the winning price and let everything else go.

These pages are the receipts. Each claim points at a file or a run, and what has only been tried on a fake or a simulation says so.

  • G0 PASS
  • G1 PASS
  • G2 PASS
  • G3 IN PROGRESS
  • G4 PASS
  • G5 PARTIAL

Hammer price, example

Lot 14, 1962 poster

Example only

  • A  Priya Raman | hold $500charged $350

    $150 of this hold is released, not charged

  • B  Tomas Eriksen | hold $300released
  • C  Lena Okafor | hold $200released

cents: held 0 | charged 35000 | released 65000 = 100000

  • Hatched means held
  • Solid means charged
  • Hollow means released

Try it: drag the price, or replay the hammer. Three bidders, one winner; the same rule the ledger follows, in whole cents.

Status, stated plainly

What we can prove, and how.

Numbers come from the committed gate record and the simulation output, read at build time.

Sandbox, PASS

4 of 6

PayPal sandbox gates passed: G0, G1, G2, G4. G3 is in progress, G5 is partial (synthetic audio only).

Simulated

40 of 40

simulated lots settled in the 30 day run, none in review.

Simulated

51

simulated process crashes survived, with no double capture.

Simulated

294

simulated holds, every one captured or released, none left open.

The idea

Commit before the first bid.

Online auctions break at the hammer: the winner may never pay. GAVEL moves the money question to the start. The bidder's ceiling is held by PayPal, and only the winning price is ever charged.

Bidder

approves a ceiling

GAVEL

bids, ledger, saga

PayPal sandbox

holds the money

Five diagrams

The mechanism, drawn from the code.

Each figure uses the product's own language: hatched means held, solid means charged, hollow means released. State and event names are the ones in the source, and each figure says what it was verified against.

1. Where the money is

Money flow: holds, capture and void

G1 on sandbox
Money flow: holds, capture and voidThree bidders approve holds at T0: A for 500 dollars, B for 300 and C for 200. A bids 350 at T1 and wins at the hammer at T2. At T3 GAVEL captures 350 dollars from A with final capture and PayPal closes the remaining 150 dollars. At T4 the holds of B and C are voided. Charged 350 plus released 150, 300 and 200 equals the 1000 dollars held in total.BIDDERGAVELPAYPAL SANDBOXT0 ApproveT1 BidT2 HammerT3 CaptureT4 Voidapprove a ceiling(a PayPal order, AUTHORIZE)A bids $350engine accepts,ledger appendshammer:A wins at $350truth read, thencapture $350final_capturevoid B and C,journal closesA ceiling $500charged $350$150 closedB ceiling $300released $300C ceiling $200released $200bar height is the dollar amount1A hold is an authorization,not a payment. Nothing is charged at T0.2Only the winning price is captured:$350, not the $500 ceiling.3PayPal webhooks and a truth readconfirm each step (signatures: G4 PASS).charged 350+released 150 + 300 + 200=held 500 + 300 + 200 = 1000 dollarsThe 30 day simulation keeps the same equality on its own totals: held $159,385.00 = captured $34,170.00 + released $125,215.00 + open $0.00.That run is SIMULATED (fake PayPal, fake clock). See the proof pages.heldchargedreleasedneeds review

Scroll the figure sideways, or open it full size.

G1 (partial capture with final_capture) passed on the real PayPal sandbox for two PayPal-account buyers. That the remainder is released is inferred (the authorization reads CAPTURED and a later void answers 422 PREVIOUSLY_CAPTURED), not read from a buyer balance. Settlement of a whole auction was run on the fake PayPal. The three bidders and their ceilings are round illustration numbers.
Text version of this diagram
  1. T0 Approve: three bidders approve holds. A holds $500, B $300, C $200. A hold is an authorization, nothing is charged.
  2. T1 Bid: A bids $350. The engine accepts it and the ledger appends a hash-chained event.
  3. T2 Hammer: A wins at $350.
  4. T3 Capture: GAVEL reads the truth of A's hold, then captures $350 with final_capture. PayPal closes the remaining $150 (inferred in gate G1).
  5. T4 Void: the holds of B ($300) and C ($200) are voided. The journal closes.

Invariant: charged $350 + released $150 + $300 + $200 = $1000 held. In the 30 day simulation the same equality holds on larger totals: held $159,385.00 = captured $34,170.00 + released $125,215.00 + open $0.00.

2. The settlement saga

Settlement saga: every state and legal move

Fake PayPal
Settlement saga: every state and legal moveState machine of a settlement. The main path is HELD, BIDDING, WINNER_SELECTED, CAPTURING, CAPTURED, VOIDING, SETTLED. A definite capture failure moves CAPTURING to FALLBACK_RUNNER_UP, which returns to CAPTURING for the next bidder or goes to VOIDING when none is left. Unknown answers, invariant breaks, mismatches and exhausted void retries end in FAILED_NEEDS_REVIEW, which needs a person. Below the states, a journal lane shows that intent is saved before each PayPal call and the outcome after it.HELDBIDDINGWINNER_SELECTEDCAPTURINGFALLBACK_RUNNER_UPCAPTUREDVOIDINGSETTLEDstart_auctionhammerbegindefinite_failnext_candidatecapture_ok / adoptedbegin_voidall_releasedcancel (HELD or BIDDING)no_candidateno_candidates_leftFAILED_NEEDS_REVIEWterminal: a person must look, nothing is retried blindlyinvariantfrom WINNER_SELECTED, CAPTURING, FALLBACK_RUNNER_UPunknown_deadlinefrom CAPTURING: the capture answer stayed unknown for 10 minutesmismatchfrom CAPTURING or CAPTURED: capture amount or identity differs from the planvoid_budget_exhaustedfrom VOIDING: a void still failed after 5 resumesA red corner on a state means it can move here.1One request id per capture, a hashof auction, op, hold, amount andgeneration. Same on every resume.2An unknown answer is not a failure:it waits. Never the next bidder,never a new request id.3Only a definite refusal falls back,after a truth read shows no capture.JOURNAL: written before and after each PayPal callhammer savedWINNER_SELECTEDintent savedcapture + request idPayPal capture callsame id on retrycapture_ok savedCAPTURED, intent clearedbegin_void savedother holds targetedall_released savedSETTLEDheldchargedreleasedneeds reviewbrass top rule: a state where money moves

Scroll the figure sideways, or open it full size.

Verified against the fake PayPal. Not run end to end on the PayPal sandbox as a whole auction; the sandbox runs recorded in docs/GATES.md cover single calls and the nightly specs. States and moves are copied from the transition table in packages/payments/src/saga/machine.ts.
Text version of this diagram

The main path is HELD, BIDDING (start_auction), WINNER_SELECTED (hammer), CAPTURING (begin), CAPTURED (capture_ok or adopted), VOIDING (begin_void), SETTLED (all_released).

  • cancel moves HELD or BIDDING to VOIDING. no_candidate moves WINNER_SELECTED to VOIDING.
  • definite_fail moves CAPTURING to FALLBACK_RUNNER_UP. next_candidate returns to CAPTURING. no_candidates_left moves it to VOIDING.
  • FAILED_NEEDS_REVIEW is terminal. It is reached by invariant (from WINNER_SELECTED, CAPTURING, FALLBACK_RUNNER_UP), unknown_deadline (from CAPTURING, after ten minutes), mismatch (from CAPTURING or CAPTURED) and void_budget_exhausted (from VOIDING).
  • Each transition is one compare-and-set save of the journal. The capture intent is saved before the PayPal call and cleared when the outcome is saved.

3. Crash recovery

Crash recovery: charged exactly once

Fake PayPal
Crash recovery: charged exactly onceTwo timelines. The process timeline runs hammer, truth read, intent saved, PayPal capture call, capture_ok saved, then voiding. Three crash points are marked: A after the intent is saved and before the call, B after PayPal took the capture and before the answer is saved, C after capture_ok is saved. The journal timeline shows the intent as a hatched entry and the outcome as a solid entry. Three panels say what the journal holds and what the restart does at each point. The result is one capture.heldchargedreleasedneeds reviewPROCESS TIMELINEhammertruth readintent savedPayPal capturecapture_ok savedvoid, SETTLEDABCX: the process diesJOURNAL TIMELINE (what survives the crash)INTENT capture $350request id gv1-...capture_ok, winnerintent clearedno write1CRASH ADied after the intent was saved,before PayPal got the callJOURNALintent saved: capture, $350,request id. State: CAPTURING.RESTARTreads the truth of the hold first.PayPal shows no capture.DOESsends the saved intent again,same request id, answered once.2CRASH BDied after PayPal took the capture,before the answer was savedJOURNALthe same intent, no outcome.RESTARTreads the truth first: it showsour capture (id and amount match).DOESadopts it as capture_ok.No new capture call.UNKNOWNtruth unreadable or hold unknown:wait, same id, 10 minutes, thenFAILED_NEEDS_REVIEW. Never thenext bidder.3CRASH CDied after capture_ok was savedJOURNALstate CAPTURED, winner recorded,intent cleared.RESTARTreads the state: already CAPTURED.DOESgoes on to void the other holds.No capture call is made.Charged exactly onceWhere this lives: packages/payments/src/saga/capture.ts (truth first, write-ahead intent),saga/unknown.ts (the 10 minute deadline), saga/machine.ts, packages/payments/src/requestId.ts.

Scroll the figure sideways, or open it full size.

Verified with injected crashes at these boundaries against the fake PayPal (the simulation scenario S6 runs 10 crash points, each with and without the idempotency table). One in-process crash at the same place (spec R10) passed on the real sandbox in the nightly runs of 6 and 7 Nov (redacted reports committed). No real server kill and restart has run on a hosted host.
Text version of this diagram

The capture sequence is: read the truth of the hold, save the intent (capture, amount, request id), call PayPal, save capture_ok.

  • Crash A, after the intent is saved and before the call: on restart the truth shows no capture, and the saved intent is sent again with the same request id.
  • Crash B, after PayPal took the capture and before the answer was saved: on restart the truth shows the capture, which is adopted. If the truth cannot be read, or PayPal does not know the hold, the settlement waits with the same request id for ten minutes and then goes to FAILED_NEEDS_REVIEW. It never tries the next bidder.
  • Crash C, after capture_ok was saved: the state is already CAPTURED, so the restart goes on to void the other holds without a capture call.

Result: the winner is charged once.

4. Voice, and the confirm gate

Voice pipeline and the confirm gate

G5 PARTIAL
Voice pipeline and the confirm gateAudio goes from the microphone through a segmenter, speech to text, a parser and a decision step. The decision is accept, confirm or drop. Confirm goes to the confirm gate: the bidder is asked a question, yes places the bid after it is checked again, no or expiry places nothing. Drop is logged and places nothing. Manual entry and tap to bid skip speech and enter at the engine. A dashed path shows that a clean, high confidence parse can be accepted without the gate; the default build keeps scores below that line.MICROPHONEaudioSEGMENTERspeech segmentsSTTcached by defaultPARSERamount, paddleDECIDEscore, classifyconfirmCONFIRM GATEthe bidder answersbefore the engine sees itENGINEthen ledgeraccept: skips the gate (two parsers agree, 0.85 or more)not the default build: its scores stay under 0.85dropnot a bid, or too uncertainlogged, nothing placedPrompt to the bidder"Did you mean $150for paddle 12?"yes: checked againagainst the live priceno, or 15 s passnothing is placedloggedManual entryTap to bid (QR)skip speechheldchargedreleasedneeds review

Scroll the figure sideways, or open it full size.

Read from packages/voice/src/decide/policy.ts, decide.ts, score.ts, packages/server/src/voice/bridge.ts and room/confirm.ts. The dashed red path is what the code allows, not what the default build does: with one parser the score tops out at 0.82, so a spoken bid confirms. Accuracy numbers are in the table below the figure, not drawn here.
Text version of this diagram
  1. Microphone audio is cut into segments.
  2. Speech to text turns a segment into a transcript. The default build plays cached transcripts; a live provider (Groq or Deepgram) runs only when VOICE_STT is set with its key.
  3. The parser reads amount, paddle and intent from the transcript.
  4. The decision step merges parser opinions, scores confidence and validates the bid against the room. The result is accept, confirm or drop.
  5. Confirm: the bidder is asked, for example "Did you mean $150 for paddle 12?". Yes places the bid after it is checked against the current price. No or expiry places nothing and is logged.
  6. Drop: nothing is placed, the decision is logged.
  7. Accept (dashed): a clean parse with confidence of 0.85 or more that both parsers agree on skips the gate and goes to the engine.
  8. Manual entry by the auctioneer and tap to bid skip speech and enter at the engine.
  9. The engine accepts or rejects the bid and an accepted bid is appended to the hash-chained ledger.

Gate G5 is PARTIAL: wrong-accept 0.000 to 0.000 on 24 synthetic, noise-free clips per set (limit 0.05), after fixes chosen on those same clips. No real-room or noisy-room test exists, and spoken bids always confirm.

5. Thirty simulated days

30 day simulated timeline

SIMULATED
30 day simulated timelineEight simulated scenarios on a day axis from day 0 to day 32, run on a fake PayPal with a fake clock. Each row shows holds as hatched bars and captures as solid blocks. Dashed lines mark the model's 3 day honor period and 29 day validity. The bottom shows the totals of scenario S8 and that the ledger balances. This is a simulation, not real traffic and not the PayPal sandbox.SIMULATED: fake PayPal, fake clock. Not real traffic, not sandbox.Day 0Day 4Day 8Day 12Day 16Day 20Day 24Day 28Day 32honor period ends, day 3 (model)valid until day 29 (model)S1 hold day 0, capture day 16/6 PASSS2 close day 2.5, inside honor period6/6 PASSS3 capture day 3.5, reauthorized8/8 PASSS4 reauth day 3, capture day 55/5 PASSS5 never reauthorized, day 306/6 PASSS6 crash and restart at day 210/10 PASSS7 late expiry webhook, day 315/5 PASSS8 40 lots, 100 bidders, crashes7/7 PASSdiamond: reauthorize. cross: process crash. solid: capture. hollow block: hold released or expired. hatched bar: hold open.S8 TOTALS (read from SIM-MULTIDAY.md)40 lots, 100 bidders: 40 settled, 0 in review. 294 holds, 94 reauthorize calls, 51 crashes survived, 31 PayPal faults armed.3 lots used the runner-up. 8 of 8 injected rate-limit storms settled after a restart.balances to the centheld $159,385.00 = captured $34,170.00 + released $125,215.00 + open $0.00Fault positions inside the 30 days are random (seed 20261107) and are not in the file, so they are not drawn.heldchargedreleasedneeds review

Scroll the figure sideways, or open it full size.

What this shows: the settlement code kept its invariants under injected crashes and faults, on a model of PayPal. What it does not show: real PayPal behaviour over days. That is gate G3, still in progress. The honor period (3 days) and validity (29 days) are assumptions of the model, not PayPal facts. Figures are read from docs/evidence/SIM/SIM-MULTIDAY.md (seed 20261107).
Text version of this diagram

Everything here is simulated: a fake PayPal and a fake clock.

  • S1: hold day 0, capture day 1. 6/6 checks, PASS.
  • S2: close day 2.5, inside honor period. 6/6 checks, PASS.
  • S3: capture day 3.5, reauthorized. 8/8 checks, PASS.
  • S4: reauth day 3, capture day 5. 5/5 checks, PASS.
  • S5: never reauthorized, day 30. 6/6 checks, PASS.
  • S6: crash and restart at day 2. 10/10 checks, PASS.
  • S7: late expiry webhook, day 31. 5/5 checks, PASS.
  • S8: 40 lots, 100 bidders, crashes. 7/7 checks, PASS.

S8 ran 40 lots: 40 settled, 0 in review. 294 holds, 94 reauthorize calls, 51 process crashes survived, 31 PayPal faults armed, 3 lots used the runner-up fallback, 8 of 8 injected rate-limit storms settled after a restart.

Ledger: held $159,385.00 = captured $34,170.00 + released $125,215.00 + open $0.00.

Proof

Six gates against the real sandbox.

Source of truth: docs/GATES.md. A gate closes only with a recorded result.

GateQuestionResultRun
G0sandbox app and buyer accountsPASS2026-11-04 by harshbhx (workflow gate-g0, run 41815593520, branch gate/g0)
G1partial capture and remainder releasePASS2026-11-04 by harshbhx (wallet); 2026-11-05 by harshbhx (card, run G1-20261105T123116Z)
G2card and guest AUTHORIZEPASS2026-11-05 by harshbhx (owner laptop, run G2-20261105T115228Z)
G3honor period versus validity and reauthorizeIN PROGRESSReal probe. Day 0 was 7 Nov 2026 03:06 UTC. Final reading about 12 Nov 06:17 UTC.
G4real webhook signature verificationPASS2026-11-05 by harshbhx (workflow gate-g4, run 41947655945)
G5STT accuracy on noisy bidsPARTIAL2026-11-07, cached eval over recorded provider outputs (PR #1940, issue #1864; second clip set PR VOI-01; error analysis, parser fixes and option trials PR VOI-02)

Behaviour that no passed gate covers is established only against the project's own fake PayPal. Read each gate.

FAQ

Questions, answered straight.

Does any real money move?

No. GAVEL talks to the PayPal sandbox only, and production mode refuses the fake PayPal. The whole site carries the sandbox notice on every page.

What does a hold actually do?

A bidder approves a PayPal authorization for their ceiling. It is a promise, not a charge. At the hammer the saga captures the winning price only; the rest of the money is released.

What if the server crashes during settlement?

The saga journals its intent before each PayPal call and, on restart, reads the truth at PayPal first. That recovery is verified with injected crashes against the fake PayPal; no real server kill has run on a hosted host.

Does voice bidding work?

It exists, but the speech accuracy gate (G5) is only PARTIAL: wrong accepts are 0.000 on 24 synthetic, noise-free clips after fixes chosen on those same clips, and there is no real-room test. Spoken bids always wait for a confirmation. No live speech run is recorded.

Is there a hosted demo?

Yes, in sandbox mode: the web app is at gavel-web.creharsh.workers.dev and the API at gavel-auction-aufz.onrender.com. Storage is temporary until a disk is added, and nobody has walked the whole flow on it yet. The demo guide lists what is verified.

Read it the way a judge would.

Five minutes: the judge path, the proof page, then the crash recovery page.