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

# Agg by Snag

> Let your users trade prediction markets across Polymarket, Kalshi, Limitless and more with one integration.

## Overview

Portal integrates with [Agg](https://agg.market) so your users can trade event contracts across every major prediction market from one API. Agg aggregates Polymarket, Kalshi, Limitless, Myriad, Predict, Hyperliquid and others, routes each order to the best price, and bridges funds between chains when the venue settles somewhere the user is not funded.

Portal proxies the Agg API server-side with the credentials you store in the Portal Dashboard, and holds each end user's Agg session so your app never handles an Agg token.

> [!NOTE]
>
> Unlike most Portal integrations, you sign up with Snag directly and sign their Master Service Agreement. Portal is not a party to it. Read [Compliance](#compliance) before you ship.

## How it works

Agg holds a **managed balance** per user, funded by sending stablecoins to a deposit address. Trades draw from that balance, and Agg bridges between chains as needed, so a user funded on Base can trade a venue that settles on Polygon.

A trade is two calls. You **quote** an outcome, which prices it across every venue and returns a `quoteId`, then you **fill** that quote. Execution is asynchronous, usually under a minute, and you poll for the result.

By default, users never sign a transaction to place an order: Agg executes from the managed balance. They sign twice around the edges, once to link their wallet at sign-in, and once per on-chain transfer when funding the balance. If you would rather trade straight from the user's Portal wallet, see [Self-custody fills](#self-custody-fills).

### Markets, events and outcomes

Agg models prediction markets in three layers:

| Layer | What it is | Endpoint |
| - | - | - |
| **Event** | The real-world happening, aggregated across venues. "Buccaneers vs. Cowboys" | `GET /events` |
| **Market** | One venue's question about it. "Match Winner" on Polymarket | `GET /markets`, or `venueMarkets[]` on an event |
| **Outcome** | The tradable leg. "Yes" or "No" | `venueMarketOutcomes[]` on a market |

**You quote an `outcomeId`**, which is `venueMarketOutcomes[].id`. Not the market id, not the event id. `GET /events` embeds the whole tree, so one call gives you everything to render a browse UI; `GET /markets` queries the market layer directly when you do not need the event.

For the rest of a market UI:

| Screen | Endpoint |
| - | - |
| Event page | `GET /events/{eventId}/bootstrap` returns the event, the first page of its markets and the outcomes to chart, in one call. Page further with `GET /markets?venueEventId={eventId}`. |
| Event details | `GET /events/{eventId}`, including `description`. Agg is moving `description` out of list responses, so read it here. |
| Search | `GET /search?type=events&q=fed` (or `type=markets`), ranked by relevance. |
| Category navigation | `GET /categories` for the top level, `?parentId=` for children. |
| Price history | `GET /charts/bars` and `GET /charts/history/manifest`, see [Charts](#charts). |

Markets from `GET /markets` and from the bootstrap carry the constraints an order must meet: `tickSize`, `minOrderSize` and the fee fields (`feeRate`, `feeExponent`, `feeBasis`). The markets embedded in `GET /events` leave these out, so read them from `/markets` or the bootstrap before building an order form.

`GET /events` filters on `status` (for example `status=open`) and `categoryIds`. List responses share one shape, with the page under `data`:

```json theme={null}
{ "data": { "data": [ ... ], "nextCursor": "c2", "hasMore": true } }
```

Pass `limit`, then `cursor=<nextCursor>` while `hasMore` is true. The bootstrap's `markets.nextCursor` continues on `GET /markets?venueEventId={eventId}&cursor=`.

> [!NOTE]
>
> The discovery feed includes resolved markets alongside open ones. Filter for an open event **and** an open market before offering a trade; a quote on a resolved market returns `status: "invalid_input"`.

## Setup

### 1. Create an Agg app

1. Sign up at [admin.agg.market](https://admin.agg.market) and create an app.
2. Under **Configure → Domains**, add the domain your users sign in from. Portal reads this list and uses it in the sign-in message, so an app with no approved domain cannot link users.
3. Under **Settings**, turn on **Require an API key on every request**. This is required for the trusted proxy below.
4. Under **Configure → Fees**, set a fee recipient wallet per settlement chain (EVM, Solana, HyperCore). Venues without a wallet will not pay you.
5. Under **Develop → API**, copy your App ID, create an API key, and note the signing key.

> [!NOTE]
>
> Agg's own browser SDK and components call Agg directly from the browser, so they are not used on an app with **Require an API key** on. Portal's integration covers the same flows server-side. If you also want Agg's components, use a separate Agg app for them. The setting stays on once trusted proxy is enabled.

### 2. Ask Snag to enable trusted proxy

Agg applies each venue's location rules using the end user's IP. Because Portal calls Agg from Portal's servers, trusted proxy is what lets Agg use the user's IP that Portal forwards rather than Portal's own.

Email `support@agg.market` with your App ID, confirming that `requireApiKey` is on and that Portal forwards the end user's IP. Portal strips any inbound `x-agg-client-ip` and sets it from the connection it observes.

> [!NOTE]
>
> Before trusted proxy is enabled, quotes still price normally, but every venue carries `unavailableReason: "geo_blocked"` and fills do not go through. If fills fail during setup, check this first.

### 3. Enable the integration in Portal

1. Open the [Portal Dashboard](https://app.portalhq.io) and go to **Integrations**.
2. Add **Agg by Snag** and paste your App ID, API key and signing key.
3. Save. Integrations are environment-scoped, so repeat per environment.

## Linking a user

Before a user can trade, their Portal wallet must be linked to an Agg account. Two calls, with a signature between them.

```bash theme={null}
# 1. Ask Portal for the sign-in message
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/link/challenge' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{ "chainId": "eip155:137" }'
```

The response carries the message and `signParams`, a two-element array of the
hex-encoded message and the signing address, ready to pass straight through.

```bash theme={null}
# 2. Sign it with the Enclave MPC API, adding the client's signing share
curl --request POST \
  --url 'https://mpc-client.portalhq.io/v1/sign' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{
    "chainId": "eip155:137",
    "method": "personal_sign",
    "params": ["0x706f7274616c...", "0xafe8edc6..."],
    "rpcUrl": "https://api.portalhq.io/rpc/v1/eip155/137",
    "share": "<YOUR_SECP256K1_SHARE>"
  }'
```

`params` is the `signParams` array verbatim. `share` is the client's own
secp256k1 signing share, which Portal never sends and you must supply here.
The response `data` is the signature.

```bash theme={null}
# 3. Complete the link
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/link' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{ "signature": "0x..." }'
```

> [!NOTE]
>
> If you use a Portal SDK, sign with the SDK's `request` method, passing
> `personal_sign` and the `signParams`. The SDK supplies the share, so you never
> handle it directly. The raw call above is for server-side callers holding the
> share themselves.

Portal stores the resulting session encrypted, refreshes it automatically, and binds your Portal client id to the Agg account as an external id so you can correlate the two.

> [!NOTE]
>
> `GET /link` reports the stored link, not a live session. It can return `linked: true` while the next call returns `AGG_LINK_REQUIRED` because the Agg session has expired. Branch on the error `id`, never on `linked` alone, and re-run the link flow when you see it.

### Account-abstracted clients are not supported

Agg signs users in with an ECDSA signature from the wallet's own key (an EOA), so a smart account cannot link. Portal returns **400 `AGG_ACCOUNT_ABSTRACTION_UNSUPPORTED`** on the link and trading routes for an account-abstracted client.

Discovery and market data (`/events`, `/markets`, `/search`, `/categories`, `/charts/*`, `/venue-geo-policy`, `/app-config`, `/midpoints`, `/orderbooks`) stay open, so an AA client can still browse and render markets.

## Funding

```bash theme={null}
curl --request GET \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/deposit-addresses' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]'
```

Returns one EVM address and one Solana address, plus the chains and tokens accepted on each. Like every chain id in Agg responses through Portal, `supportedChains[].chainId` is CAIP-2 where the chain has one; a chain without one, such as Hyperliquid (`1337`), keeps Agg's id. Wallets are provisioned lazily, so the first call can return `{ "ready": false }`. Poll until it is ready.

> [!WARNING]
>
> **Fetch the deposit address each time you need it.** Agg can reassign the managed wallet of an inactive user and issue a new address on the next request, so read it immediately before showing an address or building a transfer rather than caching it.

Funding is a plain token transfer to that address. Portal is not in the path.

Each venue settles on its own chain, so where you fund determines which venues are cheapest to reach:

| Venue | Settles on |
| - | - |
| Polymarket | Polygon |
| Limitless | Base |
| Predict | BNB Chain |
| Kalshi | Solana |
| Hyperliquid | Hyperliquid |

Agg bridges between them automatically. Funding the chain your users trade most keeps bridge fees down.

> [!NOTE]
>
> Polymarket settles in pUSD. When a Polymarket order needs more pUSD than the user holds on Polygon, Agg bridges it in from another chain, and that cost appears in `fees.actual.bridge` on the order.

## Trading

```bash theme={null}
# Price the outcome across every venue
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/quotes' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{
    "outcomeId": "k6qsjixzzfg14nln9lfe67z3",
    "side": "buy",
    "amount": "5",
    "slippageBps": 100
  }'

# Fill the quote it returns
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/orders' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{ "quoteId": "x63799gxccbe5rtpool1pmrg" }'
```

Then poll `GET /orders/{quoteId}` until `terminal: true`, honouring the `pollAfterMs` the response asks for.

| Quote field | Meaning |
| - | - |
| `outcomeId` | `venueMarketOutcomes[].id` |
| `side` | `buy` or `sell` |
| `amount` | On a buy, the USD to spend. Send this or `shares` |
| `shares` | On a sell, the shares to sell |
| `slippageBps` | Maximum slippage, in basis points |
| `venues` | Restrict routing to these venues, for example `["polymarket"]` |
| `custody` | `self` to price against the user's own wallet, see [Self-custody fills](#self-custody-fills) |

> [!WARNING]
>
> **Check availability as well as price.** A quote prices every venue, including ones this user cannot trade on, so `status: "ok"` tells you pricing succeeded. Before offering a trade, check **both** that `status` is `ok` **and** that `warnings[]` is empty or absent. Per venue, branch on `unavailableReason`; `venueSoloQuotes[].status` describes whether the venue could price the trade, not whether the user may take it.

Quotes expire about 45 seconds after creation, with `refreshAt` at 15 seconds as the re-quote hint. Do not show a price for longer than that.

A quote also tells you what the user nets and why a venue could not quote:

* `estimatedNetProceeds` is what a sell returns after fees, and `positionAvailability.totalSellableShares` how much the user can sell.
* `quoteFailure` names the venue and reason when a venue could not quote, with `retryAt` when it is worth asking again.
* `venueSoloQuotes[]` shows what each venue alone would do. If the routed quote is not fillable but one venue is, quote again with `venues` set to that venue.
* Orders below a venue's minimum come back as `status: "min_order_size_violated"` before anything is submitted.

For limit orders, size in multiples of the market's `sizeStepRaw` and at least its `minSizeRaw`, both 1e6-scaled share amounts. `GET /orderbooks` returns them per market under `requestedMarket`.

### Self-custody fills

A fill can spend from the user's own Portal wallet instead of the managed balance. Quote with `custody: "self"` so the price reflects that wallet, then send `custody: "self"` with the fill:

```bash theme={null}
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/orders' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{ "quoteId": "x63799gxccbe5rtpool1pmrg", "custody": "self" }'
```

Portal names the wallet the user linked as the signer, so the fill always draws on that user's own wallet. Agg then prepares what needs signing asynchronously:

1. Poll `GET /orders/{quoteId}` and read `pendingSignatures[]`.
2. Sign each entry with the Portal wallet, by its `type`. For `eip712`, call `eth_signTypedData_v4` with `params: [signerAddress, JSON.stringify(payload)]`. For `personal_sign`, use `personal_sign`. For `transaction`, broadcast it with `eth_sendTransaction` and keep the hash. Agg can also ask for `hl_l1_action`, `eip7702_authorization` and `safe_tx` steps, depending on the venue and route. An entry with `optional: true` can be skipped.
3. Send the answers back. Each carries the entry's `stepId` and either the `signature` or, for `transaction`, the `txHash`.

```bash theme={null}
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/orders/x63799gxccbe5rtpool1pmrg/signatures' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{ "signatures": [{ "stepId": "step_1", "signature": "0x..." }] }'
```

The response carries the next batch of `pendingSignatures`, empty once the fill needs nothing more. Each entry has an `expiresAt`, so answer promptly.

> [!NOTE]
>
> Cross-chain self-custody fills need a token approval for the bridge. By default (`approveMode: "sponsored"`) the wallet signs a one-time EIP-7702 authorization over a raw digest instead of sending the approval itself. If your signing flow does not produce that, send `approveMode: "user_broadcast"` with the order, and the user broadcasts the approval and pays its gas.

### Limit orders and cancelling

`POST /orders/limit` places a resting order, which is the only kind `POST /orders/cancel` can act on. A market order fills or fails immediately.

| Limit order field | Meaning |
| - | - |
| `venue` | The venue to rest on, for example `polymarket` |
| `venueMarketOutcomeId` | The outcome id on that venue |
| `side` | `buy` or `sell` |
| `limitPriceRaw` | Price scaled by 1e6, see below |
| `sizeRaw` | Shares scaled by 1e6 |
| `timeInForce` | `GTC`, `GTD`, `FOK`, `FAK`, `IOC` or `ALO` |
| `expiresAt` | Required with `GTD` |
| `postOnly`, `clientOrderId` | Optional |

`POST /orders/direct` is a market order on one named venue without a quote. It takes `venue`, `venueMarketOutcomeId`, `side`, `externalId` (your idempotency key, up to 36 characters, so a retried request returns 409 rather than trading twice), one of `maxSpend` (USD, on a buy) or `sellShares`, and optionally `slipCapBps`.

> [!NOTE]
>
> `limitPriceRaw` and `sizeRaw` are positive integer strings **scaled by 1e6**: `200000` is a price of 0.20 and `5000000` is five shares. On the read side, `GET /orders` returns `quotedPriceRaw` as a decimal string like `"0.5"`, while `limitPriceRaw` and `averageFillPriceRaw` keep the 1e6 scale. Each row's id is `id`. Portal validates the write side and tells you which is wanted.

Cancel takes the order id, not the `quoteId`: `orderIds[]` on the fill, or `id` on a `GET /orders` row. The reservation is released in full.

> [!NOTE]
>
> If Agg bridged funds to back a limit order, cancelling it releases the reservation and leaves those funds on the venue's chain, ready for the next order there. The bridge fee is not refunded.

> [!NOTE]
>
> **Polymarket has a \$1 minimum on orders that trade immediately.** The whole order (price x size) must be at least \$1, even if only part of it fills. The venue applies this after submission, so a smaller order moves from `pending` to `failed` with the reason in `errorMessage`. Validate price x size in your UI before submitting.

### Order statuses

`GET /orders` reports each order's `status`:

| Status | Meaning |
| - | - |
| `pending`, `submitting` | Accepted by Agg, not yet live on the venue |
| `open` | Resting on the venue, nothing filled |
| `partially_filled_open` | Part filled, the rest still resting |
| `filled` | Fully filled |
| `partial_fill` | Final. Part filled, the rest will not fill |
| `cancel_pending` | Cancel requested, not yet confirmed by the venue |
| `cancelled` | Final. Cancelled with nothing filled, including a `GTD` order that expired |
| `failed` | Final. Rejected by the venue, see `errorMessage` |

A `partial_fill` looks the same whether the venue cancelled the remainder (a `FAK` order) or the user did. `partialFillReason` is `venue_cancelled` in both cases; `cancelRequestedAt` is set only when the user cancelled. On a final order, `remainingSizeRaw` and `reservedCostRaw` keep their last values, so read `filledSizeRaw` and `filledCostRaw` for what happened and `GET /balances` for the released funds.

Cancel can return `cancel_pending` or `cancelled` from either venue, so handle both.

## Positions, resolution and redemption

`GET /positions` returns open and resolved positions. When a market resolves in the user's favour, `redeemStatus` flips from `ineligible` to `eligible` and the winning leg carries `winner: true`.

```bash theme={null}
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/redemptions' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{ "venueMarketOutcomeIds": ["h9vc24gweds6zp82x9xzooyq"] }'
```

> [!NOTE]
>
> The id redemption wants is `venueMarketOutcomes[].venueBreakdown[].venueMarketOutcomeId` from the position, which is the **venue's** outcome id. It is not the `outcomeId` you quoted against and cannot be reconstructed from the order.

Redemption pays out at par with no fee and credits the chain the venue settles on. It usually completes in seconds.

## Withdrawals

Four routes, under the same base path:

| Route | Purpose |
| - | - |
| `POST /withdrawals/quote` | How much can leave, to a given chain |
| `POST /withdrawals/preview` | Fees and receive amount, dispatches nothing |
| `POST /withdrawals` | Move the funds |
| `GET /withdrawals/{withdrawalId}` | Status |

```bash theme={null}
curl --request POST \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/agg/withdrawals' \
  --header 'Authorization: Bearer [clientApiKey|clientSessionToken]' \
  --header 'Content-Type: application/json' \
  --data '{
    "tokenSymbol": "USDC",
    "destinationChainId": "eip155:8453",
    "amountRaw": "2000000",
    "requestId": "9c270726-10b1-4592-8c5e-e81edc740e0c"
  }'
```

`amountRaw` is in the destination token's base units: 6 decimals for USDC, 18 on BNB Chain. The withdrawal quote returns `decimals` for the destination you ask about. `destinationChainId` is a CAIP-2 chain id, such as `eip155:8453`, or `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` for Solana mainnet (the `solana:mainnet` alias also works). Send `max: true` alongside `amountRaw` to cap the withdrawal at what can actually be delivered.

`destinationAddress` defaults to the user's own Portal address for that chain. Any other address must be on your environment's destination allowlist for Agg, otherwise the request returns 403; add entries in the Portal Dashboard or through the Custodian API. Pass `requestId`, a UUID, to make an exact-amount withdrawal replayable: the same parameters return the original withdrawal instead of dispatching twice, and changed parameters are rejected. It cannot be combined with `max`.

> [!NOTE]
>
> `legs[]` on the response is the only place a transaction hash appears, one per hop. A same-chain withdrawal has one leg; a bridged one has several.

### Balances are per chain, not one pot

`GET /balances` reports an aggregate `availableRaw` across chains, **but that is not how much a user can spend on a given trade.** Spendability depends on which chain holds the funds and the route Agg chooses, and when a chain already holds some balance Agg may draw on that chain alone. So a quote can return `Insufficient balance: requested \$2, have \$0.00` while the aggregate shows several dollars.

The same applies on the way out: `POST /withdrawals/quote` returns a `rawBalanceRaw` that is **destination-dependent**. A Base or Polygon destination may not be able to reach a balance sitting on BNB Chain, while a BNB destination can reach everything. Always quote the specific destination rather than reading the aggregate.

## Geo restrictions

Every venue restricts countries, and the lists differ. The United States is blocked on all of them.

* `GET /venue-geo-policy` returns the current per-venue blocked lists. Venues update them, so **read it live** rather than caching it.
* A venue can exist without appearing in that response, so do not treat it as the full venue list.
* `isLocationBlocked` on the Agg user profile reflects **app-level** policy only, meaning OFAC plus whatever you configure. It reads `false` for a user every venue blocks. It is not a "can this user trade" flag.

Trading is geo-gated. **Redemption and withdrawal are not**, so a user travelling somewhere restricted can still collect winnings and move funds out.

### Forwarding the end user's IP

Agg resolves the country from the caller's IP. Portal forwards it automatically:

* **SDK traffic** reaches Portal from the user's device, so Portal observes it.
* **Server-to-server callers** must send `X-Portal-End-User-IP`, because Portal never sees the end user. Without it every one of your users resolves to your backend.

It must be a single public IPv4 or IPv6 address. Portal rejects lists and private ranges with a 400, so a malformed value surfaces right away instead of resolving the user to the wrong country.

## Market data

`GET /midpoints` and `GET /orderbooks` take a comma-separated `venueMarketIds` and return live prices. Without them every market renders at 50/50.

> [!NOTE]
>
> `arbReturn` on an event and the `matched` array are discovery signals, not trade instructions. `arbReturn` can sum across related markets that are mutually exclusive, such as "Democrats Sweep" and "Republicans Sweep", and cross-venue matching groups outcomes that describe the same event. Quote the specific outcome before presenting a price or return to users.

## Charts

`GET /charts/bars?venueMarketOutcomeId={outcomeId}&to={unixMilliseconds}` returns OHLCV bars for one outcome. `resolution` is `1`, `5`, `60` (minutes) or `1D`, and `countBack` asks for a number of bars ending at `to`. All chart times are Unix **milliseconds**, including `to`, `from` and each bar's `t`.

For long ranges, `GET /charts/history/manifest` takes the same parameters and returns up to eight CDN `chunks` of finalized history. Fetch each `url` directly, check it against `sha256`, then fill in everything after `finalizedThrough` from `/charts/bars`. This is the path the bootstrap's `references.historyManifestPath` names, and its `selectedOutcomeIds` are the outcomes to chart first.

`meta.state` tells you what to do next:

| `meta.state` | Meaning |
| - | - |
| `ready` | The series is complete |
| `warming` | Being prepared, retry after `meta.retryAfterMs` |
| `empty` | No trades in the range |
| `failed`, `unsupported` | No history for this outcome at this resolution right now, retry after `meta.retryAfterMs` when present |

## Errors

Errors carry a message, and Portal's typed errors also carry a stable `id` to branch on:

```json theme={null}
{ "error": "This client is not linked to Agg. Complete the Agg link flow and retry.", "id": "AGG_LINK_REQUIRED" }
```

| Status | `id` | What to do |
| - | - | - |
| 409 | `AGG_LINK_REQUIRED` | Run the link flow again |
| 400 | `AGG_ACCOUNT_ABSTRACTION_UNSUPPORTED` | The client is account-abstracted, see above |
| 503 | `AGG_SESSION_REFRESH_IN_PROGRESS` | Another request is renewing this user's Agg session. Retry after a moment |
| 400 | none | Your request did not validate. The message names the field, for example `Invalid request body: limitPriceRaw: ...` |
| 400, 403, 404, 409, 422, 429 | none | Agg declined the request. The message starts with `Agg rejected the request:` and gives Agg's reason, and a validation failure lists the fields in `details.fields` |

Test against staging at `https://api.portalhq.dev` before going live; every path is the same. Each response carries an `x-trace-id` header, which is what Portal support asks for.

## Compliance

Your users trade through **your** Agg app under **your** agreement with Snag. Portal provides the integration; it is not a party to that contract.

> [!WARNING]
>
> Under Snag's Master Service Agreement, geofencing is the subscriber's sole responsibility, and jurisdictional breach is excluded from the liability cap. The prohibited list is broader than the per-venue geo policy and includes Arizona and Nevada domestically. Review the agreement and take your own legal advice before going live.

You are also the reporting entity for AML and KYC for your users, and you must pass Snag's mandatory end-user terms through into your own terms of service.

Live trading opens once the partner agreement is signed in the Agg dashboard. Before that, an app runs with a limit of 20 users and 100 trades, which is enough to build and test against.

## Endpoint reference

All routes are under `https://api.portalhq.io/api/v3/clients/me/integrations/agg/` and authenticate with `Authorization: Bearer [clientApiKey|clientSessionToken]`.

| Method | Path | Purpose |
| - | - | - |
| POST | `/link/challenge` | Start sign-in, returns the message to sign |
| POST | `/link` | Complete sign-in with the signature |
| GET | `/link` | Stored link status |
| DELETE | `/link` | Unlink |
| GET | `/events` | Events with their markets and outcomes |
| GET | `/events/{eventId}` | One event, including `description` |
| GET | `/events/{eventId}/bootstrap` | Event page: the event, its first markets and outcomes to chart |
| GET | `/markets` | Markets directly |
| GET | `/search` | Search events or markets |
| GET | `/categories` | Category tree |
| GET | `/charts/bars` | OHLCV bars for an outcome |
| GET | `/charts/history/manifest` | CDN chunks of finalized chart history |
| GET | `/venue-geo-policy` | Per-venue blocked countries |
| GET | `/app-config` | Your venue and category switches |
| GET | `/midpoints` | Live midpoints for a batch of markets |
| GET | `/orderbooks` | Live books for a batch of markets |
| GET | `/orderbooks/outcome/{outcomeId}` | One outcome's ladder and depth |
| GET | `/me` | The Agg user profile |
| GET | `/deposit-addresses` | Where to send funds |
| GET | `/balances` | Managed balance, `?sync=true` to re-read chains |
| GET | `/positions` | Open and resolved positions |
| GET | `/orders` | Order history |
| GET | `/activity` | Trades, deposits, withdrawals and bridges |
| POST | `/quotes` | Price an outcome across venues, `custody: "self"` to price against the user's wallet |
| POST | `/orders` | Fill a quote, `custody: "self"` to fill from the user's wallet |
| GET | `/orders/{quoteId}` | Execution status, including `pendingSignatures` |
| POST | `/orders/{quoteId}/signatures` | Answer a self-custody fill's `pendingSignatures` |
| POST | `/orders/direct` | Market order on one venue, no quote |
| POST | `/orders/limit` | Resting limit order |
| POST | `/orders/cancel` | Cancel a resting order |
| POST | `/redemptions` | Claim winnings on resolved positions |
| POST | `/withdrawals/quote` | Maximum deliverable to a chain |
| POST | `/withdrawals/preview` | Fees and receive amount |
| POST | `/withdrawals` | Withdraw |
| GET | `/withdrawals/{withdrawalId}` | Withdrawal status |

## Webhooks

Register your webhook URL directly with Agg in their dashboard and verify deliveries with your signing key. Portal is not in the delivery path, the same as other Portal integrations.


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