> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0015. Combo Accept: the server signs the stored quote and sends it once

# 0015. Combo Accept: the server signs the stored quote and sends it once

* **Status:** Proposed
* **Date:** 2026-10-01

## Context

Accept is the one Combos step that trades. The venue's quote carries the amounts to sign
(`maker_amount_e6`, `taker_amount_e6`) and the total the trade costs (`total_required_e6`,
fee included); the fee sits outside the signed order. A quote lives 5 to 10 s.

Polymarket's docs and the SDK say a repeated Accept "can be safely retried", and the SDK
retries once after a transport error. `CLAUDE.md` forbids an automatic retry of a
non-idempotent venue request: if the first request reached the venue, a second one relies
on the venue's deduplication, which the app cannot check. The venue has a status read,
`GET /requests/{rfq_id}`, that answers 409 when no Accept was recorded.
[ADR 0011](0011-audit-first-order-path.md) set the same rules for placements.

## Decision

1. **The browser sends ids only:** `client_order_id` (a new UUIDv7), `rfq_id` and
   `quote_id`. It sends no amount, price, token or leg.
2. `engine/orders` holds this app's quote requests of the last 10 minutes in memory. It
   looks up the RFQ and checks that `quote_id` is its quote, locks the quote to this Accept,
   and reserves its amount against `max_combo_exposure` (or its shares against the held
   position for a sell). A second Accept of the same quote is refused
   `combo_already_accepted`; a quote at or past its expiry is refused `combo_quote_expired`.
3. `engine/orders` writes the **intent** row, `engine/risk` `CheckCombo` decides (Armed,
   every trading gate, the combos scope, `max_orders_per_minute`, the legs, and for a buy
   `max_combo_notional`, `max_combo_exposure` and `max_daily_notional` against
   `total_required_e6`; for a sell the held shares), and the **decision** row is written.
4. The adapter checks the stored quote's amounts against the request
   (`signing.CheckQuoteAmounts`) and signs an Exchange V3 order with those amounts copied
   unchanged. The **signed** row records the order hash and the SHA-256 of the exact body.
   Nothing is sent if that row cannot be written.
5. The Accept is **sent once**. A refusal the venue gives before taking it (any 4xx other
   than 408 and 409 `INVALID_RFQ_STATE`) is final. A timeout, a lost or unreadable answer, a 408, any
   5xx (503 included) and 409 `INVALID_RFQ_STATE` are **unknown**.
6. An executing or unknown Accept is settled **only by status reads of its `rfq_id`**:
   every 500 ms, then every 5 s once it has run for a minute, with backoff after a failed
   read. `CONFIRMED` or `FILLED` with a `tx_hash` is filled; `FAILED`, `CANCELED`,
   `EXPIRED`, `REJECTED` or an unknown status is not filled; a 409 means the Accept was
   not recorded and settles it as rejected with `venue_not_found`. Each result is a
   **reconcile** row. **The Accept is never re-sent.** This overrides the venue docs'
   "safely retry".
7. **Resumed at restart.** At start, every Accept with a signed row and no final
   response or reconcile row is queued for status reads with its reservation held.

A quote request is never repeated either: a lost answer is `no_quote` with
`combo_no_response`, and nothing can be accepted.

## Consequences

* A compromised or buggy browser cannot change what is signed: the amounts come from the
  venue's quote as the server stored it, checked against the request.
* A double click or a replayed request sends at most one Accept.
* A crash after the signed row is settled at the next start by the RFQ's status.
* An unknown Accept can take seconds or minutes to settle; the slip shows `Checking…` and
  the quote cannot be accepted again meanwhile.
* If the venue's deduplication would have made a retry safe, the app gives up that
  retry: an Accept lost in transit ends as `venue_not_found`, and the user asks for a new
  quote.
* Open: security finding P8-01. As built, the first 409 settles the Accept, though
  Polymarket may record it a moment later. The proposed fix settles "not recorded" only
  when 409 answers continue past the quote's `expires_at` plus a margin. It changes step 6,
  not the rule that the Accept is never re-sent.

## Alternatives considered

* **Retry once, as the SDK does.** Rejected: it trusts a deduplication the app cannot
  check, and `CLAUDE.md` forbids it.
* **Let the browser send the signed amounts or the quote.** Rejected: the server would sign
  what the browser says, and risk would check numbers the venue never quoted.
* **Settle from the CLOB user feed or the Data API.** Rejected: neither is documented for
  requester trades (venue notes §16, A15), and the RFQ status is the single source of
  quote request state (PLAN §3.7).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.