Skip to content

PayPal sandbox only. No real money moves.

ReferenceReference

WebSocket protocol

Every frame a client may send, every frame the room sends, the sequence numbers and the close codes.

Verifiedbacked by a real run or committed evidence

Page last changed 7 Nov 2026 (from the git history)

The live auction runs over one WebSocket per client at /ws. Frames are JSON text, one object per frame. This page is the contract: the shapes come from packages/shared/src/wire/, and the behaviour from the room host in packages/server.

Verified against the code, not measured

Shapes and limits are read from the shared schemas and the server host. This is a description of the protocol, not a claim about load or latency.

WebSocket protocol: join, resume, live frames, bids and reconnect

Read from code
WebSocket protocol: join, resume, live frames, bids and reconnectSequence diagram with three lanes: client, gateway and room. The client upgrades at /ws and must send a join within 5 seconds. The gateway verifies the token for the auction and attaches the connection to the room. The room replays missed frames if the gap is 200 events or fewer, then sends a state snapshot. Live frames carry a sequence number. The client may send only bid and confirm_response. Pings run every 20 seconds, a slow reader is closed with 4008, and the client reconnects with back-off and its last sequence number.Clientbidder, screen, consoleGatewayhost/gateway.tsRoomroom/room.tsno join within 5 s: close 40011token verified for this auction, auction must existping every 20 s, two missed pongs: close 1001256 queued frames or 5 s undrained: close 4008reconnect after 0.5, 1, 2, 4, 8 s (+/- 20%), join again with lastSeqGET /ws (upgrade, Origin checked)1join { token, auctionId, lastSeq? }attach(connection, lastSeq)replay, if 200 events or fewer were missedstate { snapshot }price, audit_event, hammer, settlement_updateconfirm_request (that bidder only)bid { mid, amount, source }confirm_response { confirmId, accept }bid_accepted | bid_rejected

Scroll the figure sideways, or open it full size.

Step 1 of 7. Arrow keys work too.

1. Upgrade. The socket lives at /ws. A request with no Origin, or one that is not on the allowed list, is refused before any frame. After the upgrade the client has 5 seconds to send a join, or the host closes with 4001. A frame sent before the join gets not_joined.

Source: packages/shared/src/wire/ (frames and size caps), packages/server/src/host/gateway.ts, transport/handshake.ts, transport/connections.ts, room/resync.ts, room/room.ts and the client side in packages/web/src/net/connection.ts. Auctioneer controls (hammer, pause, extend) are not WebSocket frames; they use the HTTP control route. This is a map of code paths, not a measurement.
Text version of this diagram

Lanes: client, gateway, room.

  1. The client upgrades at /ws. No join within 5 seconds closes with 4001.
  2. The client sends join with a token, the auction id and optionally lastSeq. The gateway verifies the token for that auction and attaches the connection to the room.
  3. The room replays missed audit frames if 200 events or fewer were missed, then sends a state snapshot.
  4. Live frames carry seq and ts: price, audit_event, hammer, settlement_update. confirm_request goes to the held bid's owner only.
  5. The client may send bid and confirm_response. The room answers with bid_accepted or bid_rejected carrying the same mid.
  6. The host pings every 20 seconds and closes after two missed pongs. A connection 256 frames behind, or undrained for 5 seconds, closes with 4008.
  7. The client reconnects with a back-off of 0.5, 1, 2, 4, then 8 seconds, plus or minus 20 percent, and joins with its last seq.

The envelope

Every frame is a strict object: { "v": 1, "t": "<type>", ... }. A frame with an unknown t, an extra field, bad JSON, a non-object, or a version other than 1 is rejected.

Prop

Type

Frames a client may send

After the join a client may send only bid and confirm_response. Auctioneer controls such as hammer, pause and extend are HTTP requests, not frames.

Must be the first frame. A frame before it gets an error with not_joined.

{ "v": 1, "t": "join", "token": "<session token>", "auctionId": "<ULID>", "lastSeq": 41 }

Prop

Type

Frames the room sends

tSent toCarries
bid_acceptedthe sendermid, bidId, amount
bid_rejectedthe sendermid, reason (an error code), retryable, optional retryAfterMs
priceeveryoneprice, leaderId, leaderPaddle, endTs, minNext, extensions, optional paused and remainingMs
confirm_requestthat bidder onlyconfirmId, amount, optional transcript, expiresAt
audit_eventeveryone, redactedone audit row; the auctioneer sees full rows, others public rows
hammereveryonewinnerId (or null), price, ranking, optional stopped, registrationClosed

Sequence numbers

The room numbers every event it logs. A client that keeps the highest seq it has applied can always resume.

  1. One bid, one seq. bid_accepted, price and audit_event for the same bid share a seq. A client must not count them as three events.
  2. No gaps. An event a viewer may not see still consumes a seq, so the room sends that viewer a filler (a repeat of the current price, or a snapshot when there is no price yet). A hidden confirm hold, a voice drop and a control record all work this way.
  3. Resume. A join with lastSeq gets the missed audit frames when the gap is 200 events or fewer, then a state snapshot. A client that is current, ahead, or more than 200 behind gets the snapshot only.
  4. The web client ignores a seq it has already applied, and a seq more than one ahead is a gap: it keeps what arrives, asks for a fresh snapshot by joining again with its lastSeq, and accepts that snapshot at any seq.

Who may do what

Operationauctioneerbidderscreen
Join the socketyesyesyes
Bid as themselvesnoyesno
Bid for a bidder (manual)yesnono
Answer a confirmnoyesno
Control the clock, hammer, rotate invitesyes (HTTP)nono

Three limits keep one client from hurting the room: connections per token (3, the oldest is closed with 4011), connections per room (500) and a bid rate limit per bidder. Every frame, joined or not, also takes a token from the connection's frame bucket.

Errors and close codes

An error frame carries one of the shared codes. Some also close the socket.

Close codeReasonWhen
4001join_timeoutno join within 5 seconds
1008bad_messagea malformed join, or a bad frame after the join
4401unauthorizedthe token fails for this auction, expired or was revoked while connected
4009room_fullthe room is at its connection cap
4010host_fencedanother host owns the room now
4011supersededa newer connection on the same token pushed this one out
4008slow_consumer256 frames queued, or a write undrained for 5 seconds
1001heartbeat_losttwo missed pongs (the host pings every 20 seconds)
1011internalan unexpected failure

Edit on GitHub

Last updated on