Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2

- run: bun install

- run: bun test

- run: bun run typecheck

- run: bun run build
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ jobs:

- run: bun install

- run: bun test

- run: bun run typecheck

- run: bun run build

- uses: actions/setup-node@v4
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ dist/
*.tsbuildinfo
.DS_Store
.env
logs/
logs/
*.tgz
openapi/.spec-source.json
20 changes: 11 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ TypeScript SDK (`@structbuild/sdk`) for prediction market APIs via `api.struct.t
- **Build:** `bun run build` (outputs ESM + CJS to `dist/`, generates declaration files)
- **Typecheck:** `bun run typecheck`
- **Install deps:** `bun install`
- **Fetch OpenAPI spec:** `bun run fetch-spec:polymarket`
- **Generate types:** `bun run generate:polymarket`
- **Full pipeline:** `bun run prep` (fetch specs, generate types, check routes, build)
- **Check routes:** `bun run check-routes` (validates namespace routes match OpenAPI spec)
- **Fetch webhook spec:** `bun run fetch-spec:webhooks`
- **Generate webhook types:** `bun run generate:webhooks`
- **Fetch all specs:** `bun run fetch-specs` (fetches from production by default; use `STRUCT_ENV=staging` for staging)
- **Generate types:** `bun run generate:polymarket`, `bun run generate:webhooks`, `bun run generate:ws`
- **Full pipeline (prod):** `bun run prep` (fetch specs from prod, generate types, check routes, build)
- **Full pipeline (staging):** `bun run prep:staging` (same as prep but fetches from staging-api.struct.to)
- **Check routes:** `bun run check-routes` (validates namespace routes match OpenAPI spec; warns if specs are from staging)
- **Fix spec:** `bun run fix-spec` (fixes broken `$ref`s in the OpenAPI spec)
- **Test:** `bun test` (integration tests against live API, requires `STRUCT_API_KEY`)
- **Test watch:** `bun test --watch`
- **Link for local dev:** `bun link` (then `bun link @structbuild/sdk` in consumer repo)
- **Pack for testing:** `bun run pack` (builds and creates `.tgz`)

## Architecture

Expand All @@ -29,11 +30,12 @@ TypeScript SDK (`@structbuild/sdk`) for prediction market APIs via `api.struct.t
- `src/http.ts` — Generic `HttpClient` built on `fetch` with timeout via `AbortController`, query param building, exponential backoff retry (429/5xx), request/response hooks, and typed `HttpResponse<T>` responses.
- `src/errors.ts` — Error hierarchy: `StructError` → `HttpError` | `NetworkError` | `TimeoutError` | `WebSocketError` | `WebSocketClosedError`.
- `src/paginate.ts` — `paginate()` async generator for offset-based pagination across any namespace method.
- `src/ws.ts` — `StructWebSocket` for real-time trade streaming. Event system (`on`/`off`/`once`), subscriptions (markets, positions, wallets, conditions, whale/smart-money/insider rooms), auto-replays subscriptions on reconnect.
- `src/ws-transport.ts` — Low-level WebSocket connection management with reconnection (exponential backoff + jitter), pending/replay message queues.
- `src/types/` — All type definitions. `common.ts` (pagination/sort/Venue), `helpers.ts` (OpenAPI type utilities: `OperationQuery`, `OperationPath`, `OperationResponse`), `webhook-helpers.ts` (webhook OpenAPI type utilities), `http.ts` (client config/request/response), `ws.ts` (WebSocket types). `index.ts` barrel exports all types.
- `src/ws.ts` — `StructWebSocket` for real-time streaming. Room-based protocol (`join_room`/`room_message`). Generic typed `subscribe(room, filters?)` / `unsubscribe(room)` with Promise-based acks. Event system (`on`/`off`/`once`/`removeAllListeners`), `on()` returns disposer function. Auto-replays subscriptions on reconnect, keepalive ping.
- `src/ws-transport.ts` — Low-level WebSocket connection management with reconnection (exponential backoff + jitter), pending/replay message queues. `connect()` returns `Promise<void>`.
- `src/types/` — All type definitions. `common.ts` (pagination/sort/Venue), `helpers.ts` (OpenAPI type utilities: `OperationQuery`, `OperationPath`, `OperationResponse`), `webhook-helpers.ts` (webhook OpenAPI type utilities), `ws-helpers.ts` (WS type utilities: `WsSchemas`), `http.ts` (client config/request/response), `ws.ts` (WebSocket types: room IDs, subscribe filters, event types, event map, subscription/response maps). `index.ts` barrel exports all types.
- `src/generated/polymarket.ts` — Auto-generated types from the Polymarket OpenAPI spec via `openapi-typescript`. Do not edit manually.
- `src/generated/webhooks.ts` — Auto-generated types from the Webhooks OpenAPI spec. Do not edit manually.
- `src/generated/ws.ts` — Auto-generated types from the WS AsyncAPI spec (`scripts/generate-ws-types.ts`). Do not edit manually.
- `src/index.ts` — Public barrel export.
- `tests/integration.test.ts` — Auto-discovers namespace methods and runs them against the live API. Test config in `tests/integration.meta.ts` defines params and operationId mappings per method.

Expand Down
160 changes: 134 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ TypeScript SDK for prediction market data via [api.struct.to](https://api.struct
```bash
npm install @structbuild/sdk
# or
pnpm add @structbuild/sdk
# or
bun add @structbuild/sdk
```

Expand Down Expand Up @@ -73,11 +71,11 @@ const trades = await client.trader.getTraderTrades({ address: "0x..." });
const profile = await client.trader.getTraderProfile({ address: "0x..." });
const profiles = await client.trader.getTraderProfilesBatch({ addresses: "0x...,0x..." });
const pnl = await client.trader.getTraderPnl({ address: "0x..." });
const pnlByMarket = await client.trader.getTraderMarketPnl({ address: "0x..." });
const pnlByEvent = await client.trader.getTraderEventPnl({ address: "0x..." });
const marketPnl = await client.trader.getTraderMarketPnl({ address: "0x..." });
const eventPnl = await client.trader.getTraderEventPnl({ address: "0x..." });
const outcomePnl = await client.trader.getTraderOutcomePnl({ address: "0x..." });
const pnlCandles = await client.trader.getTraderPnlCandles({ address: "0x..." });
const calendar = await client.trader.getTraderPnlCalendar({ address: "0x..." });
const positionPnl = await client.trader.getTraderOutcomePnl({ address: "0x..." });
const pnlCalendar = await client.trader.getTraderPnlCalendar({ address: "0x..." });
const volumeChart = await client.trader.getTraderVolumeChart({ address: "0x..." });
const leaderboard = await client.trader.getGlobalPnl();
```
Expand All @@ -91,16 +89,20 @@ const history = await client.holders.getMarketHoldersHistory({ condition_id: "0x
const posHistory = await client.holders.getPositionHoldersHistory({ positionId: "123" });
```

### Series
### Order Book

```typescript
const series = await client.series.getSeriesList();
const outcomes = await client.series.getSeriesOutcomes({ series_slug: "my-series" });
const orderBook = await client.orderBook.getOrderBook({ asset_id: "0x..." });
const history = await client.orderBook.getOrderBookHistory();
const marketBook = await client.orderBook.getMarketOrderBook();
const spreads = await client.orderBook.getSpreadHistory();
```

### Assets, Search, Tags, Bonds
### Series, Search, Tags, Assets, Bonds

```typescript
const series = await client.series.getSeriesList();
const outcomes = await client.series.getSeriesOutcomes({ series_slug: "my-series" });
const assetHistory = await client.assets.getAssetHistory({ symbol: "BTC", variant: "1d" });
const results = await client.search.search({ query: "election" });
const tags = await client.tags.getTags();
Expand Down Expand Up @@ -147,7 +149,7 @@ Individual schemas are also exported: `OrderFilledTrade`, `RedemptionTrade`, `Me

### Webhooks

Manage webhook subscriptions for real-time event notifications. Webhook endpoints are platform-level (not venue-scoped).
Manage webhook subscriptions for real-time event notifications:

```typescript
const webhooks = await client.webhooks.list();
Expand All @@ -159,32 +161,129 @@ const webhook = await client.webhooks.create({
min_usd_value: 100,
},
});
const detail = await client.webhooks.getWebhook({ webhookId: webhook.data.id });
await client.webhooks.update({ webhookId: webhook.data.id, events: ["first_trade"] });
await client.webhooks.test({ webhookId: webhook.data.id });
await client.webhooks.rotateSecret({ webhookId: webhook.data.id });
await client.webhooks.deleteWebhook({ webhookId: webhook.data.id });
const events = await client.webhooks.listEvents();
```

#### Webhook Payload Types
## WebSocket API

The SDK exports typed payload schemas for building webhook receivers:
Real-time streaming via room-based subscriptions with fully typed filters, responses, and events.

```typescript
import type {
FirstTradePayload,
ProbabilitySpikePayload,
GlobalPnlPayload,
VolumeMilestonePayload,
} from "@structbuild/sdk";

function handleWebhook(payload: FirstTradePayload) {
console.log(payload.trader, payload.price, payload.side);
}
import { StructWebSocket } from "@structbuild/sdk";

const ws = new StructWebSocket({ apiKey: "your-api-key" });
await ws.connect();
```

### Subscribing to rooms

Each room has typed filters and a typed subscribe response:

```typescript
const res = await ws.subscribe("polymarket_trades", {
condition_ids: ["0xabc123"],
});

await ws.subscribe("polymarket_order_book", {
asset_ids: ["0xabc123"],
});

// Some rooms have optional filters
await ws.subscribe("polymarket_asset_prices");
await ws.subscribe("polymarket_clob_rewards", { subscribe_all: true });
```

### Listening for events

```typescript
ws.on("trade_stream_update", (event) => {
event.condition_id;
event.price;
event.size;
event.side;
});

ws.on("order_book_update", (event) => {
event.asset_id;
event.bids;
event.asks;
});

ws.on("clob_rewards_update", (event) => {
event.event_type; // "added" | "removed" | "updated"
event.condition_id;
event.reward;
});
```

Available payload types: `FirstTradePayload`, `GlobalPnlPayload`, `MarketPnlPayload`, `EventPnlPayload`, `ConditionMetricsPayload`, `EventMetricsPayload`, `PositionMetricsPayload`, `VolumeMilestonePayload`, `EventVolumeMilestonePayload`, `PositionVolumeMilestonePayload`, `ProbabilitySpikePayload`.
### Alerts

Alerts use a separate client with per-event typed filters and payloads:

```typescript
import { StructAlertsWebSocket } from "@structbuild/sdk";

const alerts = new StructAlertsWebSocket({ apiKey: "your-api-key" });
await alerts.connect();

await alerts.subscribe("trader_whale_trade", {
wallet_addresses: ["0xd91..."],
min_usd_value: 10000,
});

await alerts.subscribe("probability_spike", {
spike_direction: "up",
min_probability_change_pct: 5,
});

alerts.on("trader_whale_trade", (payload) => {
payload.data.trader;
payload.data.amount_usd;
});

alerts.on("probability_spike", (payload) => {
payload.data.spike_direction;
payload.data.spike_pct;
});
```

### Available rooms

| Room | Filters | Event |
|------|---------|-------|
| `polymarket_trades` | `condition_ids` | `trade_stream_update` |
| `polymarket_asset_prices` | `condition_ids?` | `asset_price_tick`, `asset_price_window_update` |
| `polymarket_asset_window_updates` | `condition_ids` | `asset_window_update` |
| `polymarket_market_metrics` | `condition_ids` | `market_metrics_update` |
| `polymarket_event_metrics` | `event_slugs` | `event_metrics_update` |
| `polymarket_position_metrics` | `position_ids` | `position_metrics_update` |
| `polymarket_trader_pnl` | `addresses` | `trader_global_pnl_update`, `trader_market_pnl_update`, `trader_event_pnl_update` |
| `polymarket_trader_positions` | `addresses` | `trader_position_update` |
| `polymarket_accounts` | `wallets` | `accounts_update`, `usdce_update`, `matic_update` |
| `polymarket_order_book` | `asset_ids` | `order_book_update` |
| `polymarket_clob_rewards` | `condition_ids?`, `subscribe_all?` | `clob_rewards_update` |

### Lifecycle events

```typescript
ws.on("connected", () => {});
ws.on("disconnected", ({ code, reason }) => {});
ws.on("reconnecting", ({ attempt }) => {});
ws.on("reconnect_failed", (err) => {});
ws.on("auth_failed", (err) => {});
ws.on("warning", (warning) => {});
ws.on("error", (err) => {});
```

### Cleanup

```typescript
ws.unsubscribe("polymarket_trades");
ws.disconnect();
```

## JWT Auth

Expand All @@ -208,6 +307,15 @@ const ws = new StructWebSocket({

The `pk_jwt_*` key is safe to hardcode in frontend bundles — it is useless without a valid JWT from your configured auth provider.

If your JWT can rotate while a socket stays alive, prefer `getJwt` so reconnects always rebuild the URL with a fresh token:

```typescript
const ws = new StructWebSocket({
apiKey: "pk_jwt_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
getJwt: () => userAccessToken,
});
```

## Pagination

Use the `paginate` helper to iterate through all results:
Expand Down
2 changes: 1 addition & 1 deletion openapi/polymarket.json

Large diffs are not rendered by default.

Loading
Loading