Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Outcome Integration Guide

Every Outcome endpoint has its own reference page. This guide connects them in the order an integration actually calls them, and flags the three places where the Outcome flow differs from perps.

The short version of those three differences:

  • Discovery metadata comes from a gateway aggregation in snake_case, while the on-chain instrument definition is camelCase. They describe the same market and are not interchangeable.
  • Placing an order is asynchronous. A successful HTTP response means queued, not matched, and the terminal result arrives on a different WebSocket endpoint than the matching result.
  • A position query returns only positions. Account balances live behind a separate call.

Examples call the API directly. Each step is one POST /info, POST /exchange, or WebSocket subscription, built from the same pieces as Try Testnet:

const API = 'https://rpc.txflow-testnet.xyz'
 
// every HTTP call sends the version header; /info replies are { success, code, data }
async function post(path: string, body: unknown) {
  const res = await fetch(API + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-version': '2026-08-16' },
    body: JSON.stringify(body),
  }).then((r) => r.json())
  return path === '/info' ? res.data : res
}
 
const ws = new WebSocket('wss://rpc.txflow-testnet.xyz/ws')        // node channels
const apiWs = new WebSocket('wss://rpc.txflow-testnet.xyz/api-ws') // API-service channels
const subscribe = (socket: WebSocket, subscription: object) =>
  socket.send(JSON.stringify({ method: 'subscribe', subscription }))
 
// signAgent(action) and signAccount(action) wrap an action in the signed
// { action, nonce, signature, vaultAddress } envelope — agent key or account
// key, as described in Signing.

The flow at a glance

Fund the Outcome sub-account, place the order, then follow two channels on two endpoints.

A successful order response can mean queued, not matched. Subscribe to both channels in step 4, or a rejected queue entry looks like an order that never filled.

  1. POST /info
    outcomeMeta

    Find the event and market.

    instrument_id — every later step needs it

  2. POST /exchangeAccount-signed
    tokenSend

    Move USDC from perp to outcome. Account-signed; an agent cannot move funds.

  3. POST /exchangeAgent-signed
    order

    a is the instrument_id; b/r encode the leg (buy NO = b: false, r: false) and p is that leg's price. TIF Gtc, Gtd, or Alo — Ioc is rejected.

    Possibly an async acceptance with a traceId

  4. Do all of these
    WebSocket
    outcomeOrderUpdate

    On /api-ws. Pushed only if the queued order fails; match on traceId.

    WebSocket
    outcomeOrderUpdates

    On /ws. What the book did: filled, cancelled, rejected.

  5. POST /info
    outcomePositions

    Positions only — no margin summary. Use clearinghouseState with accountType=outcome for that.

The sections below take each step in turn, with code.

1. Discover markets

outcomeMeta is the discovery feed: events with nested markets, live trading gates, pricing, volume, and settlement state.

const page = await post('/info', {
  type: 'outcomeMeta',
  exchangeId: 1,
  limit: 20,
  sortBy: 'volume',
})

Two response shapes come out of this one query. An eventSlug point lookup returns the event object directly; everything else returns a paged envelope. Narrow before reading events:

const events = 'events' in page ? page.events : [page]
const market = events[0]?.markets[0]

Page size caps at 50; walk with next_cursor for more. sortBy: 'endingSoon' is an ascending semantic order that carries its own direction, so send it without sortOrder.

Resolving a deep link works the other way round — look the event up by the market slug in your URL:

const result = await post('/info', {
  type: 'outcomeMeta',
  exchangeId: 1,
  marketSlugs: ['btc-up-or-down-5m'],
  markets: 'matched',
})

markets: 'matched' returns only the requested markets; 'event' returns every market in each owning event.

market.instrument_id is the identifier every later step needs.

2. Stream quotes

For a grid or watchlist, outcomeBbos batches best bid and ask across many markets on one subscription:

subscribe(ws, { type: 'outcomeBbos', instrumentIds: [market.instrument_id], exchangeId: 1 })
 
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  if (msg.channel !== 'outcomeBbos') return
  for (const { instrumentId, bbo } of msg.data.bbos) {
    const [bid, ask] = bbo
    console.log(instrumentId, bid, ask)
  }
}

One subscription carries 1–50 instrument ids and one connection accepts up to 20 active groups. Beyond that, open more subscriptions on the same connection. Either side of bbo is null when the book is empty there.

For a single market's full depth, the shared l2Book channel applies. Outcome markets are addressed by instrument id as a decimal string, not the OUTCOME-<id> name:

subscribe(ws, { type: 'l2Book', coin: String(market.instrument_id) })
// each `l2Book` push carries data.levels: [bids, asks], each best-first

The same string rule applies to trades and activeAssetCtx. To follow several markets' tapes at once, use outcomeTrades, which takes numeric instrumentIds instead.

For charts, outcomeProbability returns the historical YES probability series. To keep the chart live, subscribe to candle on /api-ws with the instrument id as coin and append its closes, which are YES probabilities.

3. Fund the sub-account

Outcome trades settle in a dedicated sub-account. Move USDC into it with an owner-signed tokenSend. With no toAddress it signs the TransferToken struct:

await post('/exchange', await signAccount({
  type: 'tokenSend',
  txflowNetwork: 'TxFlow-Testnet',
  chainId: 2511,
  apiVersion: 1,
  toAddress: null,
  token: 'USDC:0xd04f0d19E8FbA49cfA541E272F9C60a2ad720464',
  amount: '100',
  nonce: Date.now(),
  fromAccountType: 'perp',
  toAccountType: 'outcome',
}))

4. Place an order, and learn what happened

Orders use the shared order action with the instrument id as a. This buys 10 YES shares at 0.55:

const ack = await post('/exchange', await signAgent({
  type: 'order',
  grouping: 'na',
  orders: [{ a: market.instrument_id, b: true, p: '0.55', s: '10',
             r: false, t: { limit: { tif: 'Gtc' } } }],
}))
// ack can be an asynchronous acceptance: { status: 'ok', traceId, … }
ChannelEndpointAnswers
outcomeOrderUpdate (singular)/api-wsDid the queued order fail before reaching the node? Pushed only on failure.
outcomeOrderUpdates (plural)/wsWhat did the book do — filled, cancelled, rejected?
// Delay-queue result on /api-ws, correlated by traceId.
subscribe(apiWs, { type: 'outcomeOrderUpdate', user })
apiWs.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  if (msg.channel !== 'outcomeOrderUpdate') return
  const { traceId, done, success, body } = msg.data
  if (done && !success) console.error('never reached the node', traceId, body?.response)
}
 
// Matching-engine lifecycle on /ws.
subscribe(ws, { type: 'outcomeOrderUpdates', user })
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  if (msg.channel !== 'outcomeOrderUpdates') return
  for (const { order, status } of msg.data) console.log(order.oid, order.outcome, status)
}

Watching only the plural channel makes a rejected queue entry look like an order that silently never filled. The singular channel stays silent when the order goes through, so it never tells you whether it traded.

order.outcome is the displayed leg (Yes or No); order.side (B or A) and reduceOnly are the wire b and r (see Outcome orders).

Cancelling uses the shared cancel action.

5. Read positions and balances

This is the second place the Outcome flow surprises people: outcomePositions returns only the assetPositions array. There is no margin summary and no withdrawable.

const [positions, account] = await Promise.all([
  post('/info', { type: 'outcomePositions', user }),
  post('/info', { type: 'clearinghouseState', user, accountType: 'outcome', exchangeId: 1 }),
])
NeedCall
Open positionsoutcomePositions
Account value, margin, and withdrawable balanceclearinghouseState with accountType: 'outcome'
Active ordersoutcomeFrontendOpenOrders

The YES and NO legs of one market are two separate records; the sign of position.szi distinguishes them.

One asymmetry to keep in mind: outcomePositions accepts an optional exchangeId, outcomeFrontendOpenOrders does not.

History endpoints need an explicit opt-in. Add 'outcome' to accountType on userFills and historicalOrders, or to accountTypes on userNonFundingLedgerUpdates — note the plural field name and the singular 'perp' spelling on that last one.

6. Split and merge complete sets

outcomeOperate converts between collateral and a complete share set, without touching the book:

// Lock collateral, receive symmetric YES and NO positions.
await post('/exchange', await signAgent({
  type: 'outcomeOperate',
  split: [{ a: market.instrument_id, s: '10' }],
}))
 
// Return a symmetric pair, release the collateral.
await post('/exchange', await signAgent({
  type: 'outcomeOperate',
  merge: [{ a: market.instrument_id, s: '10' }],
}))

Exactly one of split or merge is present per action. This is useful for market making one leg while hedging the other, and for exiting when one side has no bid.

7. Settlement

Settlement state travels on the market object from step 1:

const settlement = market.settlement
 
if (settlement?.resolution_status === 'resolved') {
  console.log('winner:', settlement.winning_outcome)
}

resolution_status is prepared or resolved. winning_outcome is derived from outcomes and settlement_fraction after resolution — it is the protocol's result, not the metadata provider's oracle_outcome. Poll outcomeMeta for the market slug to refresh it.

Recurring series

Crypto up/down markets repeat on a fixed cadence. outcomeSeriesTimeline returns the past, current, and upcoming windows around an anchor event so you can build a period navigator, and cryptoPrice / cryptoPriceHistory supply the price-to-beat and the chart series.

Unlike outcomeMeta, the timeline response is camelCase.

What is out of scope

  • Creating markets. outcomeDeploy is permissioned — see Exchange API.
  • Risk-notice acknowledgement. The first-party app records this through application-session endpoints that are not part of this API surface, as noted in TIP 3. Present disclosures appropriate to your own product and jurisdiction.

Next steps