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

> From zero to a filled order with curl: sign in, fund, find a market, quote, fill, and track

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

  * An app ID. Sign in to the admin dashboard (`https://admin.agg.market`); your first sign-in
    creates an app. Copy its ID from **API**. See [Base URL, app IDs & API keys](/environments).
  * One allowed origin on that app, for example `https://app.example.com` (**Domains**). A new app
    has none, and wallet sign-in is refused until you add one. The sign-in message must name it.
  * `curl`, `jq`, and Foundry's `cast` (to create a test wallet and sign), or Node 18+ with
    `@agg-build/sdk` and `viem` for the SDK tabs.
  * A few dollars of USDC on a supported chain, or use [paper mode](#test-without-real-funds).

  **Result:** a filled order, polled until `terminal` is `true`.
</Info>

<Warning>
  This guide places a real order on a live venue and spends real funds. Use a small amount, or add
  `mode=paper` to trade without funds. Venues are restricted by the caller's country; see
  [Venue geo availability](/recipes/venue-geo-availability).
</Warning>

## 0. Set up

<CodeGroup>
  ```bash cURL theme={null}
  export API=https://api.agg.market
  export APP_ID=your-app-id
  export ORIGIN=https://app.example.com   # one of your app's allowed origins
  export DOMAIN=${ORIGIN#*://}            # app.example.com

  # A throwaway test wallet. Keep the key out of source control.
  cast wallet new
  export PK=0x...                         # private key from the output
  export ADDRESS=$(cast wallet address --private-key $PK)   # EIP-55 checksummed
  ```

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

  const client = createAggClient({
    baseUrl: "https://api.agg.market",
    appId: "your-app-id",
  });
  const ORIGIN = "https://app.example.com"; // one of your app's allowed origins
  const account = privateKeyToAccount(process.env.PK as `0x${string}` ?? generatePrivateKey());
  ```
</CodeGroup>

## 1. Sign the user in

Get a nonce, sign an EIP-4361 (SIWE) message with the wallet, and exchange it for tokens.

<CodeGroup>
  ```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)

  ISSUED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  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" "$ISSUED_AT")
  SIGNATURE=$(cast wallet sign --private-key $PK "$MESSAGE")

  export ACCESS_TOKEN=$(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}')" \
    | jq -r .accessToken)
  ```

  ```ts SDK theme={null}
  const { nonce } = await client.authStart({ provider: "siwe" });
  const message = client.buildSiweMessage({
    address: account.address,
    chainId: 1,
    nonce,
    domain: new URL(ORIGIN).host,
    uri: ORIGIN,
  });
  const signature = await account.signMessage({ message });
  await client.verify({ message, signature }); // the client now sends the token for you
  ```
</CodeGroup>

`POST /auth/verify` returns `{ accessToken, refreshToken, user }`. Every call below sends
`x-app-id` and `Authorization: Bearer $ACCESS_TOKEN`.

The message format must be exact: a blank statement slot means two empty lines between the address
and `URI:`. If sign-in fails:

| Response | Fix |
| - | - |
| `401` with `code: "unregistered_domain"` | `DOMAIN` is not one of the app's allowed origins. |
| `401` `Invalid SIWE signature` | The message text changed between signing and sending, the address is not checksummed, or the nonce was reused. Start again from `/auth/start`. |
| `/auth/start` returns `"type": "challenge_required"` | Bot protection is on for the app. See [Bot protection](/recipes/bot-protection). |
| `403` testing limit | The app reached 20 users in testing mode. |

Access tokens are short-lived. If a later call returns `401`, refresh with
`POST /auth/token/refresh` (see [Authentication & sessions](/concepts/authentication)).

## 2. Get a deposit address and fund it

<CodeGroup>
  ```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" | jq
  ```

  ```ts SDK theme={null}
  let deposit = await client.getDepositAddresses();
  while (!deposit.ready) {
    await new Promise((r) => setTimeout(r, 2000));
    deposit = await client.getDepositAddresses();
  }
  console.log(deposit.evmAddress, deposit.supportedChains);
  ```
</CodeGroup>

Send USDC to `evmAddress` on one of the chains in `supportedChains` (or to `svmAddress` on
Solana). Then check the balance until `availableRaw` is above zero:

<CodeGroup>
  ```bash cURL theme={null}
  curl -s $API/execution/balances \
    -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" \
    | jq '.cash[] | {tokenSymbol, availableRaw, decimals}'
  ```

  ```ts SDK theme={null}
  const balances = await client.getManagedBalances();
  console.log(balances.cash.map((c) => [c.tokenSymbol, c.availableRaw, c.decimals]));
  ```
</CodeGroup>

`availableRaw` is an integer string in the token's decimals: `"5000000"` with 6 decimals is 5 USDC.
More on deposits and withdrawals: [Funding & withdrawals](/concepts/funding).

## 3. Find a market

List open markets and take an outcome id. You trade an **outcome**, not a market.

<CodeGroup>
  ```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=...   # a venueMarketOutcomes[].id, for example the "Yes" outcome
  ```

  ```ts SDK theme={null}
  const markets = await client.getVenueMarkets({ status: "open", search: "bitcoin", limit: 5 });
  const outcomeId = markets.data[0]!.venueMarketOutcomes![0]!.id;
  ```
</CodeGroup>

See [Markets, outcomes & matching](/concepts/markets) for how ids and cross-venue matching work.

## 4. Get a quote

Send the user's token so the quote is executable, not a preview.

<CodeGroup>
  ```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")
  echo "$QUOTE" | jq '{quoteId, status, expiresAt, totalFilled, totalCostIncFees, fills: [.fills[] | {venue, avgPrice, venueQty}], warnings}'

  export QUOTE_ID=$(echo "$QUOTE" | jq -r .quoteId)
  ```

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

Fill only when `status` is `"ok"`. Otherwise:

| `status` | Fix |
| - | - |
| `insufficient_balance` | Fund the account (step 2) or lower `maxSpend`. |
| `min_order_size_violated`, `insufficient_input_amount` | Raise `maxSpend`. |
| `insufficient_depth`, `no_orderbooks` | Pick another market. |

The quote expires at `expiresAt`, about 45 seconds after it is created. Fill within that window or
quote again. Details: [Quotes & smart routing](/concepts/quotes).

## 5. Execute the quote

<CodeGroup>
  ```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\" }" | jq
  ```

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

```json theme={null}
{
  "quoteId": "k2r8v5n1x7c4m9t3b6q0w2za",
  "orderIds": ["cmf3r1a2b00c4mw0lq8y7e3uj", "cmf3r1a3c00c5mw0lt2k9w6nd"],
  "status": "pending"
}
```

`pending` means accepted, not done. If the call fails:

| Response | Fix |
| - | - |
| `400` `quote_not_found` or `quote_expired` | The quote expired. Check step 6 for this `quoteId` first; if it has no orders, quote again. |
| `400` `quote_insufficient_balance` | The balance changed. Re-check balances and quote again. |
| `400` other `quote_*` codes | See [Errors, retries & idempotency](/concepts/errors). |
| `403` | Trading is not available in the user's region, or the app hit its testing-mode trade cap. |

Do not create a new quote to retry a fill whose result you did not see. Poll the old `quoteId`
first, or you may trade twice.

## 6. Track it to a final state

Poll the execution status until `terminal` is `true`, waiting `pollAfterMs` between calls.

<CodeGroup>
  ```bash cURL theme={null}
  while :; do
    STATUS=$(curl -s -G $API/execution/status \
      -H "x-app-id: $APP_ID" -H "Authorization: Bearer $ACCESS_TOKEN" \
      --data-urlencode "quoteId=$QUOTE_ID")
    echo "$STATUS" | jq -c '{overallState, terminal}'
    [ "$(echo "$STATUS" | jq -r .terminal)" = "true" ] && break
    sleep 1
  done
  echo "$STATUS" | jq '{overallState, errorReason, orders: [.orders[] | {venue, status, filledAmountRaw, actualSharesRaw, executionPriceRaw, txHash}]}'
  ```

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

The final `overallState` is one of:

| `overallState` | Meaning |
| - | - |
| `filled` | Every order filled. Done. |
| `partially_filled` | Some of the trade filled. Each order's `status` and `partialFillReason` say how much and why. |
| `failed` | Nothing filled. Read `errorReason` and each order's `errorReason`. |
| `cancelled`, `expired` | Nothing filled. |

`filledAmountRaw` is USD spent and `actualSharesRaw` is shares bought, both as 6-decimal integer
strings. Every status and state is listed in
[Order lifecycle & statuses](/concepts/order-lifecycle).

The new position shows up in `GET /execution/positions`.

## Test without real funds

Add `mode=paper` to the quote, `"mode": "paper"` to the fill body, and `mode=paper` to the status
call. Skip step 2: a paper account is created for the user on first use, with a simulated
`PAPER_USD` balance. Paper fills price against live books but never reach a venue. See
[Paper trading](/recipes/paper-trading).

## Next

<CardGroup cols={2}>
  <Card title="Choose your integration" href="/integration-options">
    Direct orders, limit orders, and self-custody.
  </Card>

  <Card title="Errors, retries & idempotency" href="/concepts/errors">
    Every error code and which calls are safe to retry.
  </Card>

  <Card title="Webhooks" href="/recipes/webhooks/overview">
    Get trade and deposit events pushed to your backend.
  </Card>

  <Card title="Base URL & API keys" href="/environments">
    Move to production and add a server API key.
  </Card>
</CardGroup>
