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:
- Confirmation policy: Bifrost matching is provider-confirmed. A successful placement HTTP
response is
PENDINGacknowledgement 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. - Fill policy: currently integrated
9.xand14.xmarkets are all-or-nothing. Betfair and a future Bifrost1.xexchange 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:
PLACEDplus validatedsizeis full-match authority.- Missing
sizeMatchedis non-blocking telemetry, not incomplete financial evidence. - Present
sizeMatched == sizecorroboratesPLACED. - A partial
sizeMatched, a non-zero remaining bucket,PARTIALLY_MATCHED, orUNMATCHEDcontradicts 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
betRequestIdsbecause providerbetIdvalues 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.