@openmarketsai/connect-node
v0.7.0
Published
Server SDK for OpenMarkets Connect — provision users, mint link sessions, verify webhooks, and execute on a user's behalf.
Maintainers
Readme
@openmarketsai/connect-node
Server SDK for OpenMarkets Connect. A thin, typed wrapper over the Flow API:
provision your users, mint hosted link sessions, verify webhook deliveries, read
on a user's behalf, and execute orders on their account. Pairs with
@openmarketsai/connect-web (the in-app launcher).
npm install @openmarketsai/connect-nodeNode 18+ (uses global fetch and node:crypto). No runtime dependencies.
Quickstart
import { createClient } from '@openmarketsai/connect-node'
const om = createClient({
apiKey: process.env.OM_API_KEY!, // your Connect org key
webhookSecret: process.env.OM_WHSEC, // optional, enables om.connect.webhooks.*
})
// 1. Provision a user (idempotent)
await om.connect.users.provision({ external_user_id: 'user-123', email: '[email protected]' })
// 2. Mint a hosted link token → hand to @openmarketsai/connect-web on the client
const { token } = await om.connect.linkSessions.create({
external_user_id: 'user-123',
return_url: 'https://yourapp.com/connected',
})
// 4. Read prices, 5. execute on the user's behalf
const liquidity = await om.data.contestLiquidity(contestId)
await om.orders.buy(
// omit partner_id to route to the best venue; pass mode:'paper' for practice
[{ position_hash, amount: 50, max_price: 0.42 }],
{ actAs: 'user-123' },
)
// balances / positions, all on the user's behalf
await om.account.balances('user-123')Buy legs use the same field names as the liquidity/quote entries, so you can pass them through with minimal mapping.
Webhooks (3)
Verify deliveries over the raw request body (don't re-serialize):
import express from 'express'
const app = express()
app.post('/webhooks/openmarkets', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = om.connect.webhooks.constructEvent(
req.body, // Buffer (raw)
req.header('X-OpenMarkets-Signature'),
)
if (event.type === 'user.partners_connected') { /* … */ }
res.sendStatus(200)
} catch {
res.sendStatus(400) // bad signature / stale / malformed
}
})Or use the standalone helpers without a client:
import { verifyWebhookSignature, constructWebhookEvent } from '@openmarketsai/connect-node'
verifyWebhookSignature(secret, rawBody, sigHeader) // booleanSignature scheme: header X-OpenMarkets-Signature: t=<unix_seconds>,v1=<hmac>,
where the HMAC-SHA256 is over `${t}.${rawBody}` with your whsec_ secret;
deliveries older than 5 minutes are rejected.
API
| Method | Endpoint |
|---|---|
| connect.users.provision(params) | POST /flow/v1/connect/users |
| connect.users.get(id) | GET /flow/v1/connect/users/:id |
| connect.users.getPolicy(id) | GET /flow/v1/connect/users/:id/policy |
| connect.linkSessions.create(params) | POST /flow/v1/connect/link-sessions |
| connect.linkSessions.get(id) | GET /flow/v1/connect/link-sessions/:id |
| connect.webhooks.getConfig() | GET /flow/v1/connect/webhook-config |
| connect.webhooks.verify / constructEvent | (local, no network) |
| account.partners(id) / balances(id) | GET /flow/v1/auth/account/{partners,balances} (act-as) |
| orders.list(id) | GET /flow/v1/auth/orders (act-as) |
| orders.buy(orders, { actAs }) | POST /flow/v1/auth/orders/buy (act-as) |
| data.leagues() / contestLiquidity(id) | GET /flow/v1/leagues, …/contests/:id/liquidity |
| connect.configs.list / create / patch / setDefault / archive | named hosted flows under /flow/v1/workspaces/:id/connect-configs (player JWT, workspace admin) |
| ai.invoke(params, { actAs }) | POST /flow/v1/ai/inferences + polls GET …/:id |
| ai.inferences.create / get | the same, one request at a time |
| ai.providers({ actAs }) | GET /flow/v1/ai/providers |
| ai.models() | GET /flow/v1/ai/models — the managed-billing catalog |
| request(method, path, opts) | escape hatch (opts.actAs sets the header, opts.idempotencyKey sends Idempotency-Key) |
| requestPage(path, opts) / requestAll(path, opts) | list endpoints with their next_cursor — one page, or every page |
AI Connect — run models on your user's own key
Turn it on for your org with ai_providers: ['anthropic', 'openai'] in your Connect
config. Your users then see an AI models section in the hosted flow, where they
paste their own Anthropic or OpenAI API key. You never see the key.
const r = await om.ai.invoke<{ pick: 'home' | 'away'; confidence: number }>({
provider: 'anthropic',
model: 'claude-opus-5',
system: 'You pick sides. Answer only in the schema.',
messages: [{ role: 'user', content: JSON.stringify(snapshot) }],
output_schema: { schema: PICK_SCHEMA },
}, { actAs: 'user-123' })
if (r.stop_reason === 'refusal') { /* not an answer */ }
r.output?.pick // parsed, schema-shaped
r.billing // 'byok' — calls on the user's key are NEVER charged by OpenMarketsbilling defaults to 'auto': the user's own key when they linked and shared it,
otherwise OpenMarkets-hosted models charged in reasoning credits. Send
billing: 'byok' to guarantee a call is never charged (it fails with
409 ai_provider_not_linked instead of falling back).
Long calls return 202 server-side; invoke polls for you and throws
AiInferenceFailedError if the call fails.
Add web_search: true (up to 5 searches) or web_search: { max_uses: 3 } to let
the model search the web first; the result carries web_search.requests and the
sources it read. ai.models() lists what managed billing runs and whether your
plan allows each model — check it before offering a model, rather than learning
from a failed inference.
Errors
Non-2xx responses throw OpenMarketsError with status, code, message,
and details from the Flow error envelope:
import { OpenMarketsError } from '@openmarketsai/connect-node'
try {
await om.orders.buy(orders, { actAs: 'user-123' })
} catch (e) {
if (e instanceof OpenMarketsError && e.code === 'execution_limit_exceeded') { /* … */ }
}Options
| Option | Default | Notes |
|---|---|---|
| apiKey | — | Required. Sent as X-API-Key. |
| baseUrl | https://api.openmarkets.ai | Override for staging. |
| webhookSecret | — | Enables connect.webhooks.verify/constructEvent. |
| fetch | global fetch | Inject for Node < 18 or testing. |
| timeoutMs | 30000 | Per-request timeout. |
