Skip to main content

Bifrost confirmation and fill policies are explicit and fail-closed

Status: AMENDED AND ACCEPTED (Bhargav, 2026-07-22; supersedes the optimistic-placement portion of the 2026-07-17 decision and adds durable intent/audited replay).

Bifrost's market taxonomy is not, by itself, a financial lifecycle contract. Hannibal separates two questions that were previously conflated by the term instant_match:

  1. Confirmation policy: Bifrost matching is provider-confirmed. A successful placement HTTP response is PENDING acknowledgement and may attach provider identity/metadata only. It cannot mark an order matched, repeat netting, or downgrade a lifecycle state already advanced by a queue snapshot.
  2. Fill policy: currently integrated 9.x and 14.x markets are all-or-nothing. Betfair and a future Bifrost 1.x exchange integration are partial-fill capable.

At Bifrost 9.x/14.x placement, Hannibal persists the full order as unmatched and applies only the accepted/unmatched P-max contribution. No matched contribution or matched reserve release exists until authoritative queue confirmation. A PLACED snapshot whose immutable identity and requested size validate atomically moves the full contribution from accepted/unmatched to matched. A pre-confirmation FAILED removes the unmatched contribution and releases its reserved liability. The same transaction advances the lifecycle version, order fields, market exposure, balance, and ledger so no observable half-transition exists.

The original Bifrost PDF defines PLACED as fully matched and labels sizeMatched, sizeRemaining, sizeLapsed, sizeCancelled, sizeVoided, and averageOdds as applicable to exchange markets. Therefore 9.x/14.x confirmation does not depend on that exchange Size Vector:

  • PLACED plus validated size is full-match authority.
  • Missing sizeMatched is non-blocking telemetry, not incomplete financial evidence.
  • Present sizeMatched == size corroborates PLACED.
  • A partial sizeMatched, a non-zero remaining bucket, PARTIALLY_MATCHED, or UNMATCHED contradicts the all-or-nothing policy. Hannibal performs no state/netting mutation, persists the exact snapshot to the DLQ, and emits a deduplicated financial-integrity alert.

Before the first provider-confirmed PLACED, full CANCELLED or LAPSED terminal evidence disposes the still-unmatched order. After PLACED has been applied, FAILED, CANCELLED, or LAPSED may not erase matched evidence. Provider invalidation of a confirmed match is VOIDED and belongs to settlement.

The queue contract's "full snapshot" rule controls replacement and ordering; it does not redefine the financial meaning of a status. A newer FAILED snapshot still means placement failure, while a newer VOIDED snapshot means invalidation of a bet that was matched. Consequently, snapshot version alone never authorizes Hannibal to turn a confirmed matched contribution into an unmatched refund.

1.x retains the existing internal two_phase_exchange label, but this introduces no new lifecycle status or second state machine. It is the same accept-then-match accounting behavior already used by Betfair. Its adapter preserves the provider's absolute sizeMatched, sizeRemaining, sizeLapsed, sizeCancelled, and sizeVoided buckets and prefers averageOdds for the matched slice. Real-money 1.x placement remains blocked until exchange routing, position-take, cancellation, and post-placement dispatch are enabled and verified end to end. Unknown market families always fail closed.

Protobuf presence

The exchange bucket fields are non-optional proto3 numeric scalars, so the provider schema alone does not prove whether an omitted tag was reported as financial zero. The transport boundary preserves raw presence and never globally invents financial defaults. For 9.x/14.x, status plus validated size determines the all-or-nothing observation. Future 1.x has no verified live evidence yet, so Forsyt additionally requires every absolute-vector bucket to be present on the wire and to reconcile with requested size and status before it may drive a transition. This fail-closed integration policy does not claim that the provider schema marks those fields required.

Ambiguous placement outcomes

A post-send HTTP timeout, connection reset, or lost response does not prove that Bifrost rejected the order. Hannibal writes a ProviderPlacementIntent in the same database transaction that creates the Order and reserves its liability. The intent freezes the exact provider-native request, request ID, payload hash, native odds, price-index/line presence, provider stake, and authoritative placement-time MarketBook.oddsType. Dispatch and post-submission processing never rebuild these values from mutable MarketBook, betslip, FX, or PT state. The recovery worker asks Bifrost only to re-emit the snapshot by the original placement requestId; automatic placement resubmission remains disabled until Bifrost formally documents same-request, byte-identical-payload idempotency. While no authoritative queue snapshot has resolved the order:

  • the local order remains unmatched;
  • the full accepted/unmatched contribution and reserved liability remain unchanged;
  • no FAILED, cancellation, refund, or matched transition is synthesized;
  • Hannibal emits one deduplicated operational/financial-integrity alert; and
  • recovery requests the authoritative snapshots using the unresolved orders' persisted betRequestIds because provider betId values may not be known and are not match authority.

Only authoritative provider evidence may resolve the ambiguity: PLACED applies the full match, FAILED releases the unmatched contribution, and any contradictory snapshot follows the fail-closed DLQ/alert policy above. Recovery exhaustion or elapsed time alone never releases liability.

Bifrost's documented recovery response ({ requestId, error }) acknowledges only that an asynchronous recovery request was accepted. The original PDF and recovery OpenAPI do not define a negative existence result for betRequestIds, so an empty error or the absence of a republished snapshot is not evidence that no provider bet exists. If repeated same-requestId recovery attempts remain silent, Hannibal keeps the order unmatched and the full accepted liability reserved, then escalates the unresolved financial-integrity incident to operations. Automatic release becomes safe only if Bifrost supplies an authoritative rejection/FAILED snapshot or a formally documented negative-existence contract stating that the request did not create, and cannot later create, a bet.

Recovery is durable across backend restarts. New ambiguous intents are selected through the indexed (providerId, state, nextAttemptAt, updatedAt, id) schedule in bounded batches of 50. Dispatch and recovery have separate attempt counters. Both HTTP-acknowledged and ambiguous unmatched intents receive a recovery deadline; stale dispatching is fenced into ambiguity after five minutes. A stale prepared intent is provably pre-send and is surfaced for guarded reversal/operator handling without calling provider recovery or placement. Each accepted or failed snapshot-recovery request advances durable recovery metadata and exponential backoff; recovery silence never changes order state or liability. For the enabled 9.x/14.x policy, authoritative queue PLACED confirms the intent; FAILED, full LAPSED, and full CANCELLED reject it. Each terminalization clears recovery coordination in the same transaction as the Order transition. PENDING/CANCEL_PENDING remain non-terminal, contradictory partial states are quarantined, and VOIDED remains on the separate settlement axis. A distributed owner-token lock, heartbeat, and fenced commit prevent overlapping workers. Durable and legacy recovery pages alternate through an owner-fenced lane so neither backlog can starve the other; stale/unscheduled anomaly alerts also use bounded keyset cursors plus an aggregate backlog alert. Pre-intent legacy orders remain on the bounded fixed-horizon Order cursor until audited reconciliation; they are never given inferred intent rows. Once an authoritative legacy PLACED snapshot is durably quarantined specifically because its immutable price evidence is unavailable to the normal consumer, automatic recovery pauses for that order. Re-requesting the same snapshot cannot create that evidence; the liability remains reserved and the keyset cursor continues while the row awaits audited operator repair.

DLQ persistence deduplicates exact observations by a canonical payload hash/message identity and increments occurrence metadata instead of creating unbounded identical rows. Replay requires a typed operator reason, a fenced claim with heartbeat, and an immutable before/after attempt record. For pre-intent PLACED rows, an operator must first preview and bind one exact stored payload's authoritative resolvedOddsFormat as BifrostLegacyPriceEvidence. Apply is permitted only for the legacy already-fully-netted shape: full user/provider matched stakes, non-negative atomic acceptedAcr/matchedRc, an exact deterministic full matched PnL vector, and a zero accepted vector. The historical machine high-water may be null; sizeMatched may also be null or, if present, must equal the full provider stake. A populated high-water requires a populated full sizeMatched. Mixed or contradictory mirrors are rejected. It runs in one serializable transaction under the normal per-order advisory lock, validates the payload through the pure mapper, and CAS-updates only status, provider bet identity, and lifecycle version. It does not call the generic replay handler and cannot write balance, exposure, ledger, netting, or Phase-1 settlement state. The source resolution, replay audit, and equivalent-duplicate cleanup commit atomically with that metadata repair. Rows for the same order/type may be marked superseded only when their provider snapshot is byte-for-byte equivalent and their local resolvedOddsFormat enrichment is either absent or equal to the approved source. Contradictory formats, different provider fields, lifecycle evidence, message types, or orders are never bulk-resolved.

After all related lifecycle repairs commit, the operator invokes the separate outcome-completion endpoint. It replays any unresolved durable BetOutcomeSnapshot rows for that order through the normal version-fenced settlement handler. If no outcome is already quarantined, it requests asynchronous outcome redelivery by the persisted placement requestId. This remains a distinct audited financial command: lifecycle repair itself has zero financial delta and never implies that settlement completed.

Price-format authority

Market prefixes and MarketCatalogue.oddsType describe family/structure, not price encoding. Live evidence includes both HAAR_JEET and DECIMAL on 14.x. The placement-time MarketBook.oddsType is authoritative and is frozen in the placement intent. After submission, financial canonicalization reads the intent (or explicit audited legacy evidence), never current cache state and never a prefix default.

Price-level limit authority

For 9.x/14.x, a displayed/executable price level is complete only when its maxStakeCurrencies map contains the configured Bifrost API/map currency. Observed provider payloads encode ISO currency keys in lower case while Hannibal conventionally configures HKD, so the Bifrost boundary compares currency keys case-insensitively. It preserves an explicit zero, rejects invalid amounts and duplicate case variants, and never substitutes the unqualified scalar maxStake or exchange-liquidity size. A level without exact currency-qualified evidence is not published as executable.

The selected depth amount is not rescaled at the adapter boundary. HTTP fixture responses identify it by the separate Hannibal accounting/rate denomination and convert it to the requesting user's denomination. Direct Bifrost WebSocket deltas therefore carry an explicit sizeCurrency equal to the Bifrost Provider.currency, the same accounting/rate basis used by HTTP limits and stake conversion. This code may be a synthetic alias distinct from BIFROST_CURRENCY, which remains the provider request/map label. The client must apply the authenticated exact ratio for that accounting code before either the fixture cache or betslip sees the delta. Missing or contradictory denomination/rate evidence rejects the whole live delta and triggers an authoritative HTTP refetch. A malformed tick never combines a new price with an old limit: the previous complete price/native/index/line/limit observation remains unchanged until a complete replacement arrives.

Snapshot ordering and identity

Bifrost lifecycle messages are full snapshots, not deltas. Hannibal therefore follows the provider queue contract directly: BetSnapshot.version is a positive monotonic ordering key, stored in orders.provider_lifecycle_version and compared inside the same locked/CAS transaction as the exposure, balance, ledger, and order mutation. An older or equal version is a no-op; a newer version advances the watermark even when the domain state is already converged. This field is deliberately separate from settlement_version, because lifecycle snapshots and outcomes arrive on independent queues. A lifecycle CAS loser is retried rather than acknowledged because Bifrost has no polling backstop.

The snapshot must also agree with immutable placement identity: market, runner suffix, side, line, and requested provider stake. A mismatch is a domain-integrity failure, never an alternate order interpretation.

Settlement-axis correction

VOIDED remains outside the lifecycle reducer, as does the separate outcomes queue. For a live void or unvoid, however, cash and exposure still move in lockstep: the signed balance movement is the current locked market-level emax delta produced by reversing/re-adding the matched vector. The placement-time acceptedAcr - matchedRc snapshot is audit context, not the refund basis, because sibling orders can change the marginal exposure after placement. Lifecycle version is also the idempotency key for repeated VOID → PLACED → VOID correction cycles; a transaction type is not a lifetime-wide idempotency marker.