Try Testnet
This page talks to the live TxFlow testnet straight from your browser. It generates a throwaway key, claims testnet USDC, enables trading, and places real orders — every write is signed locally with the same EIP-712 + MessagePack flow described in Signing.
Two examples
One terminal per instrument type — perpetuals and outcome. Each is the same three pieces: a ticker, a chart, and an order ticket. Only the outcome terminal differs in how it renders them: its price is a probability, so it plots a probability curve and its ticket picks YES or NO instead of buy or sell.
Both share one throwaway account. The button below runs the whole setup in one click:
| Step | Endpoint / action | Reference |
|---|---|---|
| Generate a key | ethers.Wallet.createRandom(), in your browser | Signing |
| Claim USDC | POST /request-fund on the HTTP API host (see below) → credited to the perp account | Key Concepts |
| Enable trading | approveAgent — the main key authorizes an agent key to sign orders | Signing |
| Fund the sub-accounts | tokenSend — perps and outcome each settle on their own sub-account, and the faucet only credits the perp one | below |
The faucet is rate-limited per address, so claim once and trade both.
// every HTTP call sends the version header; /info replies are { success, code, data }
const API = 'https://rpc.txflow-testnet.xyz'
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
}
// move USDC between your own sub-accounts — "perp" | "outcome".
// Signed directly by the main key; an agent key cannot move funds.
const value = {
txflowNetwork: 'TxFlow-Testnet',
amount: '300',
fromAccountType: 'perp',
toAccountType: 'outcome',
token: 'USDC:0xd04f0d19E8FbA49cfA541E272F9C60a2ad720464', // NAME:address, from the token list
nonce: Date.now(),
}
const types = { TransferToken: [
{ name: 'txflowNetwork', type: 'string' }, { name: 'amount', type: 'string' },
{ name: 'fromAccountType', type: 'string' }, { name: 'toAccountType', type: 'string' },
{ name: 'token', type: 'string' }, { name: 'nonce', type: 'uint64' } ] }
const sig = ethers.Signature.from(await main.signTypedData(domain, types, value))
await post('/exchange', {
action: { type: 'tokenSend', chainId: 2511, apiVersion: 1, toAddress: null, ...value },
signature: { r: sig.r, s: sig.s, v: sig.v }, nonce: value.nonce, vaultAddress: null,
})Setting toAddress sends to another user instead of between your own accounts; that variant signs the SendToken struct rather than TransferToken. The first send to a new address may charge a one-time 1 USDC activation fee (0 when the destination is yourself). See tokenSend.
Every terminal calls the same endpoints, changing only the instrument id:
| Piece | Call |
|---|---|
| Ticker | /api-ws ticker subscription |
| Candles | /info candleSnapshot for 90 days of history, /api-ws candle for today's open bar — on an outcome market the closes are probabilities, so the terminal draws them as a curve |
| Order book | l2Book — an outcome market passes its instrument_id as coin |
| Outcome card quotes | /ws outcomeBbos, up to 50 instruments per subscription |
| Order | order → POST /exchange, read back with frontendOpenOrders |
| Cancel | cancelByCloid → POST /exchange |
// the only thing that differs between the three examples
const coin = String(instrumentId) // 1 = BTC-USDC, 1284355 = an outcome market
// 90 daily candles; the open one keeps streaming on the `candle` channel
const history = await post('/info', {
type: 'candleSnapshot',
req: { coin, interval: '1d', startTime: Date.now() - 90 * 86_400_000, endTime: Date.now() },
})
ws.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'ticker', coin, instrumentId } }))
ws.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'candle', coin, interval: '1d' } }))
// order book — levels[0] is bids, levels[1] is asks, each best-first
const { levels } = await post('/info', { type: 'l2Book', coin })
// signOrder(agent, action) wraps the action in the signed envelope — see Signing.
// agent-signed order. `c` is a client order id — tag every order with one so
// you can cancel it by cloid.
await post('/exchange', await signOrder(agent, {
type: 'order',
grouping: 'na',
orders: [{ a: instrumentId, b: true, p: '75000.0', s: '0.0006', r: false,
t: { limit: { tif: 'Gtc' } }, c: 'my-order-001' }],
}))
await post('/exchange', await signOrder(agent, {
type: 'cancelByCloid',
cancels: [{ asset: instrumentId, cloid: 'my-order-001' }],
}))Perpetuals
Perpetual instruments are named BASE-QUOTE and listed by perpMeta; the index field is the a you sign. Margin comes from the perp sub-account. See TIP 2: Perpetuals.
candleSnapshot history, candle stream liveOutcome
Outcome markets are prediction markets: two sides priced so that price(YES) + price(NO) = 1. They trade through the same order book and the same order action, keyed by instrumentId. See TIP 3: Outcome Markets.
Discovery is different: markets are grouped under events and scoped to a channel, so instead of perpMeta you list them with outcomeMeta. Testnet publishes them on channel 1.
That channel belongs to Probly, the first partner to run a channel on TxFlow. The questions in the terminal below, their tick sizes and settlement, and the resting orders you trade against are all Probly's — TxFlow provides the matching, margin, and settlement, the channel operator provides the markets and the liquidity. Anything you list with exchangeId: 1 is their venue; exchangeId: 0 is the default platform channel.
const { events } = await post('/info', { type: 'outcomeMeta', exchangeId: 1, limit: 50 })
const tradable = events
.flatMap((e) => e.markets)
.filter((m) => m.trading.accepting_orders && !m.trading.closed)
// → { instrument_id: 1284355, question: 'Will Hyperliquid perform an airdrop…',
// instrument: { price_tick: '0.001', sz_decimals: 2 }, outcome_prices: ['0.295', '0.705'] }Most of these have an active book but no fills yet, so ticker returns an empty lastPrice and candleSnapshot returns []. The terminal falls back to the book mid for the headline price.
There is one book per market and it is priced in YES, between 0 and 1 — that price is the market's probability. So the terminal below quotes both sides of it rather than a raw price:
| In the terminal | On the wire |
|---|---|
Buy YES at p | b: true, p |
Buy NO at q | b: false, q |
| The NO book | the same l2Book, mirrored: YES asks become NO bids at 1 − px |
| The chart | the close of each candleSnapshot bucket as a probability on a fixed 0–100% axis, not an OHLC candle |
// buy 10 NO shares at 0.93
const yes = false
const price = '0.930'
await post('/exchange', await signOrder(agent, {
type: 'order',
grouping: 'na',
orders: [{ a: instrumentId, b: yes, p: price, s: '10',
r: false, t: { limit: { tif: 'Gtc' } }, c: 'my-outcome-001' }],
}))Two more things differ. Margin comes from the outcome sub-account, funded by
the tokenSend above. And the exchange can
acknowledge an Outcome order asynchronously: if the queued order fails, /api-ws
outcomeOrderUpdate pushes the reason under
the returned traceId (successes are not pushed). Use /ws
outcomeOrderUpdates for the matching
lifecycle, and confirm the resting state with
outcomeFrontendOpenOrders.
candleSnapshot, candle stream liveTestnet endpoints
Everything above talks to https://rpc.txflow-testnet.xyz: HTTP at /info and /exchange, WebSocket at wss://…/ws and wss://…/api-ws, and the faucet at POST /request-fund with { "address": "0x…" }. The domain values are under Networks.
To build your own client, start with the TypeScript SDK quickstart. For a direct protocol integration, use the Info API, WebSocket API, and Signing reference.