Skip to main content
PredictStreet is built around a deliberate split: the hot path is off-chain for sub-second trading latency, and custody and settlement are on-chain so nothing moves without a user’s EIP-712 signature against a dedicated vault contract they control.

System diagram

Off-chain services

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.
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.
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.
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.
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.
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.
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

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.
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.
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.
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 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 calls POST /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 from locked 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.