Bid lifecycle
From a tap or a spoken bid to an accepted or rejected bid, with the confirm gate in the middle.
A bid in GAVEL is a small message with an amount in cents. It can come from a tap on a phone or from speech in the room. Either way it passes the same short chain: the room decides whether it needs a human yes, the engine decides whether the bid is legal, and only then does it reach the event log and the other screens.
Verified against the code, not measured
This page describes code paths that were read in the repository. It makes no speed or accuracy claim. Speech accuracy is a separate, still partial gate: see the note on G5 below.
In plain words
- You tap a number, or you say "paddle twelve, three hundred".
- The room checks that it is really you and that you are not sending too fast.
- If the bid came from speech and the system is not sure, it holds the bid and asks you "Did you mean $300?" for up to 15 seconds. Nothing has happened to the auction yet.
- The engine checks the rules: is the lot open, are you registered, is the amount under your ceiling, is it high enough.
- The result is written to the log first. Only then are you told, and everyone else sees the new price.
Step through it
Bid lifecycle: from a tap or a spoken bid to an accepted or rejected bid
Scroll the figure sideways, or open it full size.
1. Two ways in. A tap sends a bid frame with source qr. Speech goes through the voice bridge: the pipeline classifies each utterance as accept, confirm or drop. Accept and confirm become a bid frame with source voice (a confirm sets ambiguous); a drop is recorded as voice_dropped and never becomes a bid. As the bridge is wired today no second parser is attached, so a clean reading scores at most 0.82, below the 0.85 auto line: every spoken bid arrives as a confirm.
packages/server/src/room/room.ts (submitBid, handleBid, holdForConfirm, respondConfirm), room/confirm.ts, packages/server/src/voice/bridge.ts, packages/voice/src/decide/policy.ts and packages/engine/src/rules/validate-bid.ts. This is a map of code paths, not a measurement. Tap bids and confident voice bids skip steps 4 and 5. Hatched band: held, the engine has not seen the bid.Text version of this diagram
Actors, left to right: bidder, voice bridge, room, engine, event log.
- A tap sends a bid frame with source qr. A spoken bid is classified by the voice pipeline as accept, confirm or drop; accept and confirm become a bid frame with source voice, a drop becomes a voice_dropped record.
- The room rate-limits the sender and takes the bidder identity from the token.
- A repeat of the same message id gets the original reply; the same id with another amount is a conflict.
- A voice bid that is ambiguous or under the room threshold (0.85 in the demo config) is held. confirm_requested is logged and only that bidder is asked, for 15 seconds. The engine is not touched.
- The bidder accepts, rejects, or the hold expires.
- On accept the room sends PlaceBid with confirmed set; the resolution and the bid are one append.
- The engine checks in order: lot open, registered, ceiling, amount, voice confirmed, over ceiling, reserve, increment, already leading, limits.
- The result is appended with the expected seq, then the sender is told. Accepted bids broadcast price and a redacted audit event.
Two ways in
The web app sends a bid frame with source: "qr", a fresh message id (mid) and the amount in cents. The bidder is whoever the session token says it is. A tap is never held: it goes straight to the engine.
If the network drops before an answer, the client marks the bid as unknown after four seconds and lets the bidder send it again with the same mid, so a retry cannot place the bid twice.
The confirm gate
The gate sits in the room, before the engine. A frame is held when it is voice-like (source voice, or it carries a transcript, a confidence or an ambiguity flag) and either the parser flagged it ambiguous or its confidence is under the room threshold. A missing confidence counts as zero. Labelling a spoken bid as qr does not escape the gate.
Prop
Type
A held bid is not seen by the engine. The auctioneer's console never sees its amount or transcript either, because the bid might still be refused as over the bidder's ceiling and the amount would leak it.
On accept the room replays the held bid through the normal path at the server time of the answer, with audit.confirmed set. The resolution and the bid are written in one append, so the hold is consumed only if the bid is written. On reject, a late answer or the timer, the hold ends as rejected or expired, an audit row is written and the bidder is told the bid was not placed. Code: respondConfirm, resolveConfirm and armConfirmTimer in packages/server/src/room/room.ts.
The engine does not trust the room. A voice bid whose confidence is under confirmBelowBps and that is not marked confirmed is rejected as unconfirmed, and a voice bid with no confidence counts as zero. Only the server may set audit.confirmed. So a bug in the room cannot turn an unconfirmed low-confidence spoken bid into an accepted one.
The room remembers the reply for each bidder and message id. A repeat with the same amount and source gets the original reply and appends nothing; the reply is sent again only once the log holds the bid. The same id with another amount is a conflict. Replies are kept in a bounded map.
A refusal that costs nothing to decide is answered to the sender only, with no log entry: an unregistered bidder, a bad amount, a paused lot, and an auctioneer's manual bid beyond the next increment. Otherwise any joined client could grow the log at its rate limit. Other refusals are written as bid_rejected, up to a per-lot cap.
What the engine checks
The engine runs these checks in a fixed order and reports the first failure. The wire code is what the sender sees; several engine reasons share one code.
| Order | Engine reason | Wire code | Retryable |
|---|---|---|---|
| 1 | lot_not_open (also: lot paused, deadline passed) | closed | no |
| 2 | unregistered | unregistered | no |
| 3 | ceiling_revoked | unregistered | no |
| 4 | bad_amount | bad_amount | no |
| 5 | unconfirmed | bad_message | no |
| 6 | over_ceiling | over_ceiling | no |
| 7 | below_reserve | below_reserve | no |
| 8 | below_increment | below_increment | no |
| 9 | already_leading, not_higher_than_own_max, already_bid | already_leading | no |
| 10 | limit_exceeded | room_cap | no |
Before the engine, the room can also answer rate_limited (retryable), forbidden, auction_full, conflict and not_found. While the clock is paused the room answers auction_paused, which is retryable with the same mid.
Manual bids are narrower
An auctioneer can enter a bid for a bidder, but only up to the next minimum, the bidder must confirm it first, and one auctioneer can enter at most 25 per lot. A refusal over the bidder's ceiling is reported as limit_exceeded so the amounts cannot be used to probe a ceiling.
After the engine accepts
- The room builds
bid_acceptedwith a fresh bid id and appends it to the log with the expected sequence number. If another host already owns the room, the append fails and this host stops serving it. - The sender gets
bid_acceptedonce the log holds it. If persisting fails the sender gets a store error instead, never an acknowledgement that is ahead of the log. - Everyone gets a
priceframe and a redactedaudit_eventunder the same sequence number. The end-of-lot timer is re-armed, because a bid in the soft-close window can move the end. See Auction states and soft close.
- packages/server/src/room/room.tssubmitBid, handleBid, holdForConfirm, respondConfirm
- packages/server/src/room/confirm.tsneedsConfirm, CONFIRM_TTL_MS, the hold book
- packages/server/src/voice/bridge.tsvoice decision to bid frame, drop records
- packages/voice/src/decide/policy.tsaccept, confirm or drop
- packages/engine/src/rules/validate-bid.tsthe check order
- packages/engine/src/decide/place-bid.tsBID_RECORDED, BID_REJECTED, END_EXTENDED
- packages/shared/src/wire/client.tsthe bid frame
Last updated on
System architecture, end to end verified
Every part of GAVEL from the browser to PayPal and the disk, and how a bid, a hold, a voice bid and a webhook travel through them.
Auction states and soft close verified
The states a lot moves through, who can hammer it, and how the soft-close clock extends the end.