API reference
A resource-oriented HTTP API over JSON, plus a WebSocket surface for everything that changes. Predictable URLs, standard verbs, standard status codes, and one error envelope everywhere.
Overview
Every endpoint is served over TLS 1.3 from
https://api.strixhood.xyz. HTTP is refused, not redirected. Request and response bodies are
UTF-8 application/json unless an endpoint says otherwise.
Versioning
The path carries the major version (/v1); the Strix-Api-Version header
carries the dated minor version. Omitting the header pins you to the version that was current when
your API key was created, so existing integrations do not move under you.
curl -sS https://api.strixhood.xyz/v1/agents \
-H "Authorization: Bearer $STRIX_API_KEY" \
-H "Strix-Api-Version: 2026-07-01"| Version | Released | Breaking changes |
|---|---|---|
| 2026-07-01 | 2026-07-01 | intent.estimated replaces intent.preview; equity_order requires venue. |
| 2026-03-14 | 2026-03-14 | Cursor pagination replaces offset pagination on every list endpoint. |
| 2025-11-20 | 2025-11-20 | Initial public version. |
Environments
| Environment | Key prefix | Chains | Notes |
|---|---|---|---|
| Test | strx_sk_test_ | Base Sepolia, Arbitrum Sepolia, Solana devnet | No $STRX bond, faucet available, same schemas. |
| Live | strx_sk_live_ | All seven production networks | Real value. Bonds and fees apply. |
A test key against a live chain returns 403 environment_mismatch, and the reverse is
also true. There is no flag that lets one key straddle both.
Authentication
Bearer tokens on every request. There are no cookies, no sessions and no request signing for REST — signing happens onchain with session keys, not at the API boundary.
Bearer authentication
Authorization: Bearer strx_sk_live_9f2c41bd7a084e6cb35d0e17
Content-Type: application/json
Strix-Api-Version: 2026-07-01
Idempotency-Key: 6f1c0d9a-8b52-4a1e-9f77-2c3d4e5f6a7bA missing or malformed header returns 401 authentication_error. A well-formed key
that lacks the required scope returns 403 permission_error and names the scope it
wanted.
Scopes
Scopes are assigned at key creation and cannot be widened afterwards — create a new key instead. Every endpoint on this page states the scope it requires.
| Scope | Grants | Safe for an agent runtime |
|---|---|---|
| agents:read | Read agents, session-key metadata, reputation. | Yes |
| agents:write | Create, update, retire agents; issue and revoke session keys. | No |
| intents:read | Read intents and executions. | Yes |
| intents:write | Submit, cancel and approve intents. | Only with a tightly scoped policy |
| policies:read | Read policies and their commitments. | Yes |
| policies:write | Create and update policies. | Never — this is authority over authority. |
| portfolio:read | Balances, history, transactions. | Yes |
| webhooks:write | Manage webhook endpoints. | No |
| quotes:read | Quotes and prices. The only scope on pk_ keys. | Yes |
The blast radius of a leaked key is exactly its scope set. One key with everything is one compromise away from a rewritten policy.
Rotation and revocation
Keys support overlapping rotation: create the replacement, deploy it, then revoke the old key. Revocation is immediate and global — there is no propagation window. Keys unused for 90 consecutive days are automatically disabled and must be re-enabled from the console.
curl -sS -X DELETE https://api.strixhood.xyz/v1/api-keys/key_01JQ8ZV9R2T4W6Y8A0C2E4G6J8 \
-H "Authorization: Bearer $STRIX_ADMIN_KEY"IP allowlists
Secret keys accept an optional CIDR allowlist. Requests from outside it return
403 ip_not_allowed and are logged with the source address. Publishable
pk_ keys cannot be IP-restricted, because they are meant to be public.
Requests & responses
These rules hold for every endpoint. They are stated once here and not repeated per resource.
Idempotency
Every POST accepts an Idempotency-Key header. Replaying a key inside 24
hours returns the original response — same status, same body, plus
Idempotency-Replayed: true. Replaying a key with a different body returns
409 idempotency_conflict; the protocol will not guess which one you meant.
| Situation | Result |
|---|---|
| Same key, same body, inside 24 h | 200/202 with the original object and Idempotency-Replayed: true |
| Same key, different body | 409 idempotency_conflict |
| Same key, original request still in flight | 409 idempotency_in_progress, retry after Retry-After |
| Key older than 24 h | Treated as new |
Pagination
All list endpoints are cursor-paginated. Because object IDs are ULIDs they sort by creation time, so the cursor is just an ID.
| Parameter | Type | Default | Description |
|---|---|---|---|
| limit | integer | 25 | 1–100. |
| starting_after | string | null | Return objects created after this ID. Forward pagination. |
| ending_before | string | null | Return objects created before this ID. Backward pagination. |
| order | enum | desc | asc or desc by created_at. |
{
"object": "list",
"data": [ { "id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA", "object": "intent" } ],
"has_more": true,
"next_cursor": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"total_estimated": 1284
}total_estimated is exactly that — an estimate from the index, cheap to compute and
never used for correctness. Loop on has_more, not on a count.
Timestamps, amounts and identifiers
- Timestamps are RFC 3339 UTC strings with millisecond precision:
2026-08-16T09:00:00.412Z. Never Unix epochs, never local time. - Token amounts are decimal strings in human units, not integers in base
units:
"1500.000000"USDC, not1500000000. This avoids float truncation in JavaScript and ambiguity about decimals. - USD values are JSON numbers with at most 2 decimal places, and are always
estimates unless the field name ends in
_settled. - Basis points are integers:
50means 0.50%. - Identifiers are
prefix_ULID:agt_,int_,exe_,pol_,qte_,whk_,evt_,key_. 26-character Crockford base32, lexicographically sortable. - Chains are CAIP-2 strings:
eip155:8453.
Expanding objects
Related objects are returned as IDs by default. Request them inline with expand[],
up to three levels deep and four expansions per request.
curl -sS -G https://api.strixhood.xyz/v1/intents/int_01JQ8ZP1V6C3MD8R0YF2WKGSTA \
-H "Authorization: Bearer $STRIX_API_KEY" \
--data-urlencode "expand[]=execution" \
--data-urlencode "expand[]=execution.attestation" \
--data-urlencode "expand[]=agent.policy"Request IDs
Every response carries X-Request-Id. It is the only thing support needs to find the
full trace, including the policy evaluation and the simulation transcript. Log it.
Errors
One envelope, always. If a response has a status of 400 or above, this is its shape — there are no special cases.
The error envelope
{
"error": {
"type": "policy_error",
"code": "limit_exceeded",
"message": "Intent notional 780.00 USD exceeds per_tx_usd of 250.00 USD.",
"param": "params.sell_amount",
"rule": "limits.per_tx_usd",
"stage": "policy_check",
"doc_url": "https://strixhood.xyz/docs.html#policy-limits",
"request_id": "req_01JQ8ZW4T6V8X0Z2B4D6F8H0K2"
}
}| Field | Type | Description |
|---|---|---|
| type | enum | Coarse family. Branch on this. |
| code | string | Specific, stable machine code. Never reworded within a version. |
| message | string | Human sentence with concrete numbers. Safe to log, not safe to parse. |
| param | string | null | Dotted path to the offending request field. |
| rule | string | null | Dotted path to the policy clause that refused. Only on policy_error. |
| stage | string | null | Lifecycle stage that produced the failure. |
| request_id | string | Mirrors X-Request-Id. |
HTTP status codes
| Status | Meaning | Retry? |
|---|---|---|
| 200 | OK. | — |
| 201 | Created. Location points at the object. | — |
| 202 | Accepted. The intent is queued; watch the stream. | — |
| 204 | No content. Successful delete. | — |
| 400 | Malformed JSON or unknown field. | No — fix the request |
| 401 | Missing, malformed or revoked key. | No |
| 403 | Key valid, action not permitted. | No |
| 404 | No such object, or not visible to this key. | No |
| 409 | Idempotency conflict or state conflict. | No |
| 422 | Well-formed but refused: policy, simulation or business rule. | No |
| 429 | Rate limited. Honour Retry-After. | Yes, with backoff |
| 500 | Unhandled error on our side. Already alerting. | Yes |
| 503 | Dependency degraded — chain, bundler or relay. | Yes |
Error codes
| type | code | HTTP | Cause and fix |
|---|---|---|---|
| authentication_error | invalid_api_key | 401 | Key unknown, revoked or from the other environment. |
| authentication_error | environment_mismatch | 403 | Test key against a live chain, or the reverse. |
| permission_error | missing_scope | 403 | message names the scope. Mint a new key; scopes are immutable. |
| permission_error | ip_not_allowed | 403 | Source address outside the key's CIDR allowlist. |
| invalid_request_error | unknown_parameter | 400 | Unknown keys are rejected, never ignored. Check spelling and version. |
| invalid_request_error | missing_parameter | 400 | param names the field. |
| invalid_request_error | unresolvable_token | 400 | Symbol has no canonical address on that chain. Pass the address. |
| policy_error | policy_stale | 422 | Committed hash differs from the stored policy. Re-commit before retrying. |
| policy_error | chain_not_allowed | 422 | chain is absent from allow.chains. |
| policy_error | action_not_allowed | 422 | action is absent from allow.actions. |
| policy_error | asset_not_allowed | 422 | A token, collection, contract or venue is not allow-listed. |
| policy_error | denied | 422 | Matched a deny entry or category. |
| policy_error | limit_exceeded | 422 | rule names which ceiling. Wait for the window or raise the policy. |
| policy_error | approval_required | 202 | Not an error on submit — the intent is held at awaiting_approval. |
| simulation_error | simulation_failed | 422 | The candidate path reverts on a fork. message carries the revert reason. |
| simulation_error | price_impact_too_high | 422 | Exceeds simulation.max_price_impact_bps. Split the order. |
| simulation_error | unsafe_target | 422 | Drainer, approval-sweep or honeypot heuristic fired. Not overridable by intent. |
| routing_error | no_route | 422 | No solver and no direct route inside the slippage bound. |
| routing_error | quote_expired | 409 | Quote older than its expires_at. Request a fresh one. |
| settlement_error | insufficient_balance | 422 | Smart account cannot fund the sell leg plus gas. |
| settlement_error | reverted | 200 | Reported on the execution object, not as an HTTP failure. |
| idempotency_error | idempotency_conflict | 409 | Same key, different body. |
| rate_limit_error | too_many_requests | 429 | Honour Retry-After; use the WebSocket instead of polling. |
| api_error | internal_error | 500 | Retry with backoff. Include request_id if it persists. |
| api_error | dependency_degraded | 503 | Chain, bundler or relay is unhealthy. See the status page. |
Agents
An agent bundles a smart account, a bound policy, session keys and a reputation record. Creating one mints a passport NFT and locks a bond on live networks.
Create an agent
Deploys an ERC-4337 smart account with the session-key validator module, mints the passport, and
binds the policy. Idempotent on Idempotency-Key.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | required | 1–48 chars, unique per account. Appears in approval prompts and the marketplace. |
| kind | enum | required | trader, collector, treasury, service, verified. |
| policy_id | string | required | Existing policy to bind. Its hash is embedded in every session key issued. |
| chains | string[] | required | CAIP-2 list. Must be a subset of the policy's allow.chains. |
| session_key | object | optional | { ttl_seconds, rotate }. Issues a first key immediately. TTL 300–604800. |
| owner | address | optional | Root key that controls the smart account. Defaults to the account's registered owner. |
| metadata | object | optional | Up to 20 keys. |
Request
curl -sS https://api.strixhood.xyz/v1/agents \
-H "Authorization: Bearer $STRIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-dca-eth-01" \
-d '{
"name": "dca-eth",
"kind": "trader",
"policy_id": "pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD",
"chains": ["eip155:8453"],
"session_key": { "ttl_seconds": 86400, "rotate": true }
}'Response
{
"id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"object": "agent",
"name": "dca-eth",
"kind": "trader",
"status": "active",
"smart_account": "0x1F3c7A9b04E2d586Cf01B7e34a9D2c6058Ba9aE2",
"chains": ["eip155:8453"],
"policy": { "id": "pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD", "version": 1,
"hash": "0x7d41a9c0b83e5f2d16c4a87b90ef3524ca1d6b8f0472e93a5c18df6027ab4e91" },
"passport": { "chain": "eip155:8453", "contract": "0x2Fb1…6Ef2",
"token_id": "4182", "level": 1 },
"bond": { "amount": "2500", "token": "STRX", "status": "locked" },
"reputation": { "score": null, "settled_intents": 0 },
"session_keys": [ { "id": "key_01JQ8ZNB5C7E9G1J3L5N7Q9S1U",
"address": "0xA4e1…7C2b", "expires_at": "2026-08-17T09:00:00Z" } ],
"created_at": "2026-08-16T09:00:00.114Z"
}Errors
| code | HTTP | When |
|---|---|---|
| missing_parameter | 400 | policy_id absent. |
| policy_chain_mismatch | 422 | chains is not a subset of the policy's allowed chains. |
| insufficient_bond | 422 | Account holds less $STRX than the kind requires. |
| name_taken | 409 | Another active agent already uses that name. |
List agents
Cursor-paginated, newest first.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| status | enum | active, paused, retired, slashed. |
| kind | enum | Filter by agent kind. |
| chain | string | CAIP-2. Returns agents enabled on that chain. |
| policy_id | string | Agents bound to a specific policy. |
| limit, starting_after, ending_before, order | — | See Pagination. |
curl -sS -G https://api.strixhood.xyz/v1/agents \
-H "Authorization: Bearer $STRIX_API_KEY" \
-d status=active -d chain=eip155:8453 -d limit=25{
"object": "list",
"data": [
{ "id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP", "object": "agent", "name": "dca-eth",
"kind": "trader", "status": "active", "reputation": { "score": 96.4, "settled_intents": 812 } }
],
"has_more": false,
"next_cursor": null
}Retrieve an agent
Returns the full agent object. Supports expand[]=policy and
expand[]=passport.traits.
curl -sS -G https://api.strixhood.xyz/v1/agents/agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP \
-H "Authorization: Bearer $STRIX_API_KEY" \
--data-urlencode "expand[]=policy"Update an agent
Only name, status, policy_id, chains and
metadata are mutable. Rebinding policy_id revokes every live session
key in the same call, because the keys carry the old policy hash.
Body parameters
| Parameter | Type | Description |
|---|---|---|
| status | enum | active or paused. Pausing rejects new intents instantly; in-flight ones finish. |
| policy_id | string | Rebind. Triggers session-key revocation and a new commitment. |
| chains | string[] | Must remain a subset of the bound policy's chains. |
curl -sS -X PATCH https://api.strixhood.xyz/v1/agents/agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP \
-H "Authorization: Bearer $STRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"paused"}'Retire an agent
Retirement is a state, not a deletion: history, attestations and the passport survive. All
session keys are revoked onchain and the bond enters a 14-day unbonding period. Returns
409 has_open_intents if anything is still in flight.
{
"id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"object": "agent",
"status": "retired",
"session_keys_revoked": 2,
"bond": { "amount": "2500", "token": "STRX", "status": "unbonding",
"claimable_at": "2026-08-30T09:04:11Z" }
}Issue a session key
Generates a keypair inside the enclave, registers its permission blob on the agent's validator module, and returns the public address. The private key is never returned and never leaves the enclave — the API signs on the agent's behalf when an intent clears.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| ttl_seconds | integer | required | 300–604800. Shorter is better; rotation is free. |
| chains | string[] | optional | Defaults to the agent's chains. Cannot exceed them. |
| rotate | boolean | optional | Auto-issue a replacement at 80% of TTL. Default false. |
| max_value_usd | number | optional | Additional per-key ceiling, applied on top of the policy. Cannot be higher than the policy's per_tx_usd. |
{
"id": "key_01JQ8ZNB5C7E9G1J3L5N7Q9S1U",
"object": "session_key",
"agent_id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"address": "0xA4e1B9d3f70C2b84E651A0d7c3958Ff24bD07C2b",
"chains": ["eip155:8453"],
"policy_hash": "0x7d41a9c0b83e5f2d16c4a87b90ef3524ca1d6b8f0472e93a5c18df6027ab4e91",
"permissions": { "selectors": ["0x3593564c", "0xa9059cbb"], "max_value_usd": 250 },
"registration_tx": "0x4b18e0c9a2d7f5361840be9c07a2d5f31c68e40a95bd7213ce80f6a419d2c7b5",
"expires_at": "2026-08-17T09:00:00Z",
"rotate": true
}Revoke a session key
Submits an onchain revocation and refuses the key immediately at the API boundary. Returns
202 with the revocation transaction; the key is unusable via the API before that
transaction is mined.
{
"id": "key_01JQ8ZNB5C7E9G1J3L5N7Q9S1U",
"object": "session_key",
"status": "revoked",
"revocation_tx": "0x91cb47e2a05d8f3617b24ce09a7d51f38c60e24b95af7013dc80b6a41ed2f7c9",
"revoked_at": "2026-08-16T11:22:04.881Z"
}Intents
The intent object is documented field by field in the protocol reference. This section covers the endpoints that create and manage them.
Submit an intent
Accepts the intent, runs stages 01–03 synchronously and returns 202 as soon as the
policy check passes. Everything after that is asynchronous — watch the
executions channel.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| agent_id | string | required | Must be active with a live session key on chain. |
| action | enum | required | swap, transfer, nft_bid, nft_buy, equity_order, subscribe, agent_hire. |
| chain | string | required | CAIP-2. |
| params | object | required | Action-specific. See Action types. |
| constraints | object | optional | Slippage, fee ceilings, route preference, MEV protection. |
| simulate_only | boolean | optional | Return the quote and asset diff without signing. |
| expires_at | timestamp | optional | Default +300 s. |
| metadata | object | optional | Echoed on every webhook. |
Request
curl -sS https://api.strixhood.xyz/v1/intents \
-H "Authorization: Bearer $STRIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: dca-2026-08-16-0900" \
-d '{
"agent_id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"action": "swap",
"chain": "eip155:8453",
"params": { "sell_token": "USDC", "buy_token": "WETH", "sell_amount": "150.00" },
"constraints": { "max_slippage_bps": 40, "route_preference": "best_price" }
}'Response
{
"id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"object": "intent",
"status": "simulating",
"agent_id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"action": "swap",
"chain": "eip155:8453",
"params": { "sell_token": "USDC", "buy_token": "WETH", "sell_amount": "150.00" },
"constraints": { "max_slippage_bps": 40, "route_preference": "best_price",
"mev_protection": true, "partial_fill": false },
"policy": { "id": "pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD", "version": 1, "notional_usd": 150.00 },
"estimated": { "buy_amount": "0.04129", "price": "3632.10",
"fee_usd": 0.375, "gas_usd": 0.02, "price_impact_bps": 6 },
"execution_id": null,
"expires_at": "2026-08-16T09:05:00.412Z",
"created_at": "2026-08-16T09:00:00.412Z"
}Errors
| code | HTTP | When |
|---|---|---|
| limit_exceeded | 422 | Notional breaches a policy ceiling. rule names it. |
| asset_not_allowed | 422 | A token or venue in params is not allow-listed. |
| no_session_key | 422 | The agent has no live key for that chain. |
| unresolvable_token | 400 | Symbol has no canonical address on that chain. |
Retrieve an intent
Returns the current state. Use expand[]=execution to include quotes, fills and the
attestation in one call instead of two.
{
"id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"object": "intent",
"status": "settled",
"execution_id": "exe_01JQ8ZQ7Y9B1D3F5H7K9M1P3R5",
"settled": { "buy_amount": "0.041274", "price": "3634.02",
"fee_usd_settled": 0.375, "gas_usd_settled": 0.019 },
"rejection": null,
"created_at": "2026-08-16T09:00:00.412Z",
"settled_at": "2026-08-16T09:00:01.338Z"
}List intents
Query parameters
| Parameter | Type | Description |
|---|---|---|
| agent_id | string | Restrict to one agent. |
| status | enum | enum[] | Repeatable: status=routing&status=submitted. |
| action | enum | Filter by action. |
| chain | string | CAIP-2. |
| created_after | timestamp | Inclusive lower bound. |
| created_before | timestamp | Exclusive upper bound. |
| metadata[key] | string | Exact match on a metadata key, e.g. metadata[strategy]=weekly-rebalance. |
curl -sS -G https://api.strixhood.xyz/v1/intents \
-H "Authorization: Bearer $STRIX_API_KEY" \
-d agent_id=agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP \
-d status=settled -d limit=50 \
--data-urlencode "metadata[strategy]=weekly-rebalance"Cancel an intent
Cancellable up to and including routing. Once the user operation is broadcast the
intent is submitted and cancellation returns 409 not_cancellable —
there is no way to unsend a transaction, and the API will not pretend otherwise.
{
"id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"object": "intent",
"status": "cancelled",
"cancelled_at": "2026-08-16T09:00:00.902Z",
"budget_released_usd": 150.00
}Resolve a human gate
Resolves an intent sitting at awaiting_approval. The decision is recorded with the
approver's identity and is included in the attestation, so approvals are auditable after the
fact.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| decision | enum | required | approve or reject. |
| approver_id | string | required | Must appear in policy.hitl.approvers. |
| signature | string | recommended | EIP-191 signature over intent_id + decision + nonce. Required when quorum > 1. |
| note | string | optional | ≤ 280 chars, stored on the attestation. |
curl -sS https://api.strixhood.xyz/v1/intents/int_01JQ8ZP1V6C3MD8R0YF2WKGSTA/approval \
-H "Authorization: Bearer $STRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"decision":"approve","approver_id":"usr_01JQ8ZS2M4N6P8R0T2V4X6Z8B0","note":"Rebalance leg 1/3, reviewed"}'Policies
Policies are the authority layer. Writing one requires
policies:write, which should live on exactly one server process and nowhere near an
agent runtime.
Create a policy
Validates the document, canonicalises it, computes the hash and commits it onchain. Returns once
the commitment transaction is broadcast; commitment.block is null until
it is mined. Full field reference in
the policy schema.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | required | 1–64 chars, unique per account. |
| limits | object | required | Must include per_tx_usd. |
| allow | object | required | Must include non-empty chains and actions. |
| deny | object | optional | Wins over allow. |
| simulation | object | optional | Thresholds on the simulated asset diff. |
| hitl | object | optional | Escalation rules. Absent means never escalate. |
| expires_at | timestamp | recommended | Absent means a standing grant with no end. |
| commit_chain | string | optional | Where to commit the hash. Defaults to eip155:8453. |
{
"id": "pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD",
"object": "policy",
"name": "dca-conservative",
"version": 1,
"hash": "0x7d41a9c0b83e5f2d16c4a87b90ef3524ca1d6b8f0472e93a5c18df6027ab4e91",
"limits": { "per_tx_usd": 250, "daily_usd": 1000, "monthly_usd": 20000, "max_open_intents": 4 },
"allow": { "chains": ["eip155:8453"], "actions": ["swap", "transfer"],
"tokens": ["USDC", "WETH", "cbBTC"], "venues": ["uniswap_v4", "aerodrome"] },
"deny": { "categories": ["leverage", "gambling", "unverified_contract"] },
"hitl": { "threshold_usd": 200, "channels": ["webhook"], "timeout_sec": 180, "on_timeout": "reject" },
"agent_ids": [],
"commitment": { "chain": "eip155:8453",
"registry": "0x8Ae4…18Db",
"tx_hash": "0x2ad9…7f31", "block": null },
"expires_at": "2027-01-01T00:00:00Z",
"created_at": "2026-08-16T08:58:12.004Z"
}Retrieve a policy
Add ?version=N to read a historical version. Historical versions are immutable and
retained for seven years, because they are the evidence for why a past transaction was allowed.
curl -sS -G https://api.strixhood.xyz/v1/policies/pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD \
-H "Authorization: Bearer $STRIX_API_KEY" -d version=3List policies
Filters: status (live, expired,
superseded), agent_id, plus the standard pagination parameters.
Update a policy
Updates create a new version and a new commitment. Every session key issued under the previous
hash stops validating the moment the new commitment is mined, so bound agents must be re-keyed —
pass reissue_session_keys: true to have the API do it in the same call.
Between the commitment landing and the new keys registering, the agent cannot sign. It is
typically one block. Intents submitted in that window are held in policy_check
rather than rejected, up to their expires_at.
curl -sS -X PATCH https://api.strixhood.xyz/v1/policies/pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD \
-H "Authorization: Bearer $STRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"limits":{"per_tx_usd":250,"daily_usd":2500,"monthly_usd":20000,"max_open_intents":4},
"reissue_session_keys":true}'{
"id": "pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD",
"object": "policy",
"version": 2,
"hash": "0xb2f70c1d94a5e836027cf4a1b8d05e93762ac4108fd35b6e29c07a41db85e6f2",
"previous_hash": "0x7d41a9c0b83e5f2d16c4a87b90ef3524ca1d6b8f0472e93a5c18df6027ab4e91",
"commitment": { "tx_hash": "0x8e31…04ba", "block": null },
"session_keys_reissued": 2,
"updated_at": "2026-08-16T12:40:09.771Z"
}Simulate a policy
Evaluates a hypothetical intent against a policy without creating anything. Use it in CI: assert that the intents your agent is capable of producing are the intents your policy permits.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| intent | object | required | A full intent body minus agent_id. |
| as_of | timestamp | optional | Evaluate rolling windows as of this time. Defaults to now. |
| include_market | boolean | optional | Also run simulation thresholds against live prices. Default false. |
{
"object": "policy_simulation",
"allowed": false,
"policy": { "id": "pol_01JQ8ZK3M2X7YB4N6PA0RTVCWD", "version": 2 },
"notional_usd": 780.00,
"checks": [
{ "rule": "allow.chains", "result": "pass" },
{ "rule": "allow.actions", "result": "pass" },
{ "rule": "allow.tokens", "result": "pass" },
{ "rule": "deny.categories", "result": "pass" },
{ "rule": "limits.per_tx_usd", "result": "fail",
"detail": "780.00 > 250.00" },
{ "rule": "limits.daily_usd", "result": "skipped" }
],
"would_escalate": true,
"first_failure": "limits.per_tx_usd"
}Quotes & routing
A quote is a priced, expiring, non-binding preview. Submitting an intent runs its own auction; a quote is for showing a number to a human or a model before committing.
Request a quote
The only endpoint that accepts a publishable pk_ key, so a browser or agent runtime
can price something without holding a secret.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| action | enum | required | Same enum as intents. |
| chain | string | required | CAIP-2. |
| params | object | required | Action-specific. |
| agent_id | string | optional | Prices against that agent's policy and returns policy_ok. |
| route_preference | enum | optional | Default best_price. |
{
"id": "qte_01JQ8ZT2K4M6P8R0T2V4X6Z8B1",
"object": "quote",
"chain": "eip155:8453",
"sell": { "token": "USDC", "amount": "150.00" },
"buy": { "token": "WETH", "amount": "0.041293", "minimum": "0.041128" },
"price": "3632.10",
"price_impact_bps": 6,
"fees": { "protocol_usd": 0.375, "solver_usd": 0.09, "gas_usd": 0.02 },
"routes": [
{ "solver": "slv_kestrel", "venue": "uniswap_v4", "out": "0.041293", "score": 1.000 },
{ "solver": "slv_harrier", "venue": "aerodrome", "out": "0.041251", "score": 0.998 },
{ "solver": "direct", "venue": "uniswap_v4", "out": "0.041180", "score": 0.997 }
],
"policy_ok": true,
"expires_at": "2026-08-16T09:00:12.000Z"
}That is roughly one Base block plus margin. A quote older than its expires_at
cannot be attached to an intent — you get 409 quote_expired.
Retrieve a quote
Returns the quote as issued, including expired ones, for audit. It does not reprice.
List venues
Enumerates the venues reachable on a chain, their liquidity class and their session hours. Use
it to build a policy's allow.venues from something real instead of guessing.
{
"object": "list",
"data": [
{ "venue": "uniswap_v4", "chain": "eip155:8453", "kind": "amm",
"tvl_usd": 412000000, "session": null, "status": "live" },
{ "venue": "backed_rwa", "chain": "eip155:42161", "kind": "rwa_equity",
"tvl_usd": 88000000,
"session": { "opens_at": "2026-08-17T13:30:00Z", "closes_at": "2026-08-17T20:00:00Z",
"timezone": "UTC", "continuous_secondary": true },
"status": "live" }
],
"has_more": false
}Executions
An execution is created when an intent enters routing and carries everything that happened afterwards: the auction, the fills, the receipts and the attestation.
Retrieve an execution
{
"id": "exe_01JQ8ZQ7Y9B1D3F5H7K9M1P3R5",
"object": "execution",
"intent_id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"status": "settled",
"chain": "eip155:8453",
"solver": { "id": "slv_kestrel", "reliability": 0.9987, "bond_strx": "180000" },
"quote": { "id": "qte_01JQ8ZT2K4M6P8R0T2V4X6Z8B1",
"guaranteed_out": "0.041128", "bids_received": 3, "auction_ms": 176 },
"simulation": { "digest": "0x93af…21c7", "price_impact_bps": 6,
"asset_diff": [ { "token": "USDC", "delta": "-150.000000" },
{ "token": "WETH", "delta": "+0.041274" } ],
"warnings": [] },
"fills": [ { "buy_amount": "0.041274", "sell_amount": "150.000000", "price": "3634.02",
"tx_hash": "0x7c02e91a4f5b8d3607a2c14be9350df82461ac09b7de52318ca06f4b19e7d3a2",
"block": 24817552, "gas_usd": 0.019 } ],
"fees": { "protocol_usd": 0.375, "solver_usd": 0.09, "gas_usd": 0.019,
"split": { "treasury_usd": 0.15, "stakers_usd": 0.1125, "buyback_usd": 0.1125 } },
"attestation": { "uid": "0x5ea1…9d40", "schema": "strixhood.settlement.v1",
"chain": "eip155:8453", "explorer_url": "https://base.easscan.org/attestation/view/0x5ea1…9d40" },
"timeline": [
{ "status": "routing", "at": "2026-08-16T09:00:00.598Z" },
{ "status": "submitted", "at": "2026-08-16T09:00:00.774Z" },
{ "status": "settled", "at": "2026-08-16T09:00:01.338Z" }
]
}List executions
Filters: agent_id, status, chain, solver,
settled_after, settled_before, plus standard pagination. Set
format=csv to stream a CSV for accounting instead of JSON.
curl -sS -G https://api.strixhood.xyz/v1/executions \
-H "Authorization: Bearer $STRIX_API_KEY" \
-d format=csv -d settled_after=2026-07-01T00:00:00Z -d settled_before=2026-08-01T00:00:00Z \
-o july-executions.csvRetrieve an attestation
Returns the decoded attestation plus the raw ABI-encoded payload, so you can verify it against the EAS contract yourself rather than trusting this API.
{
"uid": "0x5ea1c73b0428f96d15a0c8e4712bd936084fa5c2e1739bd60c48af2107e59d40",
"object": "attestation",
"schema": "strixhood.settlement.v1",
"attester": "0x9D07…0d75",
"recipient": "0x1F3c7A9b04E2d586Cf01B7e34a9D2c6058Ba9aE2",
"revocable": false,
"data": {
"intent_hash": "0xc41d…8a02",
"policy_hash": "0x7d41a9c0b83e5f2d16c4a87b90ef3524ca1d6b8f0472e93a5c18df6027ab4e91",
"policy_version": 1,
"simulation_digest": "0x93af…21c7",
"solver": "slv_kestrel",
"settled_out": "41274000000000000",
"approvals": []
},
"raw": "0x0000000000000000000000000000000000000000000000000000000000000020…",
"verify_url": "https://base.easscan.org/attestation/view/0x5ea1…9d40"
}Portfolio
Read-only views over an agent's smart account: balances, valuation, history and the transaction ledger. Prices come from the same oracle set the policy engine uses, so a portfolio number and a limit calculation never disagree.
Retrieve portfolio
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| agent_id | string | required | Which agent's account to value. |
| chains | string[] | optional | Defaults to all of the agent's chains. |
| include | enum[] | optional | tokens, nfts, equities, positions. Default all. |
| min_value_usd | number | optional | Hide dust. Default 1.00. |
{
"object": "portfolio",
"agent_id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"total_value_usd": 48213.77,
"change_24h_pct": 1.84,
"as_of": "2026-08-16T09:04:00.000Z",
"tokens": [
{ "symbol": "WETH", "chain": "eip155:8453", "balance": "9.418200",
"price_usd": 3634.02, "value_usd": 34226.31, "allocation_pct": 71.0 },
{ "symbol": "USDC", "chain": "eip155:8453", "balance": "9412.400000",
"price_usd": 1.0, "value_usd": 9412.40, "allocation_pct": 19.5 }
],
"equities": [
{ "symbol": "AAPLX", "chain": "eip155:42161", "quantity": "19.204000",
"price_usd": 231.40, "value_usd": 4443.80, "venue": "backed_rwa" }
],
"nfts": [
{ "collection": "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8", "token_id": "8842",
"floor_price_eth": "3.62", "value_usd": 131.26 }
],
"unrealised_pnl_usd": 2914.08
}Portfolio history
Time series of total value. interval accepts 5m, 1h,
1d; range accepts 24h, 7d,
30d, 1y, max. Points are snapshots at interval close,
not interpolations.
{
"object": "portfolio_history",
"agent_id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"interval": "1h",
"range": "24h",
"points": [
{ "t": "2026-08-15T10:00:00Z", "value_usd": 47338.10 },
{ "t": "2026-08-15T11:00:00Z", "value_usd": 47510.62 },
{ "t": "2026-08-16T09:00:00Z", "value_usd": 48213.77 }
]
}List transactions
Every value movement in or out of the account, including ones not originated by an intent —
deposits, airdrops, third-party transfers. Cursor-paginated, format=csv supported.
{
"object": "list",
"data": [
{ "id": "txn_01JQ8ZV1C3E5G7J9L1N3Q5S7U9", "direction": "in", "kind": "settlement",
"token": "WETH", "amount": "0.041274", "value_usd": 150.00,
"intent_id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"tx_hash": "0x7c02e91a4f5b8d3607a2c14be9350df82461ac09b7de52318ca06f4b19e7d3a2",
"at": "2026-08-16T09:00:01.338Z" },
{ "id": "txn_01JQ8ZU9A1C3E5G7J9L1N3Q5S7", "direction": "in", "kind": "external_deposit",
"token": "USDC", "amount": "5000.000000", "value_usd": 5000.00,
"intent_id": null, "at": "2026-08-14T17:22:40.000Z" }
],
"has_more": true,
"next_cursor": "txn_01JQ8ZU9A1C3E5G7J9L1N3Q5S7"
}Webhooks
Webhooks are the durable channel: signed, retried and replayable. The WebSocket is the fast channel. Production systems use both — the socket to react, the webhook to be certain.
Create an endpoint
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | required | HTTPS only. Must answer a POST probe with 2xx inside 5 s before the endpoint activates. |
| events | string[] | required | Event types, or ["*"]. Unknown types are rejected. |
| agent_ids | string[] | optional | Restrict to specific agents. Default all. |
| description | string | optional | ≤ 120 chars, shown in the console. |
{
"id": "whk_01JQ8ZW7E9G1J3L5N7Q9S1U3W5",
"object": "webhook_endpoint",
"url": "https://ops.example.com/hooks/strix",
"events": ["intent.rejected", "approval.requested", "execution.settled", "execution.failed"],
"status": "active",
"signing_secret": "whsec_2f8c1a04e75b39d6c0182e4a7f31b9d5",
"created_at": "2026-08-16T09:10:22.500Z"
}It is not retrievable afterwards. Store it in your secret manager immediately, or roll the endpoint and get a new one.
List endpoints
Returns endpoints with delivery health: success_rate_24h,
last_delivery_at, consecutive_failures. An endpoint that fails 20
consecutive deliveries is disabled and an webhook.disabled event is emitted to the
remaining healthy endpoints.
Delete an endpoint
Returns 204. Deliveries already queued are dropped; nothing is redelivered
afterwards.
Event types
| Event | Fires when | Payload data |
|---|---|---|
| intent.created | An intent is accepted at stage 01. | intent |
| intent.rejected | Any of stages 02–04 refuses it. | intent with rejection |
| intent.expired | expires_at passes without a fill. | intent |
| intent.cancelled | Cancelled by the owner. | intent |
| approval.requested | The human gate opens. Carries the full asset diff. | intent + simulation |
| approval.resolved | Approved, rejected or timed out. | intent + decision |
| execution.submitted | The user operation is broadcast. | execution |
| execution.settled | Included, fee split, attestation written. | execution + attestation |
| execution.failed | Reverted onchain or the solver defaulted. | execution with failure |
| execution.reverted | A settled execution was undone by a deep reorg. | execution |
| policy.updated | A new policy version is committed. | policy |
| agent.slashed | A slashing claim executes against the bond. | agent + slash |
| session_key.rotated | Auto-rotation issues a replacement key. | session_key |
| webhook.disabled | An endpoint is disabled after 20 consecutive failures. | webhook_endpoint |
{
"id": "evt_01JQ8ZX3G5J7L9N1Q3S5U7W9Y1",
"object": "event",
"type": "execution.settled",
"api_version": "2026-07-01",
"created_at": "2026-08-16T09:00:01.402Z",
"livemode": true,
"data": {
"object": {
"id": "exe_01JQ8ZQ7Y9B1D3F5H7K9M1P3R5",
"object": "execution",
"intent_id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"status": "settled",
"fills": [ { "buy_amount": "0.041274", "price": "3634.02" } ],
"attestation": { "uid": "0x5ea1…9d40" }
}
},
"attempt": 1
}Signature verification
Every delivery carries Strix-Signature: a timestamp and one or more HMAC-SHA256
signatures over timestamp + "." + raw_body, keyed with the endpoint's signing secret.
Multiple v1= values appear during a secret roll — accept the request if any
of them verifies.
POST /hooks/strix HTTP/1.1
Content-Type: application/json
Strix-Signature: t=1786953601,v1=6a1f0c4b8d29e7350a1c8f6b2e94d075c3a8b1f60e29d47a5c30b8e1f27a94d6
Strix-Event-Id: evt_01JQ8ZX3G5J7L9N1Q3S5U7W9Y1
Strix-Delivery-Attempt: 1import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const ts = Number(parts.t);
if (!Number.isFinite(ts)) return false;
// Reject replays outside the tolerance window.
if (Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest();
return header
.split(",")
.filter((kv) => kv.startsWith("v1="))
.some((kv) => {
const given = Buffer.from(kv.slice(3), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}import hmac, hashlib, time
TOLERANCE_SECONDS = 300
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
try:
ts = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - ts) > TOLERANCE_SECONDS:
return False
signed = f"{ts}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
given = [kv.split("=", 1)[1] for kv in header.split(",") if kv.startswith("v1=")]
return any(hmac.compare_digest(g, expected) for g in given)use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};
const TOLERANCE_SECONDS: i64 = 300;
pub fn verify(raw_body: &[u8], header: &str, secret: &[u8]) -> bool {
let mut ts: i64 = 0;
let mut sigs: Vec<&str> = Vec::new();
for kv in header.split(',') {
match kv.split_once('=') {
Some(("t", v)) => ts = v.parse().unwrap_or(0),
Some(("v1", v)) => sigs.push(v),
_ => {}
}
}
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
if ts == 0 || (now - ts).abs() > TOLERANCE_SECONDS {
return false;
}
let mut mac = Hmac::<Sha256>::new_from_slice(secret).expect("key");
mac.update(format!("{ts}.").as_bytes());
mac.update(raw_body);
let expected = hex::encode(mac.finalize().into_bytes());
sigs.iter().any(|s| constant_time_eq(s.as_bytes(), expected.as_bytes()))
}Sign-check before parsing. Re-serialising the JSON changes key order and whitespace, and the signature will never match. Most webhook bugs are this bug.
Delivery, retries and ordering
- Timeout — 5 seconds to respond. Return
2xximmediately and do the work asynchronously. - Retries — 8 attempts over 24 hours with exponential backoff: 10 s, 30 s, 2 m, 10 m, 30 m, 2 h, 6 h, 12 h.
- Ordering is not guaranteed. Use
created_atand the intent status machine to order events yourself; a retriedexecution.submittedcan arrive afterexecution.settled. - At-least-once. Deduplicate on
Strix-Event-Id. The same event can arrive twice. - Replay —
POST /v1/webhooks/{id}/replaywith an event ID or a time range re-sends past events for backfill.
WebSocket API
One connection, many channels. Frames do not consume the REST rate-limit budget, which makes the socket the correct way to follow intents rather than polling.
Connect and authenticate
Authenticate with the first frame within 5 seconds of the handshake, or the socket closes with
code 4001. Query-string keys are not accepted — they end up in proxy logs.
{ "op": "auth", "token": "strx_sk_live_9f2c41bd7a084e6cb35d0e17", "id": "c1" }{ "op": "auth.ok", "id": "c1", "account": "acct_01JQ8Z…", "livemode": true,
"heartbeat_sec": 20, "channels": ["intents", "executions", "prices"] }wscat -c wss://stream.strixhood.xyz/v1 \
-x '{"op":"auth","token":"'"$STRIX_API_KEY"'","id":"c1"}'Frame format
Every frame is a JSON object with an op. Client frames may carry an
id, which is echoed on the matching acknowledgement so you can correlate.
| op | Direction | Meaning |
|---|---|---|
| auth | client → server | Authenticate the connection. |
| subscribe | client → server | Join a channel with optional filters. |
| unsubscribe | client → server | Leave a channel. |
| ping | client → server | Application-level keepalive. |
| auth.ok / subscribed / unsubscribed / pong | server → client | Acknowledgements, echoing id. |
| event | server → client | A channel payload. |
| error | server → client | Same envelope as REST, plus the offending id. |
// join two channels at once
{ "op": "subscribe", "id": "s1",
"channels": [
{ "name": "executions", "agent_ids": ["agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP"] },
{ "name": "prices", "symbols": ["ETH", "AAPLX"], "chain": "eip155:8453" }
] }
// server acknowledges
{ "op": "subscribed", "id": "s1", "channels": ["executions", "prices"] }
// leave one
{ "op": "unsubscribe", "id": "s2", "channels": ["prices"] }
{ "op": "unsubscribed", "id": "s2", "channels": ["prices"] }Channel: intents
Status transitions for every intent visible to the key. Filters: agent_ids,
statuses, chains. One frame per transition, never a full re-send.
{
"op": "event",
"channel": "intents",
"seq": 88214,
"at": "2026-08-16T09:00:00.598Z",
"data": {
"id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"object": "intent",
"status": "routing",
"previous_status": "simulating",
"agent_id": "agt_01JQ8ZM7T4B0KC2N9XE5RVDHQP",
"execution_id": "exe_01JQ8ZQ7Y9B1D3F5H7K9M1P3R5"
}
}Channel: executions
Auction results, fills and settlement. Filters: agent_ids, chains,
solvers. This is the channel to drive a UI from.
{
"op": "event",
"channel": "executions",
"seq": 88217,
"at": "2026-08-16T09:00:01.338Z",
"data": {
"id": "exe_01JQ8ZQ7Y9B1D3F5H7K9M1P3R5",
"object": "execution",
"intent_id": "int_01JQ8ZP1V6C3MD8R0YF2WKGSTA",
"status": "settled",
"solver": "slv_kestrel",
"fills": [ { "buy_amount": "0.041274", "price": "3634.02",
"tx_hash": "0x7c02e91a4f5b8d3607a2c14be9350df82461ac09b7de52318ca06f4b19e7d3a2" } ],
"attestation": { "uid": "0x5ea1…9d40" }
}
}Channel: prices
The same oracle prices the policy engine uses for notional calculation, so a client-side limit preview matches the server's decision. Throttled to 4 updates per second per symbol; subscribe to at most 50 symbols per connection.
{
"op": "event",
"channel": "prices",
"seq": 88219,
"at": "2026-08-16T09:00:01.500Z",
"data": {
"symbol": "ETH",
"chain": "eip155:8453",
"price_usd": "3634.02",
"change_24h_pct": 1.84,
"sources": 5,
"staleness_ms": 380
}
}staleness_ms is not decoration
If it exceeds 5,000 the oracle set is degraded and the policy engine widens its own tolerance. Do not size an order off a stale price.
Heartbeats and reconnect
The server sends ping every heartbeat_sec. Miss two and the socket
closes. Every event carries a monotonic seq per channel; reconnect with
resume_from to replay the gap from a 15-minute buffer.
{ "op": "subscribe", "id": "s3",
"channels": [ { "name": "executions", "resume_from": 88217 } ] }| Close code | Meaning | What to do |
|---|---|---|
| 1000 | Normal closure. | Nothing. |
| 4001 | Authentication timeout or failure. | Fix the key. Do not reconnect in a loop. |
| 4003 | Scope missing for a requested channel. | Mint a key with the scope. |
| 4008 | Too many connections for the tier. | Multiplex channels onto one socket. |
| 4009 | Heartbeat missed. | Reconnect with resume_from. |
| 4029 | Subscription flood — more than 20 subscribe ops per minute. | Back off 60 s. |
| 1012 | Server restarting for a deploy. | Reconnect after a jittered 1–5 s. |
function connect(url: string, token: string, onEvent: (e: unknown) => void) {
let seq = 0;
let backoff = 500;
const open = () => {
const ws = new WebSocket(url);
ws.onopen = () => ws.send(JSON.stringify({ op: "auth", token, id: "c1" }));
ws.onmessage = (m) => {
const f = JSON.parse(m.data as string);
if (f.op === "auth.ok") {
backoff = 500;
ws.send(JSON.stringify({
op: "subscribe", id: "s1",
channels: [{ name: "executions", resume_from: seq || undefined }],
}));
} else if (f.op === "event") {
seq = f.seq;
onEvent(f.data);
} else if (f.op === "ping") {
ws.send(JSON.stringify({ op: "pong", id: f.id }));
}
};
ws.onclose = (e) => {
if (e.code === 4001 || e.code === 4003) return; // fatal: do not retry
setTimeout(open, Math.random() * backoff);
backoff = Math.min(backoff * 2, 30_000);
};
};
open();
}