@agg-build/sdk
Vanilla TypeScript client for the AGG prediction market aggregator — one client, every surface (auth, markets, orderbooks, charts, trading, managed execution, WebSockets).
What it is
@agg-build/sdk is the lowest-level package partners install. It speaks to the AGG API over HTTPS and
WebSockets, manages auth/session state, and exposes typed wrappers for every product endpoint
(markets, orderbooks, charts, trade execution, deposits/withdrawals, managed wallets, admin and
more). It has no framework dependency and works in browsers, Node.js, and React Native.
For React apps, layer @agg-build/hooks, @agg-build/ui, and @agg-build/auth on top — they all use this client
under the hood.
When to use this package
- You are building a vanilla TS/JS app, React Native app, or server-side integration.
- You want a typed, batteries-included client for the AGG REST API and WebSocket gateway.
- You are authoring your own React layer and need the underlying auth/session/trade primitives.
- You need a server-only helper: SSR config fetching, webhook verification, external-ID signing,
or the admin API client (see
@agg-build/sdk/server).
If you are shipping a React app and want ready-made hooks or UI, install @agg-build/hooks,
@agg-build/ui, and @agg-build/auth
instead — those packages are built on top of @agg-build/sdk.
Install
npm install @agg-build/sdk
Quick start
import { createAggClient } from "@agg-build/sdk";
const client = createAggClient({
baseUrl: "https://api.agg.market",
appId: "your-app-id",
});
// Health check
const hello = await client.getHello();
// Current auth state (persisted across reloads via localStorage)
console.log(client.isAuthenticated, client.user);
Authenticate a user (SIWE)
// 1. Start the auth flow
const { nonce } = await client.authStart({ provider: "siwe" });
// 2. Build the EIP-4361 message
const message = client.buildSiweMessage({
address: "0x…",
chainId: 1,
nonce,
domain: window.location.host,
uri: window.location.origin,
});
// 3. Sign with the user's wallet (wagmi, viem, ethers, …)
const signature = await wallet.signMessage(message);
// 4. Verify → stores accessToken + user in the client
const { accessToken, user } = await client.verify({ message, signature });
SIWS (Solana) uses the same flow with buildSiwsMessage and a base58-encoded Ed25519 signature.
Redirect flows (Google, Apple, Twitter, email magic-link) return a URL to navigate to:
const response = await client.authStart({
provider: "google",
redirectUrl: "https://yourapp.com/auth/callback",
});
if (response.type === "redirect") {
window.location.assign(response.url);
}
// …back on the callback page:
const code = new URLSearchParams(window.location.search).get("code");
if (code) {
await client.exchangeAuthCode(code);
window.history.replaceState(null, "", window.location.pathname);
}
HttpOnly cookie-refresh mode
For browser apps that prefer an HttpOnly refresh cookie over storing the refresh token in JS,
construct the client with authDelivery: "cookie-refresh":
const client = createAggClient({
baseUrl: "https://api.agg.market",
appId: "your-app-id",
authDelivery: "cookie-refresh",
});
In this mode, verify() and exchangeAuthCode() may omit refreshToken from the returned
object. The refresh cookie stays scoped to the AGG API host under /auth and is never exposed to
partner frontend domains.
Streaming data (WebSocket)
const ws = client.createWebSocket({
onSnapshot: (outcomeId, book) => console.log("snapshot", outcomeId, book),
onDelta: (outcomeId, book) => console.log("delta", outcomeId, book),
onTrade: (trade) => console.log("trade", trade.venue, trade.price, trade.size),
});
// Orderbooks are keyed by venueMarketOutcomeId (the `outcomes[].id` on REST).
const unsubscribe = ws.subscribe("venue-market-outcome-id");
// Optional: authenticate for user-scoped events (orders, balances)
ws.authenticate(accessToken);
// Cleanup
unsubscribe();
ws.destroy();
createWebSocket requires wsUrl to be passed to createAggClient. The underlying
AggWebSocket can also be instantiated directly if you want finer control.
Server-side helpers
Import from the /server entrypoint for Node-only utilities:
import { AggAdminClient, signExternalId, verifyWebhook } from "@agg-build/sdk/server";
// Sign a partner external-ID assertion before sending to the browser
const assertion = signExternalId(process.env.AGG_APP_SECRET!, partnerUser.id);
// Verify an inbound webhook
verifyWebhook(rawBody, req.headers, process.env.AGG_WEBHOOK_SECRET!);
// Admin API client (uses x-app-api-key)
const admin = new AggAdminClient({
baseUrl: "https://api.agg.market",
apiKey: process.env.AGG_APP_API_KEY!,
});
const users = await admin.listUsers("your-app-id", { limit: 50 });
Client options
| Option | Type | Required | Description |
|---|---|---|---|
baseUrl | string | Yes | API base URL (e.g. https://api.agg.market) |
appId | string | One of appId/adminKey | Partner app ID |
adminKey | string | One of appId/adminKey | Admin API key for admin-tier routes |
wsUrl | string | For WebSocket | WebSocket gateway URL (required by createWebSocket) |
orderbookUrl | string | No | Second socket for the orderbook channel. Unset keeps orderbooks on wsUrl. When set, an orderbook-only subscription leaves wsUrl idle |
orderbookLatency | "conflated" | "realtime" | No | Orderbook tier. Default conflated omits latency; realtime sends it |
authDelivery | "body" | "cookie-refresh" | No | Token delivery mode (default: "body") |
persistSession | boolean | No | Persist auth hint to localStorage (default: true) |
kalshiDemo | boolean | No | Deprecated compatibility option; ignored |
Call client.destroy() when you no longer need the client to release internal resources
(BroadcastChannel used for cross-tab auth sync).
Main exports
AggClient methods
Auth & session
| Method | Description |
|---|---|
authStart(body) | Start any auth flow (SIWE nonce, OAuth redirect, etc) |
buildSiweMessage(params) | Build an EIP-4361 message string |
buildSiwsMessage(params) | Build a Solana sign-in message |
verify({ message, signature }) | Verify SIWE/SIWS signature, store tokens |
exchangeAuthCode(code) | Exchange redirect auth code for tokens |
refreshAccessToken() | Refresh the JWT using the stored refresh token |
setSession({ accessToken, refreshToken?, user? }) | Seed tokens from a redirect callback |
setAccessToken(token) / clearAccessToken() | Manage the JWT directly |
signOut() | Revoke tokens server-side and clear local session |
onAuthStateChange(listener) | Subscribe to auth state changes |
isAuthenticated / user / authStatus | Getters for current session state |
User profile & accounts
| Method | Description |
|---|---|
getCurrentUser() | Fetch current user profile from /users/me |
updateUser(data) | Update username, avatar, etc. |
createAvatarUploadUrl(contentType) | Get a presigned avatar upload URL |
linkExternalId({ externalId, timestamp, hmac }) | Link a backend-signed partner user ID |
disconnectAccount(provider) | Remove a linked auth provider |
Market data
| Method | Description |
|---|---|
getVenueEvents(options?) | List prediction market events (cursor pagination) |
getVenueEventById(id) | Get a single event by ID |
getVenueMarkets(options?) | List venue markets with filters (cursor pagination) |
getCategories(options?) | List market categories |
Correlated Markets
| Method | Description |
|---|---|
resolveCorrelatedMarkets(params) | Resolve a market and return expand/hedge ideas |
getCorrelatedMarketCascade(params) | Get expand/hedge ideas for a known venueMarketId |
getMarketCorrelatedSignals(id) | Inspect generated signals for a market |
queryCorrelatedMarkets(params) | Search markets by signal similarity |
Orderbooks & routing
| Method | Description |
|---|---|
getOrderbooks(params) | Get live per-venue orderbooks for given venueMarketIds |
getSmartRoute(params) | MILP solver for optimal order routing across venues |
Chart data
| Method | Description |
|---|---|
getChartBars(params) | TradingView-style OHLCV candles for an outcome |
getPricesHistory(params) | Price history for venue market outcomes |
getOrderbookHistory(params) | Orderbook depth history for an outcome |
Trading
| Method | Description |
|---|---|
validateTrade(body) | Pre-flight trade validation |
executeTrade(body) | Execute supported prediction-market orders via backend |
computeSplits(body) | Compute best order splits across venues via API |
Venue keys
| Method | Description |
|---|---|
listVenueKeys(venues?) | List stored venue API keys (no secrets returned) |
upsertVenueKey(body) | Create or update a venue API key |
deleteVenueKey(venue) | Delete a venue API key |
Managed execution & balances
| Method | Description |
|---|---|
validateManaged(params) | Check balances and bridge requirements |
executeManaged(params) | Fill a quote from getSmartRoute() — see note below |
withdrawManaged(params) | Withdraw from managed wallets to an external address |
withdrawPreview(params) | Preview withdrawal receive amount and route fees |
getWithdrawalQuote(params) | Quote the maximum deliverable withdrawal amount |
syncManagedBalances() | Trigger on-chain balance sync |
cancelManagedOrder(orderId) | Cancel a pending managed execution order |
getVenueBalances(venues) | Get balances across venues |
getManagedBalances() | Get managed wallet balances per-chain + per-venue |
getDepositAddresses() | Get managed wallet deposit addresses (EVM + Solana) |
To quote and fill a managed trade, target a venue market outcome. Buys are budgeted with
maxSpend; sells specify the number of contracts with sellShares. Pass the returned quoteId
to executeManaged():
const buyRoute = await client.getSmartRoute({
venueMarketOutcomeId,
tradeSide: "buy",
maxSpend: 50,
});
await client.executeManaged({ quoteId: buyRoute.quoteId });
const sellRoute = await client.getSmartRoute({
venueMarketOutcomeId,
tradeSide: "sell",
sellShares: 10,
});
await client.executeManaged({ quoteId: sellRoute.quoteId });
venueMarketId, outcomeId, and side: "yes" | "no" remain deprecated compatibility aliases;
new integrations should use venueMarketOutcomeId and tradeSide.
Removed in v4.
quoteManaged()was deleted — its endpoint,POST /execution/quote, does not exist on the API, so the call only ever returned 404. Use the two-step flow above.
Orders & positions
| Method | Description |
|---|---|
getOrders(params?) | List trade-executor orders |
getExecutionOrders(params?) | List execution orders (cursor pagination) |
getExecutionPositions(params?) | List execution positions (cursor pagination) |
getPositions(params?) | Get open positions grouped by matched market |
getUserHoldings(params) | Get user positions per venue |
Leaderboard
| Method | Description |
|---|---|
getLeaderboard(params?) | Trader leaderboard by realized P&L or volume (cursor pagination) |
Utilities
| Method | Description |
|---|---|
getHello() | Health check |
request(path, init?) | Generic HTTP with auto-retry on 401 |
createWebSocket(callbacks?) | Create a WebSocket instance (requires wsUrl) |
destroy() | Release resources (BroadcastChannel, etc.) |
Standalone utilities
| Export | Description |
|---|---|
AggWebSocket | Low-level WebSocket client (used by client.createWebSocket()) |
CandleBuilder | Chart-library-agnostic candle builder (trades → OHLC) |
computeBestSplitsByAmount | Pure function: optimal split across venue orderbooks |
applyOrderbookDelta | Apply WS delta to an in-memory orderbook |
computeChecksum | CRC32 orderbook checksum (used to detect desync) |
snapshotToOrderbook | Convert a WS snapshot to the in-memory shape |
aggregateMidpoint | Aggregate outcome midpoints across venues |
mergeCandles | Merge live + historical candles, live-wins on collision |
mergeClosedCandles | Variant that merges only fully closed candles |
TurnstileChallengeError | Thrown when an auth endpoint requires a Turnstile challenge |
AggApiError | Thrown for every non-2xx API response — see below |
isAggApiError | Type guard for AggApiError |
Error handling
Every non-2xx response is thrown as an AggApiError carrying status, and —
when the API supplies them — code, retryable and per-field errors. Branch
on status/code rather than matching message text; messages are human-facing
and may be reworded.
import { isAggApiError } from "@agg-build/sdk";
try {
await client.getPositions();
} catch (err) {
if (isAggApiError(err)) {
if (err.status === 401) return promptReconnect(); // session lapsed
if (err.code === "unregistered_domain") {
// The signed SIWE `domain` is not in the app's allowedOrigins.
// Note this is NOT the Origin header — sign-in works with no Origin
// at all, which is what makes Node and React Native work.
}
}
throw err;
}
@agg-build/sdk/server
| Export | Description |
|---|---|
AggAdminClient | Admin REST client using x-app-api-key (server-only) |
signExternalId(secret, userId) | HMAC-sign a partner user ID before calling linkExternalId() |
verifyWebhook(payload, headers, secret) | Verify an inbound webhook signature |
parseWebhookEvent(payload, headers, secret) | Verify + parse a webhook into a typed event |
WebhookVerificationError | Thrown on signature/timestamp failures |
WebSocket callbacks
| Callback | When it fires |
|---|---|
onSnapshot(outcomeId, book) | Full orderbook snapshot received |
onDelta(outcomeId, book) | Incremental orderbook update applied |
onTrade(trade) | Trade execution |
onHeartbeat(hb) | Server heartbeat (every 30s) |
onSubscribed / onUnsubscribed | Subscription lifecycle |
onConnected / onAuthenticated | Connection / auth lifecycle |
onOrderSubmitted | Order placed via WebSocket |
onBalanceUpdate | Balance change event |
onMarketResolved | Market resolution |
onDiagnostics(event) | Diagnostic events (seq gaps, stale connection) |
onConnectionStateChange(connected) | Primary connection state changed |
onOrderbookConnectionStateChange(connected) | Orderbook socket state. Fires only when orderbookUrl is set |
onError(msg) | Error message from server |
Orderbook books are keyed by outcome. OrderbookState.outcomeId is the identity of a
book; the deprecated OrderbookState.marketId holds the same outcome id, not a venue
market id, because orderbook frames carry no market id. Use the REST venueMarketId
(e.g. from getOutcomeOrderbooks) or your own outcome → market mapping when you need the
market.
Key behaviors:
- Single persistent connection with refcounted per-market subscriptions.
orderbookUrlmoves the orderbook channel onto a second socket and reports it throughonOrderbookConnectionStateChange. An orderbook-only subscription does not keepwsUrlconnected. Trades, arb, catalog, and user events stay onwsUrl. - In-memory orderbook state per market (snapshot + delta with CRC32 verification).
- Sequence gap or checksum mismatch triggers automatic resnapshot.
snapshot_unavailableretries only affected active outcomes, with up to three jittered retries and at most 20 outcomes retried per second per socket. Errors still reachonError; successful snapshots, unsubscribe, and socket closure cancel pending retries. Missing replies remain covered by the optional book watchdog.- Auto-reconnect with exponential backoff (base 1s, max 30s).
- Liveness: no message for 45s triggers reconnect; server heartbeats every 30s.
- Bids always sorted descending, asks always sorted ascending.
Headers attached automatically
x-app-id— from theappIdoptionAuthorization: Bearer <jwt>— after sign-inx-admin-key— ifadminKeyis providedx-auth-delivery: cookie-refresh+credentials: "include"on auth-only requests whenauthDelivery: "cookie-refresh"is enabled
Entrypoints
| Entry | Purpose |
|---|---|
@agg-build/sdk | AggClient, createAggClient, AggWebSocket, utilities |
@agg-build/sdk/server | Node-only helpers: admin client, external-ID / webhook signing |
Peer dependencies
None — and as of v4, no runtime dependencies either. The only module the bundle
imports is node:crypto (used by @agg-build/sdk/server), so a fresh install is
under 1 MB with nothing transitive.
Bring your own wallet library. The SDK builds SIWE/SIWS message strings and never
signs anything itself, so viem, ethers, or any other signer works — you pass
in a signMessage function.
Changed in v4. Earlier versions declared
viem,ethers@5,@polymarket/clob-clientand@polymarket/builder-signing-sdkas runtime dependencies. No code path ever imported them, but installers still paid for them — roughly 100 MB ofnode_modulesacross their transitive trees. They were removed in v4; nothing in the SDK's behaviour changed.
Links
- Documentation — https://docs.agg.market/
- Demo app — https://agg.market/
- React hooks —
@agg-build/hooks - Trading UI —
@agg-build/ui - Connect / sign-in UX —
@agg-build/auth
License
MIT
Venue event read admission
getVenueEventById shares concurrent identical reads within each client and auth
session (including normalized cluster and minVolume query values). It retains
no completed result cache. At most four distinct event reads run concurrently;
up to 128 wait in FIFO order. A full queue rejects with AggApiError status 429,
code client_read_queue_full, and a retry delay hint.
A server 429 still rejects the original call without an automatic retry. New and
queued event reads honor its Retry-After delay plus up to 250ms positive jitter;
missing hints use one second. Callers may cancel independently; the shared fetch
is cancelled only after all callers leave. Queued reads reject with AbortError
if authentication changed before admission. destroy() cancels pending reads.
Other read methods and write methods keep their existing retry behavior.