Skip to main content

Relay-backed Seamless v2

Decision

RGB Provider callbacks use the original Provider protocol through Aggregator, while retaining Seamless wallet ownership boundaries:

  1. Provider calls Aggregator.
  2. Aggregator authenticates Provider credentials/source and resolves the launch-created local Player/Brand mapping.
  3. Aggregator stores a PENDING receipt before any downstream money effect.
  4. RGB claims the signed event, executes its native Provider handler, calls wallet_service, and commits its complete bet/business ledger.
  5. RGB returns a signed internal result envelope containing the native handler's Provider business response, committed entity snapshots and wallet audit.
  6. 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 TimeStamp and ordered Signature are 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-22
  • event_id = prv2_<64 lowercase hex>
  • provider_headers_hash over the exact fixed Provider authentication/parsing header set
  • provider_identity_headers_hash over stable Content-Type and MG request ID fields
  • provider_content_type containing the bounded, non-secret original Content-Type
  • provider_request_context containing only MG's non-secret request UUID and millisecond timestamp
  • request_hash over 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 GetUserInfo token 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, DigitainRollback and DigitainTransaction. BTI/BT1/SPlus preserve every XML Bet and Selection, including four-decimal odds, child status, score, settlement time and resettlement state. Odds use NUMERIC(17,4), preserving the prior NUMERIC(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. Each debitreserve child 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 _COUPON source 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=OK topology snapshot with positive topology/policy versions and a total_balance equal 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 transient wallet_protocol_error, never a successful or inflated balance and never misclassified as wallet_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.