Relay-backed Seamless v2
Decision
RGB Provider callbacks use the original Provider protocol through Aggregator, while retaining Seamless wallet ownership boundaries:
- Provider calls Aggregator.
- Aggregator authenticates Provider credentials/source and resolves the launch-created local Player/Brand mapping.
- Aggregator stores a PENDING receipt before any downstream money effect.
- RGB claims the signed event, executes its native Provider handler, calls
wallet_service, and commits its complete bet/business ledger. - RGB returns a signed internal result envelope containing the native handler's Provider business response, committed entity snapshots and wallet audit.
- Aggregator verifies the envelope and Provider response shape, transactionally appends
its platform projection, marks the receipt APPLIED, then releases the Provider response.
Protocol fields that require an Aggregator-owned Provider secret are finalized only at
the public edge; Digitain
TimeStampand orderedSignatureare the explicit case.
RGB is the single money executor. Both systems retain their own bet data; Aggregator's copy is a platform projection, not a second wallet ledger.
Request identity
The HMAC continues to cover timestamp, method, exact RGB path, raw query bytes, encoded context and raw body. Context version 2 also contains:
contract = rgb-provider-relay-v2-2026-07-22event_id = prv2_<64 lowercase hex>provider_headers_hashover the exact fixed Provider authentication/parsing header setprovider_identity_headers_hashover stable Content-Type and MG request ID fieldsprovider_content_typecontaining the bounded, non-secret original Content-Typeprovider_request_contextcontaining only MG's non-secret request UUID and millisecond timestamprequest_hashover length-framed team/provider/operation/method/query/body/identity-header hash- authenticated Team/Provider/operation and Player aliases
RGB recomputes all hashes before the Inbox claim, so an internal proxy cannot alter a
header consumed by the native Provider handler. Refreshed Provider timestamp/signature
headers do not create a second write event for the same business request. Money/write
operations accept only
event_id = prv2_<request_hash>. Dynamic reads require a different fresh event id so a
balance/login response cannot be cached forever. Event id reuse with different immutable
scope is a conflict.
Normal delivery and controlled replay use the exact signed provider_content_type so a
JSON or XML request cannot acquire different parsing semantics during recovery. Provider
secret headers are still never persisted or replayed. MG recovery restores only its
request UUID/timestamp; RGB recomputes the replayed Content-Type + request-ID identity
hash and rejects missing, changed or legacy-incomplete correlation context before the
native handler can return a terminal 400.
Digitain GetUserInfo also carries a bounded provider_context.response_token.
Aggregator pre-signs exactly the next token version without publishing that version;
RGB returns it only after a successful player/balance lookup. Aggregator advances the
version atomically with projection apply before releasing the response to Digitain.
RGB durability
provider_relay_inbox is committed before the handler. It provides a bounded lease,
attempt count, immutable scope and exact completed envelope cache. attempt_count
also fences stale workers: after re-entry, an older attempt can neither release nor
complete the newer lease.
provider_relay_mutation_outbox is written from SQLAlchemy after_flush through the
same connection and transaction as each allow-listed RGB business row. A rollback
therefore removes both; a commit preserves both. The signed mutation carries the
Outbox row's monotonic mutation_id and database-clock committed_at; recovery never
substitutes the later envelope time. Historical Outbox rows are de-duplicated by
entity type/key, and every
in-memory committed entity must have a persisted Outbox counterpart, so a crash between
business commit and envelope storage cannot erase or reorder the Aggregator projection.
provider_relay_wallet_outbox immediately and idempotently persists the credential-free
wallet request/result under the same relay event. Recovery reads this table instead of
request memory; a later success replaces an earlier transient failure, while a stale
failure cannot overwrite an already durable success. Each physical wallet call has its
own call_id; command calls and their follow-up balance snapshots derive a bounded
SHA-256 identity from the stable wallet request id, so repeated GETs in one batch remain
distinct while an exact event retry cannot grow duplicate audit rows indefinitely.
The envelope is canonical JSON and is HMAC-SHA256 signed in
X-RGB-Relay-Signature. Its scope repeats Team, Provider, operation and Brand code;
projection scope contains authoritative RGB Brand/Player ids. Response headers are an
explicit safe end-to-end allow-list, body bytes are base64, and request/provider/envelope
sizes are independently bounded.
Every relay handler business side effect must finish synchronously inside the claimed request. Wallet, bet, Outbox or other business writes cannot be deferred to a Starlette background task: the result middleware detects such a response, releases the Inbox lease and returns 502 instead of marking the event completed before its work runs.
Failure and retry rules
- Invalid Provider/auth/context: reject before Inbox claim or business execution.
- Exact completed event: return cached envelope; do not run handler/wallet again.
- Live lease: return
409 Retry-After; do not run handler. - Any handler non-2xx status or exception: release lease; do not cache a completed result.
- Native Provider business rejections such as insufficient funds, invalid player, maintenance/disabled policy and true idempotency conflicts are terminal and may be cached. Wallet timeout/disconnect/circuit-open, wallet HTTP 5xx, HTTP-200 internal or malformed wallet envelopes, temporary Provider mapping/catalog absence and any batch containing a transient item are retryable: result middleware releases the Inbox and returns internal 503 instead of caching the native HTTP-200 error.
- Committed business state followed by process/network failure: retry same event; native Provider idempotency plus transactional outbox reconstructs and caches the result.
- Invalid/missing result signature or Aggregator projection failure: Aggregator retains UNKNOWN/retryable state and never releases an unverified Provider response.
- Digitain
GetUserInfotoken rotation and Aggregator projection/APPLIED transition commit in one local transaction. An exact retry may read the historical receipt after Digitain signature verification even though its old token version is no longer current.
No component performs an automatic loop retry of a money callback within the same
public request. In addition to Provider exact retries, Aggregator reconciles stale write
events through an exactly scoped, HMAC-authenticated
GET /aggregator/relay/{team_code}/events/{event_id}. A cached signed envelope is applied
directly. For pending/not-found state, Aggregator may then submit the immutable original
method/query/body with reconciliation=true in the separately signed relay context.
RGB reclaims the same event and converges through native Provider and wallet idempotency;
it uses the original signed Provider-header hashes from context, so raw Provider secret
headers are neither persisted nor replayed. The original signed Content-Type is restored
exactly; MG's non-secret correlation headers are restored and rebound to the original
identity hash. The status endpoint itself never executes a handler, and a live lease still
returns conflict without concurrent money execution.
Native lifecycle, retention and observability
- Digitain credit/batch/debit/rollback/ChequeRedact maintain
SportsBet,SportsBetLine,DigitainCreditBatch,DigitainDebit,DigitainRollbackandDigitainTransaction. BTI/BT1/SPlus preserve every XML Bet and Selection, including four-decimal odds, child status, score, settlement time and resettlement state. Odds useNUMERIC(17,4), preserving the priorNUMERIC(15,2)integer capacity during migration. - BTI selection updates are keyed by
(bet_id, line_id); reused line ids in different child bets cannot cross-update. Native and transaction identity indexes fail migration closed when legacy duplicates require operator reconciliation. - BTI reserve persists wallet_service's complete ordered
funding_breakdown. Eachdebitreservechild consumes a contiguous slice of that authoritative order, and its generic/native bet projections retain the exact coupon/bonus/normal/withdrawable attribution. Aggregate source totals and every before/after transition are validated; source kind/code/role identity is fail-closed, and Digitain child slices must preserve the same physical source's cross-child balance continuity. Malformed authorization envelopes remain retryable rather than being cached. - Digitain batch idempotency remains durable across releases. A pre-upgrade cached child allocation with unsplit balance transitions is recovered only from the locked original reserve, then cross-checked against every brand-scoped historical child and rewritten to the canonical response. Immutable request/bet/Provider/source/amount or topology-policy mismatches fail closed; completed money idempotency is never deleted to force re-execution.
- Topology and bucket identifiers are canonical ASCII with a 64-character ceiling.
Wallet groups are limited to 57 characters so the generated
_COUPONsource remains within that same wire/database limit; draft writes and atomic activation revalidate the contract even for unchecked in-process models. - Wallet balance reads require a complete
status=OKtopology snapshot with positive topology/policy versions and atotal_balanceequal to all returned buckets and coupons. Bucket codes are non-empty and unique, roles are canonical, and every Bucket/Coupon amount is finite, non-negative and cent-precision. A malformed HTTP-200 response is a transientwallet_protocol_error, never a successful or inflated balance and never misclassified aswallet_unavailable. - Inbox maintenance exposes stale-pending and failure metrics. Completed dynamic reads
are retained 1 day, completed writes 90 days; an expired read-only pending event may
be abandoned after 1 day, but unresolved write events are never deleted. Read/write
classification is applied in PostgreSQL before every bounded
LIMIT. Completed events and abandoned stale reads share one oldest-first batch, so neither a row class nor a priority scan can starve reconciliation or retention.