Parlays Atlas

Parlays Atlas

One place for everything about Outcome's parlays RFQ product: the mind map of Francisco's seven design documents and the two outcome-clearing code branches, plus the customer journey, the storage map, the branch comparison and the FAQ — as tabs above. Click a map node to read it, open its source, or jump to the page that covers it.

7 docs + 2 code branches · captured 2026-09-05 · wire 0.1 · doc statuses: READY / UNDER REVIEW / WIP / design
click select · click again fold/unfold · drag pan · wheel zoom
Customer perspective · every step, every exit

Parlay Journey

Every step a customer goes through, with every exit the documents define. Sources: Litepaper (01), API spec (03), Data (04), custody docs (05–07) and the two outcome-clearing branches. Where the two custody branches change what the customer experiences, both are shown. Nothing here is a fee: the customer pays the maker's price P and nothing else at launch.

you actsystem stepgood outcomeexit or bad outcome◇ decisiondotted = edge case

Overview

flowchart TD
  P0["0 · Fund your account"] --> P1["1 · Browse the catalogue"]
  P1 --> P2["2 · Build a parlay and request a price"]
  P2 -->|no price, or quote expired| P1
  P2 --> P3["3 · Commit to the quote"]
  P3 --> P4{"4 · Maker last look"}
  P4 -->|confirm| P5["5 · Execution: stake and liability locked"]
  P4 -->|reject, timeout, market invalidated| R1(["No trade · stake released"])
  P4 -->|replacement quote| P3
  P5 -->|contract or funding failure| R1
  R1 --> P1
  P5 --> P6["6 · Legs resolve"]
  P6 --> P7{"7 · Settlement"}
  P7 -->|every leg wins| W(["WIN · payout P to Available"])
  P7 -->|a leg loses| L(["LOSS · stake S to the maker"])
  P7 -->|any leg void or unresolvable| V(["REFUND · exact S back"])
  W --> P8["8 · Withdraw"]
  V --> P8
  L --> P1
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  classDef sys fill:#EEF2ED,stroke:#7D877F,color:#1C221E
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  class P0,P1,P2,P3,P8 user
  class P5,P6 sys
  class W,V good
  class L,R1 bad
0

Fund your account

flowchart TD
  A["Connect wallet"] --> B{"Do you have a vault yet?"}
  B -->|no| C["Outcome creates your UserVault (Admin pays gas)"]
  B -->|yes| D
  C --> D{"Custody branch"}
  D -->|A · HyperCore| E["Send spot USDC on HyperCore to your vault address"]
  E --> F["Operator sees the transfer and credits it"]
  F --> G(["Available = deposit"])
  D -->|B · HyperEVM| H["Move USDC HyperCore to HyperEVM (one-time)"]
  H --> I["Approve, then deposit into your vault (you pay gas)"]
  I --> G
  E -.->|sent from a different wallet| X1(["Left unclaimed - not credited"])
  I -.->|raw transfer instead of deposit| X2(["Unaccounted - stranded"])
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  class A,E,H,I user
  class G good
  class X1,X2 bad
You seeWhat happens underneathExits
A balance with Available / Locked (branch A also shows Withdrawing)One immutable UserVault per person, created lazily. A: the vault address is your HyperCore account; the operator watches the ledger and calls creditDeposit only for owner → own-vault transfers. B: the vault holds ERC-20 USDC; deposit() credits the observed balance delta.Sending from an unrelated wallet (A) or a raw token transfer (B) is not credited. Designed but not built: authorizing a bounded trading key once so you never sign per trade (doc 07).
1

Browse the catalogue

flowchart LR
  A["Open the catalogue"] --> B["Eligible legs only: pre-event moneylines"]
  B --> C["Reference prices shown (informational)"]
  C --> D["Pick a leg"]
  D -.->|market suspended or closed on Outcome Market| X(["Leg disappears or is marked ineligible"])
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  class A,D user
  class X bad
You seeUnderneathExits
Sports, competitions, moneyline legs; a reference probability per legoutcome-market polls Outcome Market, normalizes to Market / Leg / PriceObservation, applies the content-hashed eligibility policy, serves snapshots over REST and changes over WebSocket. Reference prices are never executable.Lifecycle changes only on explicit Outcome Market evidence; an ineligible leg cannot be added to a parlay.
2

Build a parlay and request a price

flowchart TD
  A["Select 2 to 4 legs"] --> B["Enter stake S"]
  B --> C{"Outcome validates"}
  C -->|ineligible combination, over limits, insufficient Available| X1(["Request refused with a reason"])
  C -->|ok| D["RFQ sent to the maker"]
  D --> E{"Maker quotes before the deadline?"}
  E -->|no| X2(["No price available right now"])
  E -->|yes| F["Quote shown: payout P, expires at"]
  F --> G{"You act before expiry?"}
  G -->|maker re-prices first| F
  G -->|expired| X3(["Quote gone - request again"])
  G -->|accept| H(["Go to Commit"])
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  class A,B user
  class X1,X2,X3 bad
  class H good
You seeUnderneathExits
Your legs, your stake S, then one price: total payout P (stake included) with a countdownOutcome checks eligibility, permitted combinations, limits and funding readiness, then delivers a maker-scoped RFQ (quote_response_deadline, rfq_expires_at). The maker prices the whole ticket and submits P; Outcome derives the maker's liability L = P − S, validates P > S, and reserves maker capacity atomically so the price cannot later fail for "no capacity". Before you act the maker may quote.replace (you see the new price) or quote.cancel.Validation failure → refused with reason. Maker silent → closed_no_quote. Quote expiry → request again. RFQs are ephemeral — there is no order book to rest on.
3

Commit to the quote

flowchart TD
  A["Tap Confirm on the quote"] --> B["Wallet authorization"]
  B --> C{"Outcome verifies authority and funding"}
  C -->|fails| X(["Not committed - nothing reserved"])
  C -->|ok| D["Commitment recorded, confirm_by clock starts"]
  D --> E(["Stake S reserved - cannot withdraw it now"])
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  class A,B user
  class X bad
  class E good
You seeUnderneathExits
A short "waiting for the maker" state; your Available drops by S (reserved)customer_action.recorded with effective_for_execution = true carries S, P, L, an attestation that Outcome verified your wallet authority and funding, and the Outcome-set confirm_by deadline (target 500 ms). This is a binding instruction to proceed at that price — not yet a trade.Authorization or funding failure → the action is not effective and nothing is reserved.
4

Maker last look

flowchart TD
  A["Maker has until confirm_by"] --> B{"Decision"}
  B -->|confirm| C(["Proceed to execution"])
  B -->|reject with an approved reason| D(["No trade - stake released"])
  B -->|timeout| D
  B -->|market objectively invalidated| D
  B -->|replacement quote, if enabled| E["New price P' shown - fresh approval needed"]
  E -->|you approve| F(["Back to Commit with P'"])
  E -->|you decline or it expires| D
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  class C,F good
  class D bad
  class E user
You seeUnderneathExits
Either "confirmed", "declined by the maker", or a new price to approveEvery commitment gets a maker confirm or reject; reject needs an approved reason code — "not enough capacity" is never allowed because capacity was reserved when the quote went live. maker_decision.replace (only if the environment advertises it) creates a new immutable quote, exhausts your commitment, and is bounded by replacement depth and RFQ lifetime. Outcome's recorded order decides every race.Reject / timeout / invalidation → no trade, reservation released, you can request again. Replacement → your choice. SLA targets: ≥ 99 % decided in time, ≤ 1 % timeouts.
5

Execution

flowchart TD
  A["Outcome authorizes one contract operation"] --> B["TradeManager.openTrade locks S in your vault and L in the maker's vault - one transaction"]
  B --> C{"Finality predicate passes?"}
  C -->|yes| D(["Binding trade · Available -S, Locked +S"])
  C -->|revert or definitive failure| E(["No trade - stake released, no one-sided lock possible"])
  C -->|outcome unknown| F["Operator recovers from receipt and chain state - never resubmits"]
  F --> C
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef sys fill:#EEF2ED,stroke:#7D877F,color:#1C221E
  class D good
  class E bad
  class A,B,F sys
You seeUnderneathExits
The trade appears as Open: stake S moved from Available to Locked; payout P shownexecution.submission_authorized → pending → finalized → trade.created. On both branches openTrade locks the customer stake and the maker liability atomically — if either lock fails the whole transaction reverts. Submission or observation is not a trade; only the configured finality predicate is. The trade's S, P, L, rulebook and mapping versions are now immutable.Objective invalidation before submission, loss of funding authority, security revocation or a contract failure → explicit no-trade with your stake released. An unknown outcome blocks capacity and is resolved from authoritative status, never by blind retry.
6

Legs resolve

flowchart TD
  A["Events happen"] --> B["Outcome Market publishes outcomes"]
  B --> C["Outcome applies the recorded rulebook and mapping per leg"]
  C --> D{"Evidence complete and final?"}
  D -->|yes| E(["Leg result recorded: won, lost or void"])
  D -->|conflicting or missing| F["Resolution suspended - you see Pending"]
  F --> G{"Long-stop outer deadline reached?"}
  G -->|no| D
  G -->|yes| H(["Leg classified unresolvable"])
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef sys fill:#EEF2ED,stroke:#7D877F,color:#1C221E
  class E good
  class H bad
  class A,B,C,F sys
You seeUnderneathExits
Each leg: Pending → Won / Lost / Void. The parlay stays Open until every leg is final. No cash-out at launch.Outcome Market evidence (determination, dispute or correction state, finality, settlement value, cancellation, payout availability) → normalized source state → canonical result under the rulebook version fixed at execution. Conflicting or unavailable evidence → resolution.suspended, never silently collapsed. A later Outcome Market corrections opens a new case; it never rewrites your trade.Long-stop (parameters still TBD) caps how long collateral can stay locked; at the absolute outer deadline any unresolved leg becomes unresolvable.
7

Settlement

flowchart TD
  A{"All legs final - combined result?"} -->|every leg won| W["WIN"]
  A -->|any leg lost, none void| L["LOSS"]
  A -->|any leg void, cancelled, invalid or unresolvable| V["REFUND"]
  W --> W1{"Custody branch"}
  W1 -->|A| W2["Maker vault requests HyperCore transfer of L to your vault - shows Settling"]
  W2 --> W3["Operator confirms the transfer arrived"]
  W3 --> W4(["Available +P (your S released plus L received)"])
  W1 -->|B| W4
  L --> L1(["Locked -S, your stake now belongs to the maker"])
  V --> V1(["Locked -S, Available +S · exact stake back"])
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef sys fill:#EEF2ED,stroke:#7D877F,color:#1C221E
  class W4,V1 good
  class L1 bad
  class W2,W3 sys
You seeUnderneathExits
Win: Available rises by P. Loss: Locked drops by S, nothing comes back. Refund: exact S returns to Available. Exactly one of the three, ever.Settlement amounts and recipients are derived from the immutable trade; the maker approves nothing and supplies nothing. A: beginSettlement → CoreWriter spotSend → operator matches the exact HyperCore ledger event → finalizeSettlement; your vault refuses the credit until the USDC has physically arrived. B: settle releases, transfers and credits in one atomic transaction. Void needs no transfer on either branch.Win/loss and refund are mutually exclusive. Void-adjusted settlement (payout on surviving legs) is a future option, not launch behavior.
8

Withdraw

flowchart TD
  A["Choose an amount up to Available"] --> B["Sign once (EIP-712) - Outcome relays and pays gas"]
  B --> C{"Branch"}
  C -->|A · HyperCore| D["Available to Withdrawing, CoreWriter sends"]
  D --> E["Operator confirms the HyperCore payment"]
  E --> F(["USDC in your HyperCore wallet"])
  C -->|B · HyperEVM| G["Vault transfers ERC-20 USDC immediately"]
  G --> H(["USDC in your HyperEVM wallet"])
  H -.->|optional| I["Move back to HyperCore"]
  A -.->|amount above Available, or locked in an open trade| X(["Rejected"])
  classDef user fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E
  classDef good fill:#E6F1EE,stroke:#2F6F5E,color:#1C221E,stroke-width:2px
  classDef bad fill:#F6E9E9,stroke:#9A3B3B,color:#1C221E
  classDef sys fill:#EEF2ED,stroke:#7D877F,color:#1C221E
  class A,B,I user
  class F,H good
  class X bad
  class D,E,G sys
You seeUnderneathExits
Enter amount → one wallet signature → funds arrive. You never need gas.The signature binds owner, destination, amount, nonce and expiry (branch B also the vault) under the Outcome UserVault EIP-712 domain, so the relayer cannot change anything. A: available → withdrawing → CoreWriter → operator observes the payment → completeWithdrawal. B: available -= amount and safeTransfer in the same call. You may also call withdraw yourself and pay your own gas.Anything above Available — including stake locked in open trades — is rejected. Nobody but you can withdraw from your vault (Admin is a relayer, not an owner).

Invariants the customer can rely on

  • You pay the maker's price P — Outcome adds no fee, markup or rebate at launch.
  • Once you commit, the price cannot be edited; a different price is a new quote you must approve.
  • You cannot be told "no capacity" after committing.
  • Nothing is a trade until your S and the maker's L are locked together and final.
  • Your trade's rules are frozen at execution; later Outcome Market corrections cannot change it.
  • Exactly one ending: win (+P), loss (−S), or refund (S back). Any voided leg refunds the whole ticket.
  • Your locked collateral cannot stay locked past the long-stop outer deadline.
  • The maker who priced your trade stays on the hook for it even if Outcome stops using that maker.
  • Only you can withdraw from your vault; Outcome relays and pays gas but cannot redirect or resize a withdrawal.

Designed but not yet built (so not in the flow above)

Bounded trading/session keys (sign once for a session instead of per commit) · void-adjusted settlement on surviving legs · cash-out and secondary liquidity · same-event and in-play parlays · multiple makers competing on your RFQ · gasless deposits on branch B (permit / EIP-3009).

Storage and recording, mapped to the customer flow

Ledger Behind the Bet

Technical companion to user-flow.md. For every customer step: which records are written, in which store, by which system — and how a move in Outcome Market odds affects the price the customer is holding at that moment. Sources: docs 01, 03, 04, 05–07 and both outcome-clearing branches. Layers marked spec are defined in the API document but not present in the repositories we have seen; layers marked built exist in code.

1

The systems and their stores

flowchart LR
  subgraph V["Outcome Market"]
    OM["Outcome Market · REST discovery and lifecycle · WebSocket prices"]
  end
  subgraph D["Data layer · built · outcome-market / outcome-catalogue"]
    SE[("Postgres · source_events · raw payloads")]
    CAT[("Postgres · catalogue_markets / legs · eligibility · transitions · equivalence")]
    RD[("Redis · latest reference price per leg · no history")]
  end
  subgraph P["Protocol layer · spec (doc 03) · not in repos yet"]
    RFQ[("RFQ · quote · customer_action · maker_decision · execution records")]
    EV[("Event log with sequence_transitions · replay cursors · checkpoints")]
    RES[("Resolution cases · settlement lineages · reconciliation breaks")]
  end
  subgraph M["Market maker"]
    MK["Pricing engine · whole-ticket P"]
  end
  subgraph C["Clearing · built · outcome-clearing"]
    OP["Rust operator (Admin key)"]
    SQ[("SQLite · trades · submitted_actions · signed intents · evm_logs · (A) settlements, deposits, solvency")]
    TM["HyperEVM · TradeManager + UserVaults · locks, S and L"]
    HC["HyperCore · physical USDC (branch A) · CoreWriter"]
  end
  OM --> SE --> CAT --> RD
  CAT -->|catalogue snapshot + WS changes| P
  RD -->|reference_observations at RFQ time| RFQ
  RFQ <-->|rfq.delivered / quote.submit / decisions| MK
  RFQ --> EV
  RFQ -->|authorized execution| OP
  OP --> SQ
  OP --> TM
  TM <-->|A only: spotSend / balance reads| HC
  TM -->|events| OP
  V -->|resolution evidence| RES
  RES -->|settle trade as WIN / LOSS / VOID| OP

Reading rule for the whole system: raw before normalized (data layer), Outcome's recorded order decides races (protocol), the chain decides whether money moved (clearing). Five things are recorded separately and never collapsed: Outcome Market evidence → normalized source state → Outcome's canonical result → settlement authorization → contract execution.

2

The price the customer holds — a state machine

stateDiagram-v2
  [*] --> Browsing
  Browsing: Browsing — no price held
  Browsing --> Quoted: request price → maker submits P
  Quoted: Quoted — P is the maker's, not yet yours
  Quoted --> Quoted: maker quote.replace → new quote, new P
  Quoted --> Browsing: quote.cancel / expires_at / RFQ lifetime
  Quoted --> Committed: you accept → customer_action.recorded (effective)
  Committed: Committed — P locked for you, S reserved
  Committed --> Executed: maker confirm before confirm_by
  Committed --> Browsing: reject · timeout · objective invalidation → S released
  Committed --> Quoted: maker_decision.replace (if enabled) → new P needs your approval
  Executed: Executed — S, P, L immutable forever
  Executed --> Resolved: every leg final
  Resolved --> [*]: WIN (+P) · LOSS (−S) · REFUND (S back)
StateWho can change the number you seeWhat a move in Outcome Market odds doesRecord that proves it
BrowsingOutcome Market, continuouslyRedis latest price updates in real time (Outcome Market WebSocket). Nothing is held; the displayed probability is informational only.source_events (raw), Redis (latest) — history is not kept
QuotedThe maker, until you commitThe maker may re-price: quote.replace creates a new quote id with a new P; the old one is superseded. Or quote.cancel. Or the quote simply reaches expires_at. You are never repriced silently — you see a new number and must act on it. If you commit first and Outcome records the commitment before the replace/cancel, your commitment wins at the old P ("cancel or pre-selection replacement wins only if Outcome records it before customer selection").quote.activated / quote.superseded / quote.cancelled / quote.expired events, each with sequence_transitions; capacity reservation exchanged atomically
CommittedThe maker, once, via last look — and only within confirm_byThis is exactly what last look exists for: "to protect against material changes in market or risk conditions between quote creation and execution". Maker options: confirm at your P; reject with an approved reason (never "no capacity"); replace (only if the environment enables it) → new quote, fresh approval, bounded by replacement depth; or time out. Rejection/replacement frequency and price worsening are measured against the maker (SLA: ≥ 99 % decided, ≤ 1 % timeout). Outcome cannot change P at all.customer_action.recorded (S, P, L, confirm_by, attestation) and maker_decision.recorded (winning decision, reason code)
ExecutedNobodyOdds are irrelevant from here. S, P, L are "exact immutable integers after acceptance"; the rulebook and mapping versions are frozen. No cash-out at launch, so there is no mark-to-market either.execution.finalized + trade.created (off-chain immutable trade record); on-chain TradeManager.trades[id]{stake, liability}
Resolved / SettledNobodyOutcome Market prices never matter — only each leg's final result. A later Outcome Market corrections opens a new case and does not rewrite your trade.resolution.finalized (per-leg results, evidence refs, predicate version) → settlement.finalized / .refunded; on-chain TradeSettled / TradeVoided
3

Phase by phase — what is written, where

Phase (user action)Records writtenStore · ownerOdds / price effect
0 Fund — deposit(A) HyperCore ledger spotTransfer; operator core_observations, isolated_deposits, deposits; on-chain DepositCredited, UserVault.available += amt. (B) on-chain Deposited, UserVault.available += received.HyperCore ledger · SQLite · HyperEVM vault storage · owner: operator (A) / participant (B)none
1 Browse — open catalogueDiscovery sweeps: source_events (raw), catalogue_markets, catalogue_legs, catalogue_eligibility_decisions/current (policy version + content hash), catalogue_market_transitions (append-only), canonical_offerings; Redis latest price + Pub/Sub.Postgres + Redis · outcome-marketReference prices move continuously; you hold nothing
2 Price — pick 2–4 legs, stake S, requestRFQ record: maker-scoped rfq_id, stake, legs[] (leg id, venue, market/outcome ids, payoff meaning, raw + normalized state, observation time, provenance, mapping/rulebook refs), pricing_snapshot_id, optional reference_observations, policy/tier/rulebook/limit versions, quote_response_deadline, rfq_expires_at, customer_ref (no raw identity). Then quote.activated {quote_id, P, expires_at, derived L, capacity_reservation_id}.Protocol store (spec) · OutcomeMaker prices the whole ticket from the snapshot + own models; per-leg odds are inputs, never stored as part of the quote. Replace/cancel/expire produce new events, never edits
3 Commit — accept quote, signcustomer_action.recorded {S, P, L, asset, recorded_at, confirm_by, effective_for_execution=true, authorization_attestation}; capacity reservation → conversion_pending; your stake reserved (reservation, not yet on-chain).Protocol store (spec) · OutcomeP is now locked from your side. Maker still has last look
4 Last look — waitmaker_decision.recorded {confirm \reject + reason_code + evidence_ref \replace → successor quote \timed_out \closed_by_invalidation}.Protocol store (spec) · Outcome (maker decides, Outcome records)Only exit for a price change: replacement, which you must re-approve
5 Execution — nothing to doexecution.submission_authorizedexecution.pendingexecution.finalizedtrade.created (immutable: legs, S, P, L, snapshot, versions). On-chain: TradeManager.openTradeTradeOpened, UserVault available→locked for S (you) and L (maker). Operator: SQLite trades (open_pending → lock_submitted → locked), submitted_actions reservation, evm_logs.Protocol store (spec) + HyperEVM (built) + SQLite (built)Frozen. Only S and L exist on-chain; P = S + L
6 Resolve — waitOutcome Market evidence (raw) → normalized source state → canonical per-leg result (resolution.finalized with evidence refs, adapter/mapping/rulebook/predicate versions) or resolution.suspended.Protocol store (spec) · Outcome; evidence from Outcome Market via the same adapters as the data layerPrices irrelevant; only outcomes. Long-stop caps lock time
7 Settle — nothing to do`settlement.authorized → pending → finalized \refunded (one terminal lineage). On-chain: (A) TradeSettlementRequested → CoreWriter → HyperCore ledger event → operator settlements observedfinalizeSettlementTradeSettled; (B) settleTradeSettled in one tx; void → TradeVoided`. Vault buckets updated.Protocol store (spec) + HyperEVM + HyperCore (A) + SQLiteWIN pays exactly the recorded P; LOSS takes exactly S; REFUND returns exactly S
8 Withdraw — sign oncesigned_withdrawal_intents (SQLite), on-chain WithdrawalRequested (A) / Withdrawn (B); (A) HyperCore payment observed → completeWithdrawalWithdrawalReconciled.SQLite + HyperEVM + HyperCore (A)none
AlwaysDaily + event-driven reconciliation: item-level compare of RFQ/quote/action/decision/execution/trade/resolution/settlement ids and exact collateral units; reconciliation.break.*. Operator SLA metrics from Outcome-recorded timestamps.Protocol store (spec) · SQLite (built, per branch)Detects, never nets away, any drift between records and chain
4

Races between "odds moved" and "user acted" — who wins

Outcome's recorded order is the only arbiter; maker timestamps are audit only.

SituationResult
Maker sends quote.replace, you tap accept a moment later, Outcome records the replace firstYour action targets a superseded quote → rejected (ANOTHER_QUOTE_SELECTED / superseded); you are shown the new P and can accept it
Same, but Outcome records your commitment firstYou are committed at the old P; the replace loses. Maker's remaining option is last look (confirm / reject / enabled replace)
Quote hits expires_at before your commitment is recordedQUOTE_EXPIRED; request a new price
Market suspended on Outcome Market after you commit but before execution is authorizedObjective invalidation → closed_by_invalidation, no trade, S released
Market suspended after execution is authorizedAudit information only — it "cannot defeat an operation that subsequently reaches the configured finality predicate"; the trade stands and resolves under its rulebook
Maker replaces after your commitment (capability enabled)Your commitment is exhausted; successor quote in replacement_review; nothing executes until you approve P′
Outcome Market corrects an outcome after settlementNew correction case; your settled trade is never rewritten
5

What is not recorded today, and why it matters for "odds at bet time"

  • The data layer keeps no price history (Redis latest only; no tick archive; no raw WS frames) — doc 04, Q35.
  • The protocol layer that would persist pricing_snapshot_id and reference_observations per RFQ is specified, not built in the repositories we have.
  • The clearing PoC stores only {id, customer, market_maker, stake, liability, outcome} per trade — no legs.

So the only place the per-leg reference odds at the moment of the bet can survive is the RFQ record. If product, risk or analytics wants "what were the legs priced at when this ticket was quoted" or "how far did the maker's P sit from the reference product", the RFQ store must persist the observations themselves (not just the snapshot pointer), or the data layer must start retaining price history. Neither changes the customer's economics — P is opaque and immutable regardless.

Outcome · outcome-clearing · doc 07 realized twice

Clearing Branches

Francisco built the isolated-vault custody design (doc 06) twice on 2026-09-01, once per doc 07 option: A · feat/collateral-hypercore keeps USDC in a HyperCore account per vault, B · feat/collateral-hyperevm keeps it as ERC-20 inside the vault. Same skeleton, opposite answers to "where is the money" — and that one choice decides everything downstream.

Captured 2026-09-05 · both branches built and tested locally (forge 1.3.6, cargo 1.93) · full write-up in docs/knowledge/code/

Lineage

main e156b49 "Outcome HyperCore Collateral PoC" — doc 05 Phase I: one pooled HyperCore account, CollateralVault.sol, manual Admin deposit attribution ├── feat/collateral-hypercore cf83e2d (+1 commit) doc 06 vaults + doc 07 OPTION A 29 files +2431 / −1163 └── feat/collateral-hyperevm d7713bd (+2 commits) doc 06 vaults + doc 07 OPTION B 33 files +2198 / −1382 hypercore ↔ hyperevm: 35 files differ (+2050 / −2502); siblings, no shared commits after main

Shared by both: Foundry contracts (solc 0.8.26) · Rust crate outcome_hyperliquid_poc (tokio, alloy, rusqlite, axum, ratatui) · just recipes · three encrypted keystores (Admin, MM, Alice) · Admin as deployer, operator, relayer and gas payer · all operator transactions via cast send · SQLite action reservations for exactly-once submission · HyperEVM testnet only (chain 998).

A · forge
13 / 13
12 unit + fuzz · 1 invariant (16,384 calls)
A · cargo
50 / 50
lib 16 · isolated_storage 7 · operator_storage 24 · signing 3
B · forge
16 / 16
unit + fuzz · no invariant suite
B · cargo
42 / 42
lib 15 · erc20_storage 2 · operator_storage 22 · signing 3

Where the money is

A · Collateral stays on HyperCore

contracts/src/UserVault.sol · 242 lines · immutables: owner, tradeManager, operator, l1Read 0x…0801, coreWriter 0x3333…3333, tokenIndex 0
  • The vault's address is its HyperCore account; physical USDC (8-dp atoms) sits there.
  • Four claim buckets: available + locked + withdrawing + settlingOut == totalCollateral ≤ coreBalance(), where coreBalance() staticcalls the spot-balance precompile.
  • Every credit (creditDeposit, creditSettlement) re-reads the physical balance — a winner cannot be credited before the USDC lands (test: ClaimsExceedCore(13,10)).
  • Money leaves only via CoreWriter spotSend (version 1, action 6) — asynchronous; an EVM receipt proves the request, not the payment.
  • Three authorities, no generic transfer: owner (withdraw) · tradeManager (lock/release/beginSettlement/finalize/credit under an exact trade id) · operator (creditDeposit after evidence; complete/fail an already-authorized withdrawal).

B · Collateral inside the HyperEVM vault

contracts/src/UserVault.sol · 192 lines · immutables: owner, tradeManager, usdc (0x2B33…D8Ab, 6 dp) · plus UserVaultFactory, IERC20/SafeERC20
  • The vault holds ERC-20 USDC. deposit(amount) pulls via transferFrom and credits the observed balanceOf delta.
  • Two buckets plus views: accounted = available + locked, actual = balanceOf(this), unaccounted = actual − accounted (never auto-credited), solvent = actual ≥ accounted.
  • receiveSettlement requires balanceOf ≥ available + locked + amount — the tokens must already be present in the same transaction.
  • Money leaves via safeTransfer — synchronous, atomic with the accounting; a failing token call reverts everything (tested).
  • Two authorities only: owner (withdraw) · tradeManager (lock/release/send/receive against an exact counterparty + amount). No operator role on the vault. Reentrancy guard on every token path.

The same trade, settled on each branch

Alice stakes 1, MM's liability is 3, Alice wins. Opening is identical on both branches — one openTrade transaction that locks both vaults atomically and moves no money. Settlement is where they diverge.

A · hypercore — two phases, evidence in between B · hyperevm — one atomic transaction HyperEVM HyperCore Rust operator (Admin key, SQLite) tx 1 · TM.beginSettlement(id, Win) MMVault: locked 3 → settlingOut 3 · state Settling CoreWriter spotSend(AliceVault, 3) executes (async) core_worker sees ledger event MMVault→AliceVault 3 · exact match · consumed once tx 2 · TM.finalizeSettlement(id) MM settlingOut 0 · Alice release 1 + credit 3 (coreBalance ≥ 13) requests observed (5 s poll, 5-min lookback) reserve + submit HyperEVM (one fast block) tx 1 · TM.settle(id, CUSTOMER_WIN) requires factory.isVault(both) state ACTIVE → SETTLED AliceVault.releaseTrade(1) → avail 10 MMVault.sendSettlement(3) USDC.transfer(AliceVault, 3) AliceVault.receiveSettlement(3) requires balanceOf ≥ 13 available 13 · actual 13 Any revert anywhere in the B transaction (including a failing ERC-20 call) undoes the release, the transfer and the credit together.
Per completed win or loss, A spends three EVM transactions (open, begin, finalize) plus one HyperCore transfer plus operator evidence matching; B spends two (open, settle). Void is two transactions on both and moves no money.

Deposits, withdrawals, onboarding

StepA · hypercoreB · hyperevm
Fund a participantAlready holds HyperCore spot USDC — nothing to dojust wallet move-usdc-to-evm role amt: HyperCore spotSend to system address 0x2000…0000 credits the linked ERC-20 (Hyperliquid documents 200k gas)
Create a vaultAdmin forge create UserVault(owner, TM, ADMIN, 0x0801, 0x3333…, 0) + TM.registerVault(owner, vault) — 2 txsAdmin factory.createVault(owner) — 1 tx; createVault is permissionless, one per owner
ActivateAdmin sends ~1 USDC to the vault's HyperCore account; operator classifies it activation, never credits itn/a
DepositParticipant spotSends to own vault account (free); operator observes the ledger, auto-attributes owner → own-vault, calls creditDeposit (Admin gas)Participant approve + deposit2 participant-paid EVM txs; needs EVM HYPE; no operator involvement
Stray fundsUnrelated sender → unclaimed row; inter-vault transfers skippedRaw transfer to a vault → unaccounted, no recovery function (documented as stranded)
Gasless withdrawalOwner signs EIP-712 → Admin relays withdrawWithSigwithdrawing + CoreWriter → operator observes payment → completeWithdrawal — 2 Admin txs + evidenceOwner signs EIP-712 → Admin relays withdrawWithSig → immediate safeTransfer — 1 Admin tx
Failure pathfailWithdrawal is manual and needs proof the spotSend did not executeTransaction reverts atomically
EIP-712 structWithdraw(owner, destination, amount, nonce, expiry)Withdraw(vault, owner, destination, amount, nonce, deadline) — signatures not interchangeable

TradeManager

A · 130 linesB · 103 lines
RegistryregisterVault(participant, vault) by operator; vaultOf[participant]; cannot replaceNone — trusts factory.isVault(); factory deploys the manager in its constructor
openTrade argsparticipant addresses; manager resolves vaultsvault addresses; manager checks factory membership, same manager, same USDC
Lock recordtradeLocks[id] → amounttradeLocks[id] → {counterpartyVault, amount}; release/settle must match both
StatesNone → Active → Settling → Settled / VoidNONE → ACTIVE → SETTLED / VOID
Win / lossbeginSettlement(id, outcome) then finalizeSettlement(id) — gated by operator evidencesettle(id, outcome) — release, transfer, receive in one call

Rust operator

AB
Workersevm_worker + core_worker + trade_worker + healthevm_worker + trade_worker + health — no HyperCore worker
Startup checkschain 998 · TM.operator()==Admin · every registered vault's owner/tradeManager/operator vs SQLite · keystorechain 998 · TM.operator()==Admin · keystore
Events decodedTradeOpened, TradeSettlementRequested, TradeSettled, TradeVoided, WithdrawalRequested, WithdrawalReconciled, DepositCreditedTradeOpened, TradeSettled, TradeVoided; vault events (Deposited, Withdrawn, CollateralLocked…) stored as Unknown
HyperCore pollingPer-account cursors, lookback max(2×poll, 5 min), raw core_observations, deposit attribution, settlement + withdrawal evidence matching, per-vault solvency → state.solventNone (core_once compiled but never called)
SolvencyPer vault physical ≥ totalCollateral; gates deposit credits; drives /readyDashboard computes actual ≥ accounted; operator never sets it → /ready always 503
SQLite migration 6participants, participant_vaults (+ activation buffer), isolated_deposits, settlements, consumed_settlement_events, vault_solvency; migration 7 scopes nonces per vaultparticipants(role), participant_vaults(role), erc20_transactions (unused); same version number, different schema
Per completed win/loss3 EVM txs + 1 HyperCore transfer + matching2 EVM txs

What the choice buys and costs

DimensionA · hypercoreB · hyperevm
Custody clarityGood — isolated per HyperCore account; needs precompile reads + operator evidenceCleanest — vault holds the asset; solvent() is one view call
Operator on the money pathCredits deposits, completes withdrawals (bounded by exact ids and physical checks)Opens and settles trades only — no deposit or withdrawal authority
Complexity4-bucket ledger, 2-phase settlement, ~1.4k-line reconciliation worker, state machines for settlements/deposits/solvency2-bucket ledger, 1-phase settlement, chain authoritative
Entry / exit~1 USDC activation per vault; deposits free for the participant; withdrawals land as HyperCore USDCCore→EVM move + participant-paid approve/deposit gas (needs HYPE); exit is another Core move
Smart-contract riskLower — no token heldHigher — assets on EVM (SafeERC20, reentrancy guard, CEI)
LatencyWaits for HyperCore ledger observation, then a second txFinal in one fast block once submitted; still behind the 10 s operator poll
Doc 07 says it suitsParticipants who keep capital on HyperCore and trade rarelyParticipants who fund once and trade many times

What neither branch does yet

Both authors' own notes agree on the list: participant-authorized tradesopenTrade is operator-only, so doc 07's owner / trading-key / relayer split is not implemented and a compromised operator could fabricate a trade between registered vaults; no pause, wind-down or retirement state; no operator or owner key rotation (key loss strands locked collateral); no reserve/active MM vault split; no batching, nonce management or native Rust submission; no link to the protocol layer — trades are enqueued from the CLI; and no benchmark of A against B, which is exactly what doc 07 asked for.

Worth raising with Francisco

  1. B: /ready can never return 200. Health.solvent is only set in core_once, which no worker on this branch calls. Drop the gate or compute actual ≥ accounted per vault in the trade worker.
  2. B: inherited dead code. core_once, credit_deposit → credit(), complete_withdrawal → completeWithdrawal(), the deposit-credit / retry-credit / pending-deposits CLI, and the erc20_transactions table all target functions this branch's contracts do not have.
  3. B: vault events undecoded. Deposited, Withdrawn, CollateralLocked/Released, SettlementSent/Received land in evm_logs as Unknown; the activity feed and any future reconciliation would want them.
  4. Both: migration-6 collision. The two branches define different schemas under the same version number. A database from one would silently "pass" migration on the other — renumber before either merges.
  5. Both: inconsistent poll defaults. init.sh writes 60 s / 90 s; operator configure and .env.example use 10 s / 5 s.
  6. B: naming. AuthoritativeTrade.customer / market_maker now hold vault addresses, not participants — rename before the protocol layer feeds real trades.
  7. B: deposit() is callable by anyone into anyone's vault (credits the owner). Economically harmless; note it for the gasless-deposit design.

Bottom line

A is the evidence-driven answer

It keeps USDC where Hyperliquid users already hold it and proves — with a mock precompile, an invariant suite and 50 Rust tests — that a winner is never credited before the HyperCore transfer lands. The price: two-phase settlement, a reconciliation worker, per-vault activation, and an operator that still touches deposits and withdrawals.

B is the atomic answer

The vault holds the token, so lock, settle and withdraw are single transactions, the operator has no money-path authority beyond trades, and solvency is a balanceOf comparison. The price: Core→EVM entry/exit friction, participant-paid deposit gas, EVM custody risk — and, today, some inherited dead code and a health endpoint that never says ready.

Neither branch has been benchmarked at realistic volume. Both READMEs refuse to claim economic savings without measurement, which leaves doc 07's decision exactly where it was — now with two working candidates to measure.

Questions answered from the docs and code

FAQ

Click a question to open it · link to one with #faq-q1

Legend — variables and terms used in the answers

Symbol / termMeaning
SCustomer stake — what you put down on the parlay. Reserved when you commit; locked in your vault at execution.
PAccepted total payout if every leg wins, including your stake. Authored by the market maker for the whole ticket; the only executable price.
LMarket maker's maximum liability = P − S. Locked in the maker's vault at execution. Identity: S + L = P.
Leg / synthetic legOne selectable outcome inside your parlay, identified by canonical_leg_id and its Outcome Market source ids. Not a separate trade or position.
Reference priceOutcome Market's observed probability for a leg — informational only, never executable, latest value only (no history).
RFQ → quote → commit → last look → executionRequest for a price → maker's immutable P (expires_at) → you accept (stake reserved, confirm_by clock) → maker confirms / rejects / replaces → atomic lock of S and L creates the trade.
Position / tradeThe single binding parlay record created at execution: S, P, L, legs, rulebook and mapping versions; state open → resolution_pending → final_ → settled_.
Available / LockedYour vault's buckets: free to withdraw or stake / committed to open trades. Branch A adds Withdrawing and settlingOut.
Vault / TradeManagerYour immutable per-participant collateral contract on HyperEVM / the shared contract that locks both sides and settles.
Protocol storeOutcome's off-chain records (RFQ, quote, action, decision, execution, trade, resolution, settlement) — specified in the API doc, not yet built in the repos.
OperatorOutcome's Rust service (Admin key) that submits trades, relays withdrawals and reconciles; it cannot move your funds.
Branch A / Boutcome-clearing variants: A = collateral stays on HyperCore (async settlement); B = USDC held inside the HyperEVM vault (one-tx settlement).
WIN / LOSS / REFUNDThe only three endings: +P to Available / −S to the maker / exact S back (any leg void or unresolvable).
01Where are the odds of each leg stored when a customer builds a parlay?

They aren't — by design. Two different things get called "price":

Per-leg reference priceThe parlay's price
WhatExternal probability observed on Outcome Market for one leg (PriceObservation)Maker's total payout P for the whole combination at stake S
Executable?No — "never executable Outcome or market-maker quotes" (doc 04)Yes — the only number the customer commits to
Per-leg breakdown of P?None. Outcome never derives or stores leg odds from P; the maker owns dependence modelling (doc 01)

Storage, layer by layer:

  • Catalogue (doc 04, running): Postgres catalogue_legs = leg identity/eligibility (durable); Redis = one latest reference price per leg, rebuildable, no history.
  • RFQ (doc 03, spec): legs[] with canonical_leg_id, venue, market/outcome ids, payoff meaning, raw + normalized state, observation time, provenance, mapping/rulebook refs; pricing_snapshot_id; optional reference_observations (inputs for the maker); stake = S.
  • Quote (doc 03): maker submits only payout_units = P; Outcome derives L = P − S. Implied parlay odds = P / S; no per-leg split exists.
  • Trade (doc 03): immutable record of legs + S, P, L + snapshot and policy/rulebook/mapping versions — "exact immutable integers after acceptance".
  • On-chain (outcome-clearing, both branches): TradeManager.trades[id] = {customerVault, makerVault, stake, liability, state} — no legs, no odds; P = stake + liability. SQLite trades.payload_json carries only ids, stake, liability, outcome.

Why: settlement never consults a price again (legs resolve won/lost/void → WIN pays P, LOSS takes S, REFUND returns S); leg odds don't multiply for correlated legs; S/P/L immutability. The one future feature that would need per-leg economics — void-adjusted settlement — is deferred and would require pre-agreed subset payouts in the quote, not inferred leg odds.

Gap: with no price history (Q35), the only durable capture of leg reference prices at RFQ time is the RFQ record's pricing_snapshot_id / reference_observations. The RFQ/quote service is not in the repos yet, so nothing persists this today. If product or analytics needs "what were the legs priced at when you bet", the RFQ store must persist the observations themselves.

Sources: summaries 01, 03, 04; code/outcome-clearing-comparison.md §2.

02Does executing a parlay create a position on my account?

Yes — execution creates it; committing only reserves your stake. The position is one parlay trade, not per-leg positions.

MomentPosition?What exists
QuotedNoA quote record (P, expires_at) on Outcome's side; your account untouched
CommittedNo — a reservationcustomer_action.recorded with S, P, L, confirm_by; stake S reserved (UI: Available −S); nothing on-chain; released if the maker rejects, times out or the market is invalidated
ExecutedYes — one parlay positionOff-chain trade.created (immutable: legs, S, P, L, snapshot, rulebook/mapping versions); on-chain TradeManager.trades[id]{stake, liability, state} and your vault locked += S under tradeLocks[id]
SettledClosedsettled_win (+P), settled_loss (0), refunded (+S); vault locked released
  • Granularity is the ticket. Legs are stored inside the trade as identities + resolution context so each can be resolved won/lost/void later — not as separate positions. No per-leg cash-out, P&L or mark-to-market. A synthetic leg "is not a separate customer trade at the reference market"; nothing is opened on Outcome Market for you.
  • The position's value needs no odds: exactly P if every leg wins, 0 if any leg loses, exactly S back if any leg is void/unresolvable. That is why leg prices at bet time are never consulted again.
  • API trade "position state": open → resolution_pending → (suspended) → final_win | final_loss | final_refund → settled_win | settled_loss | refunded.
  • In the clearing code the stored position is thinner still: SQLite {id, customer, market_maker, stake_atoms, liability_atoms, final_outcome} and on-chain {stake, liability, state} — legs live only in the (spec-only) protocol store.

Sources: summaries 01, 03; state-machines.md; code/outcome-clearing-comparison.md §2.

03So the stake, the position and the legs are stored in our database?

Yes — across three stores with different jobs; only the off-chain protocol store holds all three together.

WhatOutcome protocol store (off-chain, authoritative record; spec, not built yet)On-chain (vault + TradeManager)Clearing operator SQLite (PoC today)
Stake Syes — immutable trade recordyes, enforced: trades[id].stake; vault locked += Syes (stake_atoms)
Position (trade + state)yes — trade_id, open → resolution_pending → final_ → settled_yes — trades[id].state; locked in both vaultsyes (trades row: ids, status, tx hashes, outcome)
Legsonly herecanonical_leg_id, source ids, payoff meaning, raw+normalized state, mapping_version, rulebook_ref, pricing_snapshot_idnono
P / Lyesliability; P = stake + liabilityliability_atoms
Per-leg oddsno (snapshot pointer / optional reference_observations only)nono

The database is the authoritative description of the position; the vault lock is the authoritative collateral. Outcome cannot administratively change a binding trade's parties, stake, payout, wallets, rulebook or mapping versions, and neither record may silently overwrite the other — discrepancies go to reconciliation.

04When a trade settles as a LOSS, how does my locked stake get to the maker?

Trigger (both branches): resolution marks the parlay final_loss → Outcome authorizes settlement → the operator submits the outcome. Nobody signs; the TradeManager can only move the exact amount recorded under that trade id to the counterparty fixed at openTrade — it has no generic transfer function.

Branch A — collateral on HyperCore (two phases, physical transfer between them)

StepWhereEffect on your stake S
1Operator → TradeManager.beginSettlement(id, CustomerLoss)Trade Active → Settling; TM calls yourVault.beginSettlement(id, makerVault)
2Your UserVaultlocked −= S, settlingOut += S, lock deleted; _spotSend(makerVault, S) via CoreWriter (spotSend action 6). Receipt proves only the request
3HyperCore (async)Real USDC moves from your vault's HyperCore account to the maker vault's account; ledger spotTransfer{from: yourVault, to: makerVault, amount: S}
4Operator core_workerExact match on source, destination, token, amount, time ≥ request; event consumed once → settlement observed
5Operator → TradeManager.finalizeSettlement(id)yourVault.finalizeSettlementOut(id, S) (settlingOut −= S, totalCollateral −= S) · makerVault.releaseTrade(id) (maker's L unlocks) · makerVault.creditSettlement(id, yourVault, S) — reverts ClaimsExceedCore unless coreBalance() ≥ totalCollateral + S, then available += S. Trade Settled

Branch B — USDC inside the HyperEVM vault (one atomic tx): TradeManager.settle(id, CUSTOMER_LOSS)makerVault.releaseTrade(id, yourVault, L)yourVault.sendSettlement(id, makerVault, S) (locked −= S, usdc.safeTransfer(makerVault, S)) → makerVault.receiveSettlement(id, yourVault, S) (requires balanceOf ≥ available + locked + S, then available += S) → SETTLED. Any failure — including a false-returning token transfer — reverts everything and both locks stay.

  • Only S crosses vaults on a loss; the maker's L never moves, it unlocks (mirror on a win: L crosses, S unlocks).
  • Destination fixed at open time (A: vaultOf, B: counterparty stored in the lock); no redirect, no resize, no double settlement (Settled → revert).
  • Your available balance is never touched by settlement — only the amount locked under that trade id.
  • Phase I on main moved nothing on a loss; it reassigned a claim inside one shared account — the design Phase II rejected.

Sources: code/outcome-clearing-comparison.md §3; TradeManager.sol / UserVault.sol on both branches; tests testLoss, testLossSettlement, testTokenFailureRevertsAccountingAndSettlement.

05Does my stake move to the maker's vault when the parlay executes, and come back if I win?

No on both counts. Nothing crosses vaults at execution, and on a win your stake never left — it unlocks in place; what you receive is the maker's L.

1. CommitS is reserved (off-chain record with S, P, L, confirm_by). Nothing on-chain.

2. Execution → the trade record (S, P, L, legs, versions) is stored, and TradeManager.openTrade locks S inside your vault and L inside the maker's vault, each in place, atomically. No transfer between vaults.

3. Settlement is the only moment value crosses, and only one side crosses:

OutcomeYour vaultMaker vaultPhysically moves
LOSSlocked −S; S leavesL unlocks; +S receivedS: you → maker
WINS unlocks in place; +L received ⇒ net +Plocked −L; L leavesL: maker → you
REFUNDS unlocks in placeL unlocks in placenothing

4. Lock accounting is on HyperEVM on both branches. A: USDC sits in each vault's own HyperCore account; the crossing amount is a CoreWriter spotSend loser → winner, confirmed by the operator before finalizeSettlement credits it. B: USDC sits inside the HyperEVM vaults; the crossing amount is an ERC-20 safeTransfer inside the single settle transaction.

> Commit reserves; execution locks in place; settlement moves exactly one amount one way (S to the maker on a loss, L to you on a win, nothing on a refund) and unlocks the rest where it already is.

06How does the market maker earn in this system?

One way only: by quoting the whole parlay below its fair probability and keeping the stakes of losing tickets. No spread, fee or rebate is paid to the maker by Outcome; the edge lives entirely inside P.

  • Per ticket the maker earns S − p_true × P, where p_true is the real probability that every leg wins. Profit requires P < S / p_true — the quoted implied probability exceeds the true combined probability. Over the book: Σ losing stakes − Σ winning liabilities; refunds are flat.
  • Example (Litepaper): S = 100, P = 350 → implied 28.6%. If the combination truly hits 25%, expected profit is 100 − 0.25 × 350 = 12.5 per ticket; at 30% it loses 5.
  • Where the edge comes from (all the maker's, per the Litepaper): correlation modelling (Outcome stores no per-leg odds and does not prescribe the model), margin over the fair product, sharper-than-reference pricing of Outcome Market inputs, inventory and concentration skew, adverse-selection widening (last look protects the quote-to-execution gap), and pricing in hedge cost, capital usage and required return.
  • Costs: L fully locked per trade until every leg is final (no netting or portfolio margin; long-stop bounds disputes); optional hedging at the maker's discretion and expense; zero gas (Admin sponsors all HyperEVM transactions); unpriced adverse selection.
  • What it pays Outcome (Commercials): gross trading result − agreed costs + eligible rebates = defined trading profit, split Outcome [TBD]% / MM [TBD]% monthly. Negative month → Outcome gets nothing, maker bears the loss, no carry-forward. No retainer, minimum guarantee or high-water mark by default; distribution partners are paid from Outcome's share; any future customer fee never touches P, L or the maker's cut.
  • What it gets: Outcome-originated (later partner) flow with no product, distribution, catalogue, RFQ, collateral or settlement build; full control of price, size and hedging; in return the SLA targets (≥ 90% coverage, p95 ≤ 500 ms, ≥ 99% last-look decisions), the capital, and responsibility for every executed trade through settlement even after termination.

Sources: summaries 01 (§Product, §Capital usage, §Economics), 02 (§Commercial framework); decisions D7, D8, D21, D29, D40–D42.

07Is there a chance of a race condition anywhere in the planned system?

Yes. They split into races the spec serializes away, races that exist in the code today, and seams the spec leaves open.

Designed-out by the protocol (rule: "Outcome's recorded order decides every race"): replace/cancel vs. commit; expiry vs. commit; confirm/reject/replace/timeout/invalidation in last look (first eligible recorded decision wins); invalidation vs. execution (blocks only before submission_authorized); concurrent capacity reservations (one serialized tx, atomic swap on replace); duplicates and redelivery (idempotency namespace, message_id replay, event_id dedupe, per-aggregate sequence guards); double settlement (permanent uniqueness: ≤ 1 selection, ≤ 1 execution lineage, ≤ 1 trade, ≤ 1 terminal effect). On-chain, openTrade locks both vaults atomically and settlement reverts on any non-eligible state.

Present in the clearing code

#WhereRaceSeverity
R1BothOutcome enqueued before TradeOpened is ingested; status flips only via the event path's final_outcome check — works, fragile if workers are reorderedLow
R2ALedger event observed before request_tx_hash is written; recovered on the next core poll (unmatched_outbound_observations)Low, self-healing
R3ATwo same-amount loss settlements (Alice→MM) → two identical ledger events; each consumed once, but event↔trade assignment is iteration orderLow–Med (audit)
R4AWithdrawal matching demands exactly one candidate; two identical pending withdrawals stay pending forever until manual completeWithdrawalMedium (liveness)
R5BothSingle Admin EOA nonce lane shared by every worker; concurrent submits → nonce collision/stuck tx blocks everything behind it; ambiguous submissions never auto-retriedMed–High
R6BothTwo operator processes on one DB would race on nonces and status writes; documented as unsafe, no leader lockHigh if attempted
R7ASolvency gate flickers false during in-flight withdrawals → deposit credits pause briefly (fails closed)Low
R8AOn-chain registerVault and SQLite participant-register are sequential, not atomic; a missed step leaves a vault unwatched until restartMedium
R9BPermissionless createVault / anyone-can-deposit — front-runnable but harmless (same immutable owner)Low
R10AbeginSettlement moves locked → settlingOut and requests a spotSend; if HyperCore never executes it, the trade sits in Settling with no on-chain timeout or failSettlement (only failWithdrawal exists)Med–High

Open seams in the spec: no quantitative cap yet on maker replace/reject rates during last look (Q8); a source correction after settlement.authorized but before finality is audit-only, so a known-wrong settlement can complete and become a correction case; long-stop parameters are TBD (Q15) so the evidence cut-off is undefined; protocol-store ↔ chain reconciliation is daily, so the two can disagree between runs; openTrade needs no participant signature, so ordering is the only defense until doc 07's trading keys are built.

Bottom line: protocol layer is race-hardened on paper and contracts are atomic where it matters; live risk sits in the operator and the cross-environment seams (R5, R10, R4, R8). Branch B removes R2, R3, R4 and R10 by settling inside one EVM transaction.

Sources: summaries 03 (§Ordering, §Sequence guards, §Idempotency); code/outcome-clearing-.md; rust/src/operator.rs, storage.rs on both branches; SPEED_AND_SCALABILITY.md.*