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

> ## Agent Instructions
> To integrate AGG, start with Quickstart: REST (https://docs.agg.market/quickstart/rest), then Order lifecycle & statuses (https://docs.agg.market/concepts/order-lifecycle).
> Track every trade until it reaches a terminal status. Before retrying a failed or timed-out call, read Errors, retries & idempotency (https://docs.agg.market/concepts/errors).
> The API reference is generated from https://docs.agg.market/openapi/openapi.json.

# Quickstart: trading bot / agent

> One headless TypeScript process that signs in with its own key, quotes, fills, and tracks a trade

## Decide in 30 seconds

| You are building | Who signs in | Headers on trading calls | Start here |
| - | - | - | - |
| A bot or agent trading its own funds | The bot, once, with its own wallet key (SIWE) | `x-app-id` and `Authorization: Bearer <accessToken>` | This page |
| A platform for many users | Each user, on your client | `x-app-id` and that user's `Authorization: Bearer` token. Your backend adds `x-app-api-key` only for server features. | [Quickstart: REST](/quickstart/rest), [Authentication & sessions](/concepts/authentication) |
| A trading terminal UI | Each user, in the browser | `x-app-id` and the user's token, sent by the SDK. Never ship `x-app-api-key` to a browser. | [Quickstart: React](/quickstart/react) |

Which call needs which header: [Base URL, app IDs & API keys](/environments#headers).

A bot is one user. It signs a SIWE message with its own private key, gets a user access token, and
trades from that user's managed balance. No browser, no API key.

<Info>
  **You'll need:**

  * An app ID from the admin dashboard (`https://admin.agg.market`), under **API**.
  * One allowed origin on that app (**Domains**), for example `https://bot.example.com`. It never
    has to serve a page. Wallet sign-in names it, and an app with none cannot complete sign-in.
  * Node 18 or later, `@agg-build/sdk`, and `viem`.
  * A private key for the bot. Keep it out of source control.

  **Result:** a filled order, polled until `terminal` is `true`, then the bot's positions.
</Info>

The steps below run top to bottom as one file. The same code as a single script is in
[Full script](#full-script). It trades in [paper mode](#paper-mode) unless you set `AGG_LIVE=1`.

## 0. Set up

<CodeGroup>
  ```ts SDK theme={null}
  // Add these imports (the full script below has them):
  // import { createAggClient, isAggApiError, MarketStatus } from "@agg-build/sdk";
  // import { privateKeyToAccount } from "viem/accounts";
  const ORIGIN = "https://bot.example.com"; // one of your app's allowed origins
  const MODE = process.env.AGG_LIVE === "1" ? "live" : "paper";

  const client = createAggClient({
    baseUrl: "https://api.agg.market",
    appId: process.env.AGG_APP_ID ?? "your-app-id",
    persistSession: false, // no browser storage
  });
  const account = privateKeyToAccount(process.env.BOT_PRIVATE_KEY as `0x${string}`);
  ```

  ```bash cURL theme={null}
  npm install @agg-build/sdk viem

  export API=https://api.agg.market
  export APP_ID=your-app-id
  export ORIGIN=https://bot.example.com
  export DOMAIN=${ORIGIN#*://}
  export PK=0x...                                           # the bot's private key
  export ADDRESS=$(cast wallet address --private-key $PK)   # Foundry's cast
  ```
</CodeGroup>

## 1. Sign in with the bot's key

Get a nonce, build the SIWE message, sign it locally, and verify it. `verify()` keeps the access
and refresh tokens in the client and sends them from then on.

<CodeGroup>
  ```ts SDK theme={null}
  async function signIn() {
    const start = await client.authStart({ provider: "siwe" });
    if (start.type !== "nonce") throw new Error(`Expected a nonce, got ${start.type}`);
    const message = client.buildSiweMessage({
      address: account.address,
      chainId: 1,
      nonce: start.nonce,
      statement: start.statement, // set only when your app configures one
      domain: new URL(ORIGIN).host,
      uri: ORIGIN,
    });
    const signature = await account.signMessage({ message });
    return client.verify({ message, signature });
  }
  const { user } = await signIn();
  console.log("signed in as", user.id);
  ```

  ```bash cURL theme={null}
  NONCE=$(curl -s -X POST $API/auth/start -H "x-app-id: $APP_ID" \
    -H "content-type: application/json" -d '{ "provider": "siwe" }' | jq -r .nonce)
  MESSAGE=$(printf '%s wants you to sign in with your Ethereum account:\n%s\n\n\nURI: %s\nVersion: 1\nChain ID: 1\nNonce: %s\nIssued At: %s' \
    "$DOMAIN" "$ADDRESS" "$ORIGIN" "$NONCE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)")
  SIGNATURE=$(cast wallet sign --private-key $PK "$MESSAGE")
  AUTH=$(curl -s -X POST $API/auth/verify -H "x-app-id: $APP_ID" -H "content-type: application/json" \
    -d "$(jq -n --arg m "$MESSAGE" --arg s "$SIGNATURE" '{message: $m, signature: $s}')")
  export ACCESS_TOKEN=$(echo "$AUTH" | jq -r .accessToken)
  export REFRESH_TOKEN=$(echo "$AUTH" | jq -r .refreshToken)
  ```
</CodeGroup>

[Start authentication](/api-reference/authentication/start-authentication) ·
[Verify sign-in](/api-reference/authentication/verify-sign-in)

### Keeping the session alive

* The SDK keeps tokens in memory only. A restarted process must call `signIn()` again. Each
  sign-in uses a fresh nonce.
* The access token lasts 15 minutes and the refresh token 7 days. When a call returns `401`, the
  SDK refreshes once with the refresh token, stores the new pair, and retries the call. You write
  no refresh code.
* If that refresh fails (the refresh token expired or was revoked), the SDK clears the session and
  throws the `401`. Call `signIn()` again.
* With cURL, refresh yourself: `POST /auth/token/refresh` with `{ "refreshToken": "..." }` and
  `x-app-id`, then use the new pair. See
  [Refresh access token](/api-reference/authentication/refresh-access-token).
* In Node, a client holds the process open. Call `client.destroy()` when the bot is done.

## 2. Fund the bot

Live mode only. In paper mode, skip to the balance check: the paper account starts with a
simulated `PAPER_USD` balance.

<CodeGroup>
  ```ts SDK theme={null}
  if (MODE === "live") {
    let deposit = await client.getDepositAddresses();
    while (!deposit.ready) {
      await new Promise((r) => setTimeout(r, 2000));
      deposit = await client.getDepositAddresses();
    }
    console.log("send USDC to", deposit.evmAddress, "on", deposit.supportedChains);
  }
  const balances = await client.getManagedBalances({ mode: MODE });
  console.log(balances.cash.map((c) => [c.tokenSymbol, c.availableRaw, c.decimals]));
  ```

  ```bash cURL theme={null}
  # 202 { "ready": false } while addresses are created. Repeat until 200.
  curl -s $API/execution/deposit-addresses -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN"
  curl -s "$API/execution/balances?mode=paper" -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" \
    | jq '.cash[] | {tokenSymbol, availableRaw, decimals}'
  ```
</CodeGroup>

[Get deposit addresses](/api-reference/funding/get-deposit-addresses) ·
[Get balances](/api-reference/portfolio/get-balances) · [Funding](/concepts/funding)

`availableRaw` is an integer string in the token's decimals: `"5000000"` with 6 decimals is 5 USDC.

## 3. Find an outcome

You trade an **outcome**, not a market. Take a `venueMarketOutcomes[].id`.

<CodeGroup>
  ```ts SDK theme={null}
  const markets = await client.getVenueMarkets({ status: MarketStatus.open, search: "bitcoin", limit: 5 });
  const outcomeId = markets.data[0]?.venueMarketOutcomes?.[0]?.id;
  if (!outcomeId) throw new Error("No open outcome found; try another search");
  ```

  ```bash cURL theme={null}
  curl -s "$API/venue-markets?status=open&search=bitcoin&limit=5" -H "x-app-id: $APP_ID" \
    | jq '.data[] | {question, venue, outcomes: [.venueMarketOutcomes[] | {id, label}]}'
  export OUTCOME_ID=...
  ```
</CodeGroup>

[List venue markets](/api-reference/markets/list-venue-markets) · [Markets, outcomes & matching](/concepts/markets)

## 4. Quote

The bot's token makes the quote executable. Fill only when `status` is `"ok"`.

<CodeGroup>
  ```ts SDK theme={null}
  const quote = await client.getSmartRoute({
    venueMarketOutcomeId: outcomeId,
    tradeSide: "buy",
    maxSpend: 5,
    mode: MODE,
  });
  if (quote.status !== "ok") throw new Error(`${quote.status}: ${quote.error ?? ""}`);
  ```

  ```bash cURL theme={null}
  QUOTE=$(curl -s -G "$API/orderbook/$OUTCOME_ID/route" \
    -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" \
    --data-urlencode "side=buy" --data-urlencode "maxSpend=5" --data-urlencode "mode=paper")
  echo "$QUOTE" | jq '{quoteId, status, error, expiresAt, totalCostIncFees}'
  export QUOTE_ID=$(echo "$QUOTE" | jq -r .quoteId)
  ```
</CodeGroup>

[Get a quote (smart route)](/api-reference/trading/get-a-quote-smart-route) · [Quotes & smart routing](/concepts/quotes)

A quote expires about 45 seconds after it is created. Fill right away.

## 5. Fill

<CodeGroup>
  ```ts SDK theme={null}
  const fill = await client.executeManaged({ quoteId: quote.quoteId, mode: MODE });
  ```

  ```bash cURL theme={null}
  curl -s -X POST $API/execution/fill \
    -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" -H "content-type: application/json" \
    -d "{ \"quoteId\": \"$QUOTE_ID\", \"mode\": \"paper\" }"
  ```
</CodeGroup>

[Execute quote](/api-reference/trading/execute-quote)

The response means accepted, not done. If the call times out, do not re-quote: resend the same
`quoteId` or poll status. Resending a `quoteId` never trades twice. See
[Errors, retries & idempotency](/concepts/errors#idempotency-and-safe-retries).

## 6. Poll to a final state

<CodeGroup>
  ```ts SDK theme={null}
  let status = await client.getExecutionStatus({ quoteId: fill.quoteId, mode: MODE });
  while (!status.terminal) {
    await new Promise((r) => setTimeout(r, status.pollAfterMs ?? 1000));
    status = await client.getExecutionStatus({ quoteId: fill.quoteId, mode: MODE });
  }
  console.log(status.overallState, status.errorReason ?? "");
  ```

  ```bash cURL theme={null}
  until [ "$(curl -s -G $API/execution/status -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" \
    --data-urlencode "quoteId=$QUOTE_ID" --data-urlencode "mode=paper" | tee /dev/stderr | jq -r .terminal)" = "true" ]; do
    sleep 1
  done
  ```
</CodeGroup>

[Get execution status](/api-reference/trading/get-execution-status) · [Order lifecycle & statuses](/concepts/order-lifecycle)

`overallState` ends as `filled`, `partially_filled`, `failed`, `cancelled`, or `expired`.

## 7. Read positions

<CodeGroup>
  ```ts SDK theme={null}
  const positions = await client.getExecutionPositions({ mode: MODE });
  for (const p of positions.data) {
    console.log(p.venueMarket.question, p.venueMarket.venueMarketOutcomes.map((o) => [o.label, o.totalSize]));
  }
  client.destroy();
  ```

  ```bash cURL theme={null}
  curl -s "$API/execution/positions?mode=paper" -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" | jq
  ```
</CodeGroup>

[Get user positions](/api-reference/portfolio/get-user-positions). When a market settles, claim
winnings with [Claim winnings](/recipes/claim-winnings).

## Paper mode

`mode: "paper"` on the quote, the fill, the status call, and the balance and position reads.
Paper fills price against live books but never reach a venue, so no deposit is needed. A paper
quote must be filled in paper mode. See [Paper trading](/recipes/paper-trading).

To trade live, set `AGG_LIVE=1`, fund the deposit address from step 2, and drop `mode=paper` from
the cURL calls.

## Failures a bot hits

| Response | What to do |
| - | - |
| `401` with `code: "unregistered_domain"` on verify | `ORIGIN` is not one of the app's allowed origins. Add it under **Domains**. |
| `401` `Invalid SIWE signature` | The message changed after signing, or the nonce was reused. Run `signIn()` again. |
| `401` on any other call | The SDK already tried one refresh. Run `signIn()` again. |
| `/auth/start` returns `type: "challenge_required"` | Bot protection is on and the app is past its user threshold. A headless bot cannot solve it. See [Bot protection](/recipes/bot-protection). |
| `401` that names an API key | The app has **Require API key** on. Pass `apiKey` to `createAggClient` from a secret store. |
| Quote `status` not `"ok"` | `insufficient_balance`: fund or lower `maxSpend`. `min_order_size_violated`: raise `maxSpend`. `insufficient_depth`, `no_orderbooks`: pick another outcome. |
| `400` `quote_not_found` or `quote_expired` on fill | Poll status for the same `quoteId` first. Re-quote only after the steps in [Errors](/concepts/errors#idempotency-and-safe-retries). |
| `400` `quote_already_executed` | Already filled. Poll status. |
| `403` testing limit | The app reached its testing caps (20 users, 100 trades). See [Testing mode](/environments#testing-mode). |
| `403` region | Trading is not available in the bot's country. See [Venue geo availability](/recipes/venue-geo-availability). |
| `429` | Wait `Retry-After` seconds. The SDK error carries it as `retryAfterMs`. |

The SDK throws `AggApiError` with `status`, `code`, and `retryAfterMs`; check it with
`isAggApiError(err)`. Every code: [Errors, retries & idempotency](/concepts/errors).

## Full script

Save as `bot.ts` and run with `AGG_APP_ID=... BOT_PRIVATE_KEY=0x... npx tsx bot.ts`.

```ts SDK theme={null}
import { createAggClient, isAggApiError, MarketStatus } from "@agg-build/sdk";
import { privateKeyToAccount } from "viem/accounts";

async function main() {
  const ORIGIN = "https://bot.example.com"; // one of your app's allowed origins
  const MODE = process.env.AGG_LIVE === "1" ? "live" : "paper";
  const client = createAggClient({
    baseUrl: "https://api.agg.market",
    appId: process.env.AGG_APP_ID ?? "your-app-id",
    persistSession: false,
  });
  const account = privateKeyToAccount(process.env.BOT_PRIVATE_KEY as `0x${string}`);
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

  try {
    // 1. Sign in with the bot's key
    const start = await client.authStart({ provider: "siwe" });
    if (start.type !== "nonce") throw new Error(`Expected a nonce, got ${start.type}`);
    const message = client.buildSiweMessage({
      address: account.address,
      chainId: 1,
      nonce: start.nonce,
      statement: start.statement,
      domain: new URL(ORIGIN).host,
      uri: ORIGIN,
    });
    await client.verify({ message, signature: await account.signMessage({ message }) });

    // 2. Fund (live only) and check the balance
    if (MODE === "live") {
      let deposit = await client.getDepositAddresses();
      while (!deposit.ready) {
        await sleep(2000);
        deposit = await client.getDepositAddresses();
      }
      console.log("send USDC to", deposit.evmAddress, "on", deposit.supportedChains);
    }
    const balances = await client.getManagedBalances({ mode: MODE });
    console.log(balances.cash.map((c) => [c.tokenSymbol, c.availableRaw, c.decimals]));

    // 3. Find an outcome
    const markets = await client.getVenueMarkets({ status: MarketStatus.open, search: "bitcoin", limit: 5 });
    const outcomeId = markets.data[0]?.venueMarketOutcomes?.[0]?.id;
    if (!outcomeId) throw new Error("No open outcome found; try another search");

    // 4. Quote
    const quote = await client.getSmartRoute({
      venueMarketOutcomeId: outcomeId,
      tradeSide: "buy",
      maxSpend: 5,
      mode: MODE,
    });
    if (quote.status !== "ok") throw new Error(`${quote.status}: ${quote.error ?? ""}`);

    // 5. Fill
    const fill = await client.executeManaged({ quoteId: quote.quoteId, mode: MODE });

    // 6. Poll to a final state
    let status = await client.getExecutionStatus({ quoteId: fill.quoteId, mode: MODE });
    while (!status.terminal) {
      await sleep(status.pollAfterMs ?? 1000);
      status = await client.getExecutionStatus({ quoteId: fill.quoteId, mode: MODE });
    }
    console.log(status.overallState, status.errorReason ?? "");

    // 7. Positions
    const positions = await client.getExecutionPositions({ mode: MODE });
    for (const p of positions.data) {
      console.log(p.venueMarket.question, p.venueMarket.venueMarketOutcomes.map((o) => [o.label, o.totalSize]));
    }
  } catch (err) {
    if (isAggApiError(err)) console.error(err.status, err.code, err.message);
    throw err;
  } finally {
    client.destroy(); // lets Node exit
  }
}

main().catch(() => process.exit(1));
```

## Next

<CardGroup cols={2}>
  <Card title="Direct orders" href="/recipes/direct-order-execution">
    One call per trade with your own idempotent `externalId`. Live only.
  </Card>

  <Card title="Limit orders" href="/recipes/limit-orders">
    Rest an order at your price on one venue.
  </Card>

  <Card title="Order book stream" href="/recipes/websocket-orderbook">
    Live books over WebSocket instead of polling.
  </Card>

  <Card title="Self-custody" href="/recipes/self-custody">
    Keep funds in the bot's own wallet; the bot signs each step.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.