Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.adipredictstreet.com/llms.txt

Use this file to discover all available pages before exploring further.

This page takes the private key of your key’s associatedWallet from nothing to fully trading in five steps. Every code block is real ethers v6 against the production environment — paste it in, swap your API key + private key, run.
Don’t have an API key yet? Keys are issued by PredictStreet ops — they are never self-service. Email your integration manager (or partners@predictstreet.com) with the four items in API keys → Getting a key and you’ll receive a production ps_live_… key plus the IP-allowlist + KYC prerequisites for orders:write and vault:write scopes.

Prerequisites

WhatWhere
Node ≥ 20 with ethers@6 installednpm i ethers
PredictStreet API keyIssued by ops — see API keys. Format: ps_live_<keyId>_<secret>
Private key of the key’s associatedWalletStored in your secrets manager. The wallet must have completed KYC tier 1 before write-scope keys are issued.
Production RPChttps://rpc.adifoundation.ai/
API base URLhttps://core-api.adipredictstreet.com
import {
  Wallet, JsonRpcProvider, Contract, MaxUint256,
  parseUnits, parseEther, randomBytes,
} from 'ethers';

const RPC  = 'https://rpc.adifoundation.ai/';
const BASE = 'https://core-api.adipredictstreet.com';

// Load both secrets from your secrets manager.
const API_KEY = process.env.PS_API_KEY!;            // ps_live_<keyId>_<secret>
const eoaPk   = process.env.PS_ASSOCIATED_WALLET_PK!;

// Canonical production addresses (chainId 36900)
const VAULT_FACTORY = '0x6Ed1E5787e4667bEf8485cbfC624DFF6FF561BbA';
const USDC          = '0x9cb8142aEBBcdc60AF7c97Af897A67A8f3CA71C2';
const CTF_EXCHANGE          = '0x3b32619897ae40C79b7086a0EB3F985077e7Fed7'; // binary markets
const CTF_EXCHANGE_NEG_RISK = '0x65A068b3C1C3088B1B23499A6104045f2b661B3e'; // 3+-outcome (neg-risk) markets

const provider = new JsonRpcProvider(RPC);
const eoa      = new Wallet(eoaPk, provider);
console.log('associatedWallet:', eoa.address);

// Tiny helper — every authenticated call ships the key header.
const auth = { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' };

1. Fund your EOA with USDC and ADI gas

Your EOA needs two things before the deposit step:
  • USDC in the EOA balance. Source it via a bridge / off-chain ramp per your partner runbook. The platform does not mint USDC — it is a real on-chain asset.
  • ADI for gas. ~5 ADI covers vault deposit + a handful of trades.
const usdc = new Contract(
  USDC,
  [
    'function balanceOf(address) view returns (uint256)',
    'function approve(address,uint256) returns (bool)',
  ],
  eoa,
);
console.log('USDC balance:', (await usdc.balanceOf(eoa.address)).toString());

2. Get your vault address

The platform deploys your vault automatically the moment your associated wallet clears KYC tier 1 — you do not call VaultFactory.deployVault yourself, and you do not pay the deploy gas. KYC clearance is a hard prerequisite for any orders:write / vault:write key, so by the time you have a working API key, your vault is either deployed or about to be. Poll GET /api/me/vault until deployed: true:
async function waitForVault(timeoutMs = 60_000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const r = await fetch(`${BASE}/api/me/vault`, { headers: auth });
    const me = await r.json();
    // deployStatus is one of: not_started | pending | building |
    //   submitted | confirmed | skipped_existing | failed_permanent
    // See /concepts/vaults/auto-deploy#deploystatus-values for
    // semantics. `deployed: true` means VaultFactory.vaultOf(eoa)
    // returns a non-zero address on chain.
    if (me.deployed) return me.vaultAddress;
    await new Promise(r => setTimeout(r, 1500));
  }
  throw new Error('vault not auto-deployed within 60s — check KYC status');
}

const vault = await waitForVault();
console.log('your vault:', vault);
Save vault. It’s the address you’ll send as maker in every signed order, and the verifyingContract in vault-side EIP-712 domains (splits / withdrawals).
Vault deploy isn’t currently surfaced on the typed WS channels — poll GET /api/me/vault until deployed: true like the snippet above. Auto-deploy normally completes within a few seconds.

3. Approve + deposit USDC into the vault

// One-time MaxUint256 approval is the standard pattern — saves gas
// on every future deposit. Tighten if your security model needs it.
await (await usdc.approve(vault, MaxUint256, { gasLimit: 100_000n })).wait();

const v = new Contract(
  vault,
  ['function depositERC20(address token, uint256 amount)'],
  eoa,
);
await (await v.depositERC20(
  USDC,
  parseUnits('500', 6),               // 500 USDC into the vault
  { gasLimit: 600_000n },
)).wait();
After ~1 block the platform reflects the deposit and your /api/me/balances will show available = 500 USDC. The on-chain approve step stays on your side regardless of auto-deploy — ERC-20 transferability is owner-only by design. See Deposits for the full lifecycle and Deposit limits for the daily / weekly / monthly caps applied to your vault.
Per-EOA deposit limits are initialised by the platform automatically alongside the vault deploy. By the time GET /api/me/vault returns deployed: true, the limits are already registered and the deposit call succeeds.

4. Place orders

EIP-712 helper

Every order is signed against the on-chain CTFExchange Order struct (11 fields). See EIP-712 signing for the full type table. Helper:
Binary vs neg-risk domain. The verifyingContract you sign against depends on the market: 2-outcome binary markets settle on CTFExchange, 3+-outcome (neg-risk) markets settle on PredictStreetNegRiskCtfExchange. The market response carries negRiskEligible: boolean — branch on it before signing or you’ll get bad_signature rejections at order placement:
const verifyingContract = market.negRiskEligible
  ? CTF_EXCHANGE_NEG_RISK
  : CTF_EXCHANGE;
The Order struct itself is identical between the two — only the domain verifyingContract differs.
const DOMAIN = {
  name: 'PredictStreet', version: '1',
  chainId: 36900, verifyingContract: CTF_EXCHANGE,
};
const TYPES = { Order: [
  { name: 'salt',          type: 'uint256' },
  { name: 'maker',         type: 'address' },
  { name: 'signer',        type: 'address' },
  { name: 'taker',         type: 'address' },
  { name: 'tokenId',       type: 'uint256' },
  { name: 'makerAmount',   type: 'uint256' },
  { name: 'takerAmount',   type: 'uint256' },
  { name: 'expiration',    type: 'uint256' },
  { name: 'feeRateBps',    type: 'uint256' },
  { name: 'side',          type: 'uint8'   },
  { name: 'signatureType', type: 'uint8'   },
]};

async function signOrder(opts: {
  side: 'buy' | 'sell',
  price: string,                 // e.g. "0.42" — decimal USDC
  qty: string,                   // e.g. "10"   — decimal outcome qty
  tokenId: string,               // from market.yesTokenId / market.noTokenId
  feeTakerBps: number,           // from market.feeTakerBps — see below
}) {
  const priceWei    = parseUnits(opts.price, 6);
  const qtyWei      = parseUnits(opts.qty,   6);
  const notionalWei = (priceWei * qtyWei) / 1_000_000n;
  const salt        = BigInt('0x' + Buffer.from(randomBytes(32)).toString('hex'));

  const struct = {
    salt,
    maker:        vault,                                // your vault
    signer:       eoa.address,                          // your EOA
    taker:        '0x0000000000000000000000000000000000000000',
    tokenId:      BigInt(opts.tokenId),
    makerAmount:  opts.side === 'buy' ? notionalWei : qtyWei,
    takerAmount:  opts.side === 'buy' ? qtyWei      : notionalWei,
    expiration:   0n,                                   // 0 = no on-chain expiry (recommended)
    // CRITICAL: must equal `market.feeTakerBps` returned by
    // GET /api/markets/{slug} (root market lookup is slug-keyed
    // since 2026-05-16; sub-resources still take symbol). The
    // platform reconstructs the canonical Order with the live
    // market fee and recovers the signer against THAT digest —
    // signing with 0n produces a hash mismatch and `POST
    // /api/orders/place` rejects every order with `bad_signature`.
    // See `Pull a real tokenId` below for where this comes from.
    feeRateBps:   BigInt(opts.feeTakerBps),
    side:         opts.side === 'buy' ? 0 : 1,
    signatureType: 1,                                   // VAULT
  };
  const signature = await eoa.signTypedData(DOMAIN, TYPES, struct);
  return { struct, signature };
}

Pull a real tokenId (and the per-market fee)

// Step 1 — discover the slug from the list endpoint. The `slug`
// field is the canonical, URL-safe identifier required by the root
// /api/markets/{slug} lookup (breaking change 2026-05-16; sub-
// resource endpoints like `/orderbook` still accept `{symbol}`).
const list = await fetch(`${BASE}/api/markets?status=OPEN`).then(r => r.json());
const target = list.markets.find(m => m.symbol === 'NC26-BIN-83479265');
const slug = target.slug;

// Step 2 — fetch market detail by slug.
const market = await fetch(
  `${BASE}/api/markets/${encodeURIComponent(slug)}`,
).then(r => r.json());

const yesTokenId  = market.yesTokenId;    // outcome '0'
const noTokenId   = market.noTokenId;     // outcome '1'
const feeTakerBps = market.feeTakerBps;   // CRITICAL — pass into signOrder

LIMIT BUY (rests in the book)

const { struct, signature } = await signOrder({
  side: 'buy', price: '0.45', qty: '10', tokenId: yesTokenId, feeTakerBps,
});

const resp = await fetch(`${BASE}/api/orders/place`, {
  method: 'POST',
  headers: auth,
  body: JSON.stringify({
    marketId:      'NC26-BIN-83479265',
    side:          'buy',
    outcome:       '0',                      // YES
    price:         '0.45',
    quantity:      '10',
    nonce:         struct.salt.toString(),
    expiry:        0,                        // = struct.expiration
    maker:         vault,                    // = struct.maker
    signature,
    type:          'limit',                  // (default — can be omitted)
    timeInForce:   'gtc',                    // (default for limit — can be omitted)
    clientOrderId: `bot-${Date.now()}`,
  }),
}).then(r => r.json());

console.log(resp);
// { orderId, status: 'OPEN' | 'FILLED' | 'PARTIAL' | 'REJECTED',
//   filledQty, remainingQty, trades: [...] }
HTTP 201 does not mean “order accepted by the matcher.” The endpoint returns 201 Created as soon as core-api validates the request envelope and forwards it — even when the matcher later rejects it. Always branch on the body’s status field, not the HTTP code:
if (resp.status === 'REJECTED') {
  // resp.code is the reason — e.g. 'bad_signature',
  // 'market_not_open', 'insufficient_balance'. Fix and retry.
  console.error('rejected:', resp.code, resp.message);
}

MARKET BUY (best-effort, IOC)

MARKET requires timeInForce: ioc or fok. The price field still travels (it acts as a slippage cap — you only fill at price-or-better).
const { struct, signature } = await signOrder({
  side: 'buy', price: '0.99', qty: '5', tokenId: yesTokenId, feeTakerBps,
});

const resp = await fetch(`${BASE}/api/orders/place`, {
  method: 'POST',
  headers: auth,
  body: JSON.stringify({
    marketId: 'NC26-BIN-83479265', side: 'buy', outcome: '0',
    price: '0.99', quantity: '5',
    nonce: struct.salt.toString(), expiry: 0, maker: vault, signature,
    type: 'market',
    timeInForce: 'ioc',                      // or 'fok' for all-or-nothing
  }),
}).then(r => r.json());

Cancelling

// Single
await fetch(`${BASE}/api/orders/cancel`, {
  method: 'POST',
  headers: auth,
  body:    JSON.stringify({ orderId: resp.orderId }),
});

// Everything you have open
await fetch(`${BASE}/api/orders/cancel-all`, {
  method:  'POST',
  headers: auth,
  body:    '{}',
});

5. Split USDC → YES + NO so you can SELL

A SELL order must reference outcome ERC-1155 tokens that already sit in your vault. You get them by depositing USDC and then calling vault.splitPosition(...) with a backend co-signature. The backend returns the co-sig from POST /api/vault/split-signature; you sign the same struct yourself, then submit on-chain.
// 1. Get the backend co-signature
const sig = await fetch(`${BASE}/api/vault/split-signature`, {
  method:  'POST',
  headers: auth,
  body:    JSON.stringify({ marketId: 'NC26-BIN-83479265', amount: '50' }),
}).then(r => r.json());

// 2. Sign the same SplitPosition struct yourself
const SPLIT_TYPES = { SplitPosition: [
  { name: 'kind',            type: 'uint8'    },
  { name: 'collateralToken', type: 'address'  },
  { name: 'conditionId',     type: 'bytes32'  },
  { name: 'partition',       type: 'uint256[]' },
  { name: 'amount',          type: 'uint256'  },
  { name: 'salt',            type: 'uint256'  },
  { name: 'deadline',        type: 'uint256'  },
]};

const split = {
  kind:            sig.kind,
  collateralToken: sig.collateralToken,
  conditionId:     sig.conditionId,
  partition:       sig.partition.map(BigInt),
  amount:          BigInt(sig.amount),
  salt:            BigInt(sig.salt),
  deadline:        BigInt(sig.deadline),
};
const ownerSig = await eoa.signTypedData(
  { name: 'PredictStreetVault', version: '1', chainId: 36900, verifyingContract: vault },
  SPLIT_TYPES,
  split,
);

// 3. Submit on-chain
const vaultC = new Contract(
  vault,
  ['function splitPosition(uint8,address,bytes32,uint256[],uint256,uint256,uint256,bytes,bytes)'],
  eoa,
);
await (await vaultC.splitPosition(
  split.kind, split.collateralToken, split.conditionId,
  split.partition, split.amount, split.salt, split.deadline,
  ownerSig, sig.backendSig,
  { gasLimit: 1_500_000n },
)).wait();
After this confirms the vault holds 50 YES + 50 NO ERC-1155. Now SELL of either side works exactly like BUY but with side: 'sell':
const { struct, signature } = await signOrder({
  side: 'sell', price: '0.55', qty: '20', tokenId: yesTokenId, feeTakerBps,
});

await fetch(`${BASE}/api/orders/place`, {
  method: 'POST',
  headers: auth,
  body: JSON.stringify({
    marketId: 'NC26-BIN-83479265', side: 'sell', outcome: '0',
    price: '0.55', quantity: '20',
    nonce: struct.salt.toString(), expiry: 0, maker: vault, signature,
  }),
});

Order types — what you can ask for

The platform supports the following combinations end-to-end:
typetimeInForceBehaviour
limit (default)gtc (default)Rests in the book until filled, expired, or cancelled.
limitiocFill what’s available at price-or-better right now; cancel any unfilled remainder.
limitfokEither fill the entire quantity at price-or-better immediately, or cancel without any partial fill.
marketiocTake the best resting prices up to your price slippage cap; cancel any unfilled remainder.
marketfokAll-or-nothing market — fill the entire quantity within the slippage cap, or cancel.
marketgtcRejected with code: invalid_tif — MARKET orders cannot rest.
price is always required, even for MARKET — it acts as the slippage cap. The matcher will only consume liquidity at price or better. expiry (= EIP-712 expiration) defaults to 0 (no on-chain expiry). A non-zero value is fine for off-chain matching but races async on-chain settlement; if the deadline passes between match and submit, the on-chain leg reverts with OrderExpired(). Recommend keeping 0 unless you specifically need a hard TTL well above worst-case settlement latency.

Verifying state after each step

// On-chain
const usdcInVault = await usdc.balanceOf(vault);

// Off-chain (the platform mirrors on-chain state with ~1-block lag)
const balances = await fetch(`${BASE}/api/me/balances`, {
  headers: auth,
}).then(r => r.json());

const positions = await fetch(`${BASE}/api/me/positions`, {
  headers: auth,
}).then(r => r.json());

const open = await fetch(`${BASE}/api/orders/open`, {
  headers: auth,
}).then(r => r.json());

Common pitfalls

SymptomCauseFix
GET /api/me/vault keeps returning deployed: false past the typical 15-second windowKYC not yet approved (single_wallet) or sub-account not yet onboarded (multi_wallet)See Backend auto-deploy → What if auto-deploy doesn’t fire
depositERC20 reverts with selector 0x87138d5cNotInitialized() — your code raced ahead of the auto-deploy job (deploy-tx mined but the cap-init tx hasn’t yet)Wait one block and retry; or rely on GET /api/me/vault returning deployed: true before depositing
400 bad_request on /api/orders/place with no detailAn unknown field in the body — forbidNonWhitelistedOnly send the whitelisted set above (no extra fields)
400 bad_signature on order placementmakerVaultFactory.vaultOf(signer) on chainSend your vault address, not the EOA
code: invalid_tiftype: market with timeInForce: gtcUse ioc or fok for MARKET
code: insufficient_position on SELLThe outcome token isn’t in your vaultRun step 6 (split) first
code: insufficient_funds on BUYNot enough USDC available in vaultTop up via step 3
code: market_not_openMarket is PROPOSED / PAUSED / RESOLVEDPick a market with status: OPEN
MatchFailed(reason: 0xc56873ba) on settlementOrderExpired() — your expiration slipped between match and on-chain submitSign with expiration: 0

Where to go next

EIP-712 signing reference

Full struct + Python / Go / Rust signing examples.

Order lifecycle

PENDING → OPEN → PARTIAL → FILLED state machine + on-chain settlement.

Vault contract reference

Full ABI, dual-sig flow, emergency withdraw, digest invalidation.

Time-in-force semantics

GTC / IOC / FOK behaviour in detail.