TBBN.Merchant Platform docs
Architecturedocs/architecture/trade-session-state-machine.md

Trade Session State Machine (Canonical)

This merges two slightly different state-machine drafts found in source material into one canonical version. See docs/business/conflict-resolution-log.md item 2.

⚠️ The most commonly misread part of this flow

Seller B does not wait for Seller A to pay, and vice versa. As soon as a trade session reaches RESERVED, both sellers can complete their own checkout immediately, in either order, independently. Fulfillment only begins once both sides have completed checkout. Do not build any code path that gates one side's checkout availability on the other side's payment status — that is exactly the miscommunication both source documents independently called out and corrected.

Diagram

DRAFT
  |
  v
PENDING
  |
  v
OFFERED
  |
  v
ACCEPTED
  |
  v
RESERVING
  |
  v
RESERVED
  |
  +-----------------------+
  v                       v
WAITING_CHECKOUT_A    WAITING_CHECKOUT_B      <- parallel, independent
  |                       |
  v                       v
CHECKOUT_A_COMPLETE   CHECKOUT_B_COMPLETE
  |                       |
  +-----------+-----------+
              v
            LOCKED            <- both sides complete (join/barrier)
              |
              v
         FULFILLMENT
              |
              v
          FULFILLED
              |
              v
         COMPLETED

Failure / side states, reachable from most steps above: CANCELLED, EXPIRED, PAYMENT_FAILED, RESERVATION_FAILED, FULFILLMENT_FAILED, RETURNED, REFUNDED.

Transition table

FromToTriggerSide effect
DRAFTSeller starts composing an offer (not yet submitted)none external
DRAFTPENDINGOffer submittedoffer.created event
PENDINGOFFEREDOffer delivered/visible to the target selleroffer.created webhook to target's merchant
OFFEREDACCEPTEDTarget seller acceptsoffer.accepted event + webhook; trade session row created; listing set frozen into trade_session_listings_a/b
OFFEREDEXPIRED24h default timeout with no response (docs/business/conflict-resolution-log.md item 12)offer.rejected-equivalent expiry event
ACCEPTEDRESERVINGTrade-session engine begins locking both sides' itemsreservation.created events (one per listing)
RESERVINGRESERVEDAll listings on both sides successfully lockedreservation.created webhook per side; both WAITING_CHECKOUT_* become reachable
RESERVINGRESERVATION_FAILEDAny listing became unavailable mid-lock (race with another trade)reservation.expired-equivalent event; session cancelled, no charges attempted
RESERVEDWAITING_CHECKOUT_A / WAITING_CHECKOUT_BImmediately, both in parallelcheckout.started webhook to each side's counterpart merchant, with {tradeSessionId, originalPrice, tradeAmount, currency}
WAITING_CHECKOUT_ACHECKOUT_A_COMPLETEMerchant A-side webhook payment.completed receivedcheckout.completed event (side A)
WAITING_CHECKOUT_BCHECKOUT_B_COMPLETEMerchant B-side webhook payment.completed receivedcheckout.completed event (side B)
WAITING_CHECKOUT_*PAYMENT_FAILEDMerchant webhook payment.failed, or 72h default timeout (item 12)checkout.completed-equivalent failure event; session enters recoverable failure state, not silently retried
both *_COMPLETELOCKEDBarrier: both sides report completetrade.completed event fires here — this is the signal merchants listen for to know it's safe to begin fulfillment
LOCKEDFULFILLMENTMerchant(s) begin shippingfulfillment.started webhook (merchant → TBBN)
FULFILLMENTFULFILLEDMerchant(s) report fulfillment donefulfillment.completed webhook (merchant → TBBN)
FULFILLEDCOMPLETEDBoth merchants confirm no open disputes/returns windowterminal state
any pre-LOCKED stateCANCELLEDEither seller cancels, or ops intervenestrade.created-equivalent cancellation event; any completed reservations released
post-FULFILLEDRETURNED / REFUNDEDMerchant-driven, reported via webhookinformational only — TBBN does not currently process the actual return/refund, only records the state for analytics/reputation

Multi-item trades

A session's A side and B side are each an array of listings (trade_session_listings_a, trade_session_listings_b), not a single listing — N↔M is supported from ACCEPTED onward. The RESERVING/RESERVED states apply to the whole set on a side: a side is not RESERVED until every listing in its set is locked, and a single unavailable listing in the set fails the whole side's reservation (RESERVATION_FAILED), not a partial trade.

A/B assignment convention

Purely a storage convention (docs/business/conflict-resolution-log.md item 3): on offer→session conversion, the existing listing's owner (the one who was offered to) becomes sellerA; the offer's proposer becomes sellerB. No business rule may depend on which slot a seller occupies — the parallel-checkout behavior above applies identically to both.