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

# Building an agent

> A concrete, end-to-end walkthrough for an autonomous OCX trading agent: authenticate with an API key, pull market data, size an order with preview, submit it, monitor fills over the stream, and enforce risk.

This walkthrough builds a minimal but complete perpetuals agent. It authenticates
with an API key, reads the market, sizes an order safely with a preview, submits
it, listens for fills on the real-time stream, and holds a risk kill switch. Every
endpoint used here is public and verified — nothing is invented.

<Info>
  All traffic goes through the single OCX API gateway at one HTTPS base URL,
  written below as `{API_BASE}`. Prices and quantities are JSON **strings**
  (fixed-decimal) — keep them as strings end to end to preserve precision.
</Info>

## Step 1 — Provision an API key

A human owner signs in once with SIWE, then mints a scoped key for the agent. See
[Authentication](/get-started/authentication) for the SIWE flow; the key step is:

<CodeGroup>
  ```bash cURL theme={null}
  curl -s -X POST "$API_BASE/perps/me/api-keys" \
    -b cookies.txt \
    -H 'Content-Type: application/json' \
    -d '{"name":"demo-agent","scope":"trade","allowedIps":["203.0.113.7"],"expiresInDays":90}'
  # → { "id":"01H…", "secret":"ocx_sk_…", "message":"Copy this secret now — it will not be shown again." }
  ```

  ```js JavaScript theme={null}
  const res = await fetch(`${API_BASE}/perps/me/api-keys`, {
    method: "POST",
    credentials: "include", // session cookie from SIWE sign-in
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: "demo-agent",
      scope: "trade",
      allowedIps: ["203.0.113.7"],
      expiresInDays: 90,
    }),
  });
  const { secret } = await res.json(); // shown once — store in a secret manager
  ```
</CodeGroup>

<Warning>
  Store `secret` in a secret manager immediately — it is never shown again.
  From here on, the agent sends `x-api-key: <secret>` on every request and never
  needs the wallet.
</Warning>

## Step 2 — Read the market

Market data needs no auth. Discover instruments, then read the book and stats for
the market you want to trade.

<CodeGroup>
  ```bash cURL theme={null}
  # List perp/futures markets
  curl -s "$API_BASE/perps/markets"

  # Order book snapshot
  curl -s "$API_BASE/perps/orderbook/BTC-PERP?depth=25"

  # 24h stats (mark, funding context, open interest)
  curl -s "$API_BASE/perps/market-stats?marketId=BTC-PERP"

  # Fee tiers (query live rather than hardcoding)
  curl -s "$API_BASE/perps/fees"
  ```

  ```js JavaScript theme={null}
  const H = { "x-api-key": secret }; // optional on public reads, harmless to send

  const markets = await (await fetch(`${API_BASE}/perps/markets`, { headers: H })).json();
  const book    = await (await fetch(`${API_BASE}/perps/orderbook/BTC-PERP?depth=25`, { headers: H })).json();
  const stats   = await (await fetch(`${API_BASE}/perps/market-stats?marketId=BTC-PERP`, { headers: H })).json();

  // A trivial "signal": mid price from top of book
  const bestBid = Number(book.bids[0][0]);
  const bestAsk = Number(book.asks[0][0]);
  const mid = (bestBid + bestAsk) / 2;
  ```
</CodeGroup>

<Tip>
  For anything more than a one-shot decision, subscribe to the live stream in
  Step 5 instead of polling these endpoints in a loop — it is lower latency and
  lighter on your rate-limit budget.
</Tip>

## Step 3 — Size the order with a preview

Always dry-run before you commit. `preview` returns the notional, estimated fee,
your fee role and rolling-volume tier, and — critically — whether the order would
pass the server-side margin gate. It has no side effects.

<CodeGroup>
  ```bash cURL theme={null}
  curl -s -X POST "$API_BASE/perps/orders/preview" \
    -H "x-api-key: $OCX_KEY" \
    -H 'Content-Type: application/json' \
    -d '{"marketId":"BTC-PERP","side":"buy","type":"limit","price":"64000","quantity":"0.25"}'
  ```

  ```js JavaScript theme={null}
  const preview = await (await fetch(`${API_BASE}/perps/orders/preview`, {
    method: "POST",
    headers: { ...H, "Content-Type": "application/json" },
    body: JSON.stringify({
      marketId: "BTC-PERP", side: "buy", type: "limit", price: "64000", quantity: "0.25",
    }),
  })).json();

  if (!preview.approved) {
    // e.g. reason: "INSUFFICIENT_MARGIN" — resize or fund, do not blindly retry
    throw new Error(`preview rejected: ${preview.reason}`);
  }
  ```
</CodeGroup>

<ResponseField name="approved" type="boolean">
  Whether the order would pass the margin gate. If `false`, read `reason` and
  resize or fund — do not retry the same order.
</ResponseField>

<ResponseField name="marginRequired" type="string">
  Collateral the order would lock. Compare against your free balance.
</ResponseField>

<ResponseField name="estFee" type="string">
  Estimated fee for this fill, with `feeBps` and `feeRole` (`maker`/`taker`).
</ResponseField>

<ResponseField name="notional" type="string">
  Order notional in quote currency.
</ResponseField>

## Step 4 — Submit the order

With an approved preview, place the order. Set a `clientOrderId` so a network
retry can never create a duplicate.

<CodeGroup>
  ```bash cURL theme={null}
  curl -s -X POST "$API_BASE/perps/orders" \
    -H "x-api-key: $OCX_KEY" \
    -H 'Content-Type: application/json' \
    -d '{
      "marketId":"BTC-PERP","side":"buy","type":"limit",
      "price":"64000","quantity":"0.25","timeInForce":"gtc",
      "postOnly":false,"reduceOnly":false,"marginMode":"cross",
      "clientOrderId":"demo-agent-0001"
    }'
  ```

  ```js JavaScript theme={null}
  const order = await (await fetch(`${API_BASE}/perps/orders`, {
    method: "POST",
    headers: { ...H, "Content-Type": "application/json" },
    body: JSON.stringify({
      marketId: "BTC-PERP", side: "buy", type: "limit",
      price: "64000", quantity: "0.25", timeInForce: "gtc",
      marginMode: "cross", clientOrderId: "demo-agent-0001",
    }),
  })).json();
  ```
</CodeGroup>

<Note>
  For multi-order strategies (grids, liquidity ladders), prefer
  `POST /perps/quotes/bulk` with `cancelAll: true` — it atomically replaces your
  whole ladder in one call and is fully API-key enabled. See the
  [Market maker quoting guide](/market-makers/quoting).
</Note>

Order controls you'll reach for:

<AccordionGroup>
  <Accordion title="Time in force">
    `gtc` rests until filled or cancelled; `ioc` fills what it can immediately then
    cancels the rest; `fok` fills fully or cancels entirely.
  </Accordion>

  <Accordion title="postOnly / reduceOnly">
    `postOnly` rejects an order that would take liquidity (keeps you on the maker
    side). `reduceOnly` prevents an order from ever increasing your position — use
    it for exits.
  </Accordion>

  <Accordion title="Protective orders">
    Attach `stopPrice`, `takeProfitPrice`, or `stopLossPrice` for automated
    conditional exits.
  </Accordion>
</AccordionGroup>

## Step 5 — Monitor fills over the stream

Open one Server-Sent Events connection per market. On connect you receive a
`snapshot` (order book, recent trades, and **your** positions); after that,
`delta` events push incremental order-book changes, new trades, and position
upserts as they happen. A fill shows up as a trade plus a position delta — that's
your signal to rebalance.

<CodeGroup>
  ```bash cURL theme={null}
  curl -N "$API_BASE/perps/stream/events?marketId=BTC-PERP" \
    -H "x-api-key: $OCX_KEY"
  ```

  ```js JavaScript theme={null}
  const es = new EventSource(
    `${API_BASE}/perps/stream/events?marketId=BTC-PERP`,
    { withCredentials: true }, // or send the key via a header in a Node SSE client
  );

  es.addEventListener("snapshot", (e) => {
    const { payload } = JSON.parse(e.data);
    initState(payload); // orderbook + your trades + your positions
  });

  es.addEventListener("delta", (e) => {
    const { payload } = JSON.parse(e.data);
    applyDelta(payload); // changed levels, new fills, position upserts/removedIds
    // On a new fill: update inventory, then decide whether to re-quote or de-risk
  });

  es.addEventListener("error", (e) => {
    // reconnect and re-snapshot; detect gaps via the envelope eventSeq
  });
  ```
</CodeGroup>

<Warning>
  Streams diff server-side and can drop on network blips. A well-behaved agent
  applies the initial `snapshot`, folds in each `delta`, watches the envelope
  `eventSeq` for gaps, and **reconnects (re-snapshotting)** on disconnect or on a
  sequence gap.
</Warning>

Between stream events, reconcile with the pull endpoints when you need certainty:

```bash theme={null}
curl -s "$API_BASE/perps/positions"    -H "x-api-key: $OCX_KEY"
curl -s "$API_BASE/perps/orders/open"  -H "x-api-key: $OCX_KEY"
curl -s "$API_BASE/perps/trades"       -H "x-api-key: $OCX_KEY"
curl -s "$API_BASE/perps/balances"     -H "x-api-key: $OCX_KEY"
```

## Step 6 — Manage risk and shut down cleanly

Risk is enforced server-side, but your agent still owns its own limits. Track
margin headroom, de-risk proactively, and always have a kill path.

<CodeGroup>
  ```bash cURL theme={null}
  # Flatten a position
  curl -s -X POST "$API_BASE/perps/positions/close" \
    -H "x-api-key: $OCX_KEY" \
    -H 'Content-Type: application/json' \
    -d '{"marketId":"BTC-PERP"}'   # omit quantity to close fully

  # Emergency kill switch — pull all orders (optionally scoped)
  curl -s -X POST "$API_BASE/perps/orders/cancel-all" \
    -H "x-api-key: $OCX_KEY" \
    -H 'Content-Type: application/json' \
    -d '{}'
  ```

  ```js JavaScript theme={null}
  async function killSwitch() {
    await fetch(`${API_BASE}/perps/orders/cancel-all`, {
      method: "POST",
      headers: { ...H, "Content-Type": "application/json" },
      body: JSON.stringify({}), // omit filters to cancel everything
    });
  }

  // Wire killSwitch() to: feed loss, repeated errors, margin breach, or shutdown.
  ```
</CodeGroup>

<Tip>
  Handle `INSUFFICIENT_MARGIN` rejections by resizing or funding, not by
  retrying. Back off on `429` and respect the `X-RateLimit-*` headers. Rotate the
  API key on your `expiresInDays` schedule and revoke old keys with
  `DELETE /perps/me/api-keys/{id}`.
</Tip>

## The full loop

<Steps>
  <Step title="Provision">
    Owner signs in with SIWE, mints an IP-pinned, expiring `trade` key. Agent
    loads it from a secret store.
  </Step>

  <Step title="Cache & subscribe">
    Cache the market list and fee tiers; open one SSE stream per market.
  </Step>

  <Step title="Decide & size">
    Compute a signal, then `preview` to confirm margin, fee, and approval.
  </Step>

  <Step title="Execute">
    Place via `/perps/orders` (or `/perps/quotes/bulk` with `cancelAll` for
    ladders), always with a `clientOrderId`.
  </Step>

  <Step title="React">
    Fold in fill and position deltas from the stream; adjust inventory and skew.
  </Step>

  <Step title="Guard">
    Enforce your own margin limits; `cancel-all` or `positions/close` on breach,
    disconnect, or shutdown.
  </Step>
</Steps>

<Card title="Back to AI agents overview" icon="robot" href="/ai-agents/overview">
  Revisit the concepts: the agent loop, scopes, and server-side risk.
</Card>
