System diagram
Off-chain services
core-api
core-api
Client-facing HTTP gateway. Partner traffic authenticates with
X-Api-Key on core.api.dev.predictstreet.sde.adifoundation.ai; retail user sessions
live on a separate surface, the web app
(app.dev.predictstreet.sde.adifoundation.ai on testnet), and
are out of scope for this documentation. Rate limits, geo and
compliance guards, KYC gating. Every request enters here before
being routed to the appropriate back-end. Owns compliance
screening (Global Ledger), sanctions and banned-wallet checks. See
Authentication overview and
API keys.exchange-service
exchange-service
Orders, balances, trades, fees, withdrawals. PostgreSQL-backed
single source of truth for off-chain state. Verifies EIP-712
signatures, locks balance, calls matching-core over gRPC, persists
results, publishes events to NATS JetStream.
matching-core
matching-core
In-memory central-limit orderbook engine (Go). One engine per
market. Implements FIFO priority within a price level; supports
GTC / IOC / FOK time-in-force and LIMIT / MARKET order types.
Stateless from a client perspective — exchange-service holds
authoritative PG state; matcher state is warm-restartable.
oracle-service
oracle-service
Polls the data providers bound to each market (Stats Perform,
API-Football, LSports and other sports feeds; price data for
crypto markets). When a market is bound to several providers, it
proposes only when a strict majority of them agree on the outcome;
otherwise the market goes
DELAYED. Posts outcomes on-chain via
proposeOutcome on Oracle (binary markets) or NegRiskOracle
(neg-risk markets).
Handles the 2-hour challenge window, override, void, and
delayed-data paths. See
Settlement overview.match-submitter
match-submitter
Broadcasts matched trades on-chain in batches. Maintains a pool
of pre-funded operator EOAs (whitelisted via
CTFExchange.addOperator() on both binary and neg-risk exchanges),
an atomic per-wallet nonce manager, and a tx-tracker that polls
receipts and reclassifies stuck or dropped transactions. A
gas-refiller cron tops up operator wallets from a treasury
address. On a full revert, trades are marked
settlement_failed; per-order failures come in via
MatchFailed events from chain-watcher.ws-gateway
ws-gateway
WebSocket fan-out. Consumes NATS JetStream events from
exchange-service and oracle-service and multiplexes channels.
Handshake auth accepts
X-Api-Key (partners) or a retail bearer
JWT on both /ws/user and /ws/market, with the same scope rules
as HTTP. /ws/user requires credentials: a handshake without them
closes 1008 unauthorized. /ws/market also admits anonymous
browser connections (authMethod: "anon") for its public
channels. An invalid API key closes 4401 <reason> on either
gateway; an invalid or expired bearer token closes
1008 unauthorized. See
WebSocket overview.chain-watcher
chain-watcher
Ponder-based indexer for every PredictStreet contract (Exchange,
ConditionalTokens, NegRiskAdapter, VaultFactory, Oracle).
Persists events in durable outbox tables and publishes them to
NATS JetStream (
chain.> subjects) with at-least-once delivery.
Handles seq-gap recovery and reorg rewind. Internal — partners
consume confirmed state through core-api / WebSocket, not
directly from chain-watcher.On-chain contracts
User Vault
User Vault
One vault contract per user, deployed by the platform through
VaultFactory when the wallet is onboarded: on its first web-app
sign-in or KYC approval, or, for multi_wallet partners without
per-wallet KYC, on the first API request that names the wallet in
X-User-Wallet. A background sweep re-queues the deploy for
wallets still missing one. See
Backend auto-deploy.The vault must exist before you can deposit or trade. It holds the
user’s ERC-20 collateral (USDC) and ERC-1155 outcome positions,
and it is the maker of every order the wallet signs. Orders from
a wallet without a vault are rejected with vault_not_deployed;
check that GET /api/me/vault returns deployed: true before
placing orders. Non-upgradeable — the vault bytecode is fixed in
the factory and never swapped under existing vaults.Custody model: withdrawals, split / merge, and neg-risk
convertPositions require a dual EIP-712 signature: the vault
owner’s plus a platform co-signer key registered on VaultFactory
(a different address). The backend cannot move user funds
unilaterally.
An emergency-withdraw path gives the owner unilateral access with
a 7-day on-chain timelock.CTFExchange
CTFExchange
Matches and settles signed orders. Charges the PS-QUADRATIC-V1
fee curve
fee = k × P × (1−P) per Fees
on both legs of a fill (maker and taker, each at its order’s
feeRateBps). Admin-gated for pauseTrading / unpauseTrading /
pauseMarket / pauseUser / registerToken / setVaultFactory;
operator-gated for batchMatchOrders (the platform’s settlement
call), matchOrders, fillOrder / fillOrders and
cancelOrders.ConditionalTokens
ConditionalTokens
Gnosis-based ERC-1155 position tokens.
prepareCondition
registers a new market condition; position IDs are deterministic
from (collateral, conditionId, indexSet). Oracle-service calls
prepareCondition as part of market deployment.NegRiskAdapter + NegRiskCtfExchange
NegRiskAdapter + NegRiskCtfExchange
Separate deployment path for categorical markets: groups of 2 or
more mutually exclusive outcomes (
negRiskEligible: true). Reuses the same EIP-712 order shape
as the binary exchange; differences are in the position-token
derivation and convertPositions for NO-basket → collateral
conversions.Oracle + NegRiskOracle
Oracle + NegRiskOracle
Oracle resolves binary markets; neg-risk markets use a separate
NegRiskOracle (negRiskOracle in GET /api/platform/contracts).
On both, only oracle-service’s proposer key (or the owner) can call
proposeOutcome; resolveChallenge*, voidMarket and
markDelayed are owner-only; finalizeOutcome is permissionless
once the challenge window has passed without a challenge. Enforces
a 10 USDC challenger bond and a 2-hour default challenge window (a
market can carry its own window); emergency proposal clear is gated
by a 7-day admin timelock. See
Settlement flow.The hot path in one paragraph
A partner callsPOST /api/orders/place with an EIP-712-signed order. core-api
verifies the X-Api-Key header, runs compliance guards, and forwards to
exchange-service. exchange-service rebuilds the on-chain Order struct
(signer = the acting wallet,
maker = that wallet’s vault, signatureType: 1 = VAULT, the only
type the exchange accepts) and checks that the EIP-712 signature
recovers to the acting wallet (bad_signature otherwise). It then
locks the collateral the order needs (USDC for a BUY, the outcome
tokens being sold for a SELL; available → locked), persists the
order row, and calls matching-core
over gRPC. matching-core runs the book, returns fills synchronously.
exchange-service writes the trade rows + fee ledger in a single PG
transaction, publishes events to NATS, and returns the response.
No on-chain transaction happens here. Trades are picked up from
the trades table by match-submitter, which batches them into
batchMatchOrders calls against CTFExchange (or the neg-risk
exchange), signed by an operator EOA from its wallet pool.
chain-watcher sees the resulting OrderFilled / MatchFailed
events and notifies exchange-service to finalize the trade state.
Strict vs optimistic settlement
PredictStreet uses a strict settlement model: a trade row is persisted immediately after match, but the user’s balance is only decremented fromlocked after on-chain confirmation from
chain-watcher. The upside is simpler revert semantics (a reverted chain
tx simply refunds locked → available) and no need for an unsettled
balance bucket. The tradeoff is a small settlement delay between
match and the balance update being visible as “available” again.
What partners own vs what PredictStreet owns
The rule of thumb: anything that moves money is on-chain with a user
signature. Everything else is off-chain for speed.
Next
Authentication
X-Api-Key format, scope taxonomy, and rotation.Order lifecycle
State machine and transition rules.
Contracts reference
Deployed addresses, ABIs, invariants.
Environments
Testnet, staging, mainnet endpoints.