> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> End-to-end testnet onboarding for integrators: mint USDC → deploy vault → deposit → place LIMIT/MARKET orders → split for SELL. Copy-paste TypeScript that takes your associatedWallet from zero to its first fill using a partner API key.

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 live testnet — paste it in, swap your
API key + private key, run.

<Note>
  **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](mailto:partners@predictstreet.com))
  with the four items in
  [API keys → Getting a key](/auth/api-keys#getting-a-key) and you'll
  receive a testnet `ps_live_…` key plus the IP-allowlist + KYC
  prerequisites for `orders:write` and `vault:write` scopes.
</Note>

## Prerequisites

| What                                        | Where                                                                                                         |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Node ≥ 20 with `ethers@6` installed         | `npm i ethers`                                                                                                |
| **PredictStreet API key**                   | Issued by ops — see [API keys](/auth/api-keys). Format: `ps_live_<keyId>_<secret>`                            |
| Private key of the key's `associatedWallet` | Stored in your secrets manager. The wallet must have completed KYC tier 1 before write-scope keys are issued. |
| Testnet RPC                                 | `https://rpc.ab.testnet.adifoundation.ai/`                                                                    |
| API base URL                                | `https://core.api.dev.predictstreet.sde.adifoundation.ai`                                                     |

```typescript theme={null}
import {
  Wallet, JsonRpcProvider, Contract, MaxUint256,
  parseUnits, parseEther, randomBytes,
} from 'ethers';

const RPC  = 'https://rpc.ab.testnet.adifoundation.ai/';
const BASE = 'https://core.api.dev.predictstreet.sde.adifoundation.ai';

// 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 testnet addresses (chainId 99999)
const VAULT_FACTORY = '0x298F4D974a0715a8e9085EE3f48a14A421bCF5B6';
const USDC          = '0x979696A1B62d4c0F0390124447c065798ee4c70c';
const CTF_EXCHANGE          = '0x4074c225b296E1E556c565B0C3Ddba305E63E7c4'; // binary markets
const CTF_EXCHANGE_NEG_RISK = '0x2eB97912c333963a21410Af1eF7E9a0aAB7631bf'; // 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. Mint testnet USDC to your EOA

`MockCollateral` exposes a public `mint(address, uint256)` on testnet —
mint as much as you want. **Note:** USDC needs to land in your EOA
first; the vault deposit moves it from EOA → vault in a later step.

```typescript theme={null}
const usdc = new Contract(
  USDC,
  [
    'function mint(address,uint256)',
    'function balanceOf(address) view returns (uint256)',
    'function approve(address,uint256) returns (bool)',
  ],
  eoa,
);

await (await usdc.mint(eoa.address, parseUnits('1000', 6))).wait();
console.log('USDC balance:', (await usdc.balanceOf(eoa.address)).toString());
```

You also need a small ADI balance for gas. On testnet grab it from
the public [faucet](https://faucet.ab.testnet.adifoundation.ai/) —
\~5 ADI to your EOA covers deploy + deposit + a handful of trades.

## 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`:

```typescript theme={null}
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);
```

<Tip>
  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).
</Tip>

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

## 3. Approve + deposit USDC into the vault

```typescript theme={null}
// 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](/concepts/deposits/overview) for
the full lifecycle and [Deposit limits](/concepts/deposits/limits) for
the daily / weekly / monthly caps applied to your vault.

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

## 4. Place orders

### EIP-712 helper

Every order is signed against the on-chain `CTFExchange` `Order`
struct (11 fields). See [EIP-712 signing](/concepts/trading/eip712-signing)
for the full type table. Helper:

<Warning>
  **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:

  ```typescript theme={null}
  const verifyingContract = market.negRiskEligible
    ? CTF_EXCHANGE_NEG_RISK
    : CTF_EXCHANGE;
  ```

  The `Order` struct itself is identical between the two — only the
  domain `verifyingContract` differs.
</Warning>

```typescript theme={null}
const DOMAIN = {
  name: 'PredictStreet', version: '1',
  chainId: 99999, 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)

```typescript theme={null}
// 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)

```typescript theme={null}
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: [...] }
```

<Warning>
  **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:

  ```ts theme={null}
  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);
  }
  ```
</Warning>

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

```typescript theme={null}
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

```typescript theme={null}
// 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.

```typescript theme={null}
// 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: 99999, 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'`:

```typescript theme={null}
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:

| `type`            | `timeInForce`   | Behaviour                                                                                               |
| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------- |
| `limit` (default) | `gtc` (default) | Rests in the book until filled, expired, or cancelled.                                                  |
| `limit`           | `ioc`           | Fill what's available at price-or-better right now; cancel any unfilled remainder.                      |
| `limit`           | `fok`           | Either fill the **entire** quantity at price-or-better immediately, or cancel without any partial fill. |
| `market`          | `ioc`           | Take the best resting prices up to your `price` slippage cap; cancel any unfilled remainder.            |
| `market`          | `fok`           | All-or-nothing market — fill the entire quantity within the slippage cap, or cancel.                    |
| `market`          | `gtc`           | **Rejected** 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

```typescript theme={null}
// 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

| Symptom                                                                                 | Cause                                                                                                              | Fix                                                                                                                        |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/me/vault` keeps returning `deployed: false` past the typical 15-second window | KYC not yet approved (single\_wallet) or sub-account not yet onboarded (multi\_wallet)                             | See [Backend auto-deploy → What if auto-deploy doesn't fire](/concepts/vaults/auto-deploy#what-if-auto-deploy-doesnt-fire) |
| `depositERC20` reverts with selector `0x87138d5c`                                       | `NotInitialized()` — 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 detail                                 | An unknown field in the body — `forbidNonWhitelisted`                                                              | Only send the whitelisted set above (no extra fields)                                                                      |
| `400 bad_signature` on order placement                                                  | `maker` ≠ `VaultFactory.vaultOf(signer)` on chain                                                                  | Send your **vault** address, not the EOA                                                                                   |
| `code: invalid_tif`                                                                     | `type: market` with `timeInForce: gtc`                                                                             | Use `ioc` or `fok` for MARKET                                                                                              |
| `code: insufficient_position` on SELL                                                   | The outcome token isn't in your vault                                                                              | Run step 6 (split) first                                                                                                   |
| `code: insufficient_funds` on BUY                                                       | Not enough USDC `available` in vault                                                                               | Top up via step 3                                                                                                          |
| `code: market_not_open`                                                                 | Market is `PROPOSED` / `PAUSED` / `RESOLVED`                                                                       | Pick a market with `status: OPEN`                                                                                          |
| `MatchFailed(reason: 0xc56873ba)` on settlement                                         | `OrderExpired()` — your `expiration` slipped between match and on-chain submit                                     | Sign with `expiration: 0`                                                                                                  |

## Where to go next

<CardGroup cols={2}>
  <Card title="EIP-712 signing reference" icon="signature" href="/concepts/trading/eip712-signing">
    Full struct + Python / Go / Rust signing examples.
  </Card>

  <Card title="Order lifecycle" icon="timeline" href="/concepts/trading/order-lifecycle">
    PENDING → OPEN → PARTIAL → FILLED state machine + on-chain settlement.
  </Card>

  <Card title="Vault contract reference" icon="file-code" href="/concepts/contracts/vaults">
    Full ABI, dual-sig flow, emergency withdraw, digest invalidation.
  </Card>

  <Card title="Time-in-force semantics" icon="stopwatch" href="/concepts/trading/time-in-force">
    GTC / IOC / FOK behaviour in detail.
  </Card>
</CardGroup>
