OIB/1 / PUBLIC INTEGRATION REFERENCE

Broadcast API

IntentScan's OIB/1 channel broadcasts off-chain intent events to filler and solver subscribers. AgentSwap front ends can publish native orders to the same channel before an on-chain announcement. The stream is an optimization, so clients should keep their own on-chain or upstream completeness floor and deduplicate on id.

01 / CONNECTION

Base URL and routes

https://broadcast.intentscan.net

The base URL returns a JSON descriptor for OIB/1 and the service route set. The six integration routes are:

{"protocol":"OIB/1","service":"intentscan-stream","routes":{"publish":"POST /v1/stream/publish","cursor":"GET /v1/stream/intents?since=<seq>","open_book":"GET /v1/stream/open","websocket":"GET /v1/stream/ws","sse":"GET /v1/stream/sse","health":"GET /v1/health"}}
MethodPathUse
POST/v1/stream/publishAdmit and publish a native AgentSwap order
GET/v1/stream/openRead the current open book
GET/v1/stream/intents?since=<seq>Read events after a sequence cursor
GET/v1/stream/wsSubscribe over WebSocket
GET/v1/stream/sseSubscribe over Server-Sent Events
GET/v1/healthCheck service and on-chain source health

02 / WIRE FORMAT

The envelope

WebSocket, SSE, cursor, and open-book intent events use the same envelope. Values below are abbreviated for readability. fill is the bearer authorization and settlement payload; the relay cannot forge it. summary is advisory display and filter data only, and must never reach pricing or execution.

{
  "event": "intent_open",
  "seq": 918273,
  "stream_epoch": "01J8Z6R3M4...",
  "data": {
    "id": "8453:agentswap:0xab10...",
    "origin": "native",
    "platform": "agentswap",
    "chain_id": 8453,
    "order_hash": "0xab10...",
    "fillable": "permissionless",
    "exclusive_until_ms": null,
    "fill": {"standard": "agentswap-v5.1", "settler": "0xC0b66...", "order": "0x...", "signature": "0x...", "resolver": null},
    "summary": {
      "input": {"token": "0x833589...", "amount": "1000000", "decimals": null, "symbol": null},
      "output": {"token": "0x420000...", "min_amount": "900000000000000", "decimals": null, "symbol": null},
      "created_ms": 1756800000000, "decay_end_ms": 1756800030000, "deadline_ms": 1756800300000
    },
    "observed_ms": 1756800000123,
    "source": "push:native"
  }
}

Native publishing currently fills summary.input.decimals, summary.input.symbol, summary.output.decimals, and summary.output.symbol as null.

03 / PRODUCE

Publish

Send a JSON object with exactly these request fields. order is ABI-encoded IntentOrder; its fields are owner, recipient, tokenIn, amountIn, tokenOut, startAmountOut, endAmountOut, startTime, decayEndTime, endTime, appData, and nonce. auth is the authorization bytes. chainId is optional.

{"order":"0x...","auth":"0x...","chainId":8453}

Production serves chain IDs 8453, 42161, 56, and 4663. When chainId is omitted, admission starts with 8453, the default, then searches the remaining served chains in order. Admission includes an eth_call to UserProxyV5.isIntentAuthorized.

Responses

StatusMeaning
201New order. The body is the published envelope.
200Already published. The body is empty.
422Request or admission verdict: invalid order: <reason>; chain <id> is not served; order has expired; order is cancelled; order is not authorized; admission rejected: <reason>; owner proxy is not deployed; or owner admission rate limit exceeded.
500Service or admission failure: native publish has no served chains configured; admission RPC failed: <reason>; or admission metrics unavailable.
curl -X POST https://broadcast.intentscan.net/v1/stream/publish \
  -H 'content-type: application/json' \
  --data '{"order":"0x...","auth":"0x...","chainId":8453}'

04 / SNAPSHOT

Open book

GET /v1/stream/open returns the current open intents and the sequence at which the snapshot was read.

{"seq":918273,"events":[]}

Each item in events is an intent envelope. On a cold start, read this book first, then subscribe with its seq as since.

05 / CATCH-UP

Cursor feed

GET /v1/stream/intents?since=<seq> returns events after the required numeric cursor.

{
  "stream_epoch": "01J8Z6R3M4...",
  "current_seq": 918280,
  "gap": null,
  "events": []
}

gap is either null or an object with last_seq. Use the returned stream_epoch with your stored cursor. A cursor gap means the requested history is not fully available.

06 / LIVE

WebSocket

Connect to wss://broadcast.intentscan.net/v1/stream/ws and send a text JSON subscribe frame first. since is optional and replays available history before live frames.

{"op":"subscribe","filters":{"chain_ids":[8453],"fillable":["permissionless"]},"since":918273}

Filters

FieldTypeMatch
chain_idsarray of numbersExact chain_id
platformsarray of stringsExact platform
fillablearraypermissionless, exclusive, restricted, or informational
min_size_usdnumberMinimum input amount divided by 10^decimals. When summary.input.decimals is null, as it is for every natively published order today, the amount normalizes to 0, so any positive minimum excludes those events.
tokensarray of stringsInput or output token address, case-insensitive

Frame kinds

Every frame has event, seq, stream_epoch, and data.

eventdata
subscribed{"filters":{...}}
intent_open, intent_filled, intent_cancelled, intent_expiredIntent data
heartbeat{}
gap{"last_seq":...}
const BASE = "https://broadcast.intentscan.net";
const socket = new WebSocket("wss://broadcast.intentscan.net/v1/stream/ws");
socket.addEventListener("open", () => socket.send(JSON.stringify({
  op: "subscribe",
  filters: { chain_ids: [8453], fillable: ["permissionless"] },
  since: 918273,
})));
socket.addEventListener("message", ({ data }) => {
  const frame = JSON.parse(data);
  console.log(frame.event, frame.data);
  if (frame.event !== "gap") return;
  fetch(`${BASE}/v1/stream/intents?since=${frame.data.last_seq}`)
    .then((response) => response.json())
    .then(console.log);
});

07 / BROWSER

SSE

Connect to GET /v1/stream/sse. Every parameter is optional. since replays available history first. The filters are the WebSocket filters as query parameters, with list values comma-separated: chain_ids, platforms, fillable, tokens, and min_size_usd. A list item that does not parse, such as a non-numeric chain id or an unknown fillable value, answers 400 with a message naming the field. Each SSE data field carries the same JSON frame shape used by WebSocket.

const events = new EventSource(
  "https://broadcast.intentscan.net/v1/stream/sse?since=918273&chain_ids=8453,42161&fillable=permissionless"
);
events.onmessage = ({ data }) => {
  const frame = JSON.parse(data);
  console.log(frame.event, frame.data);
};

08 / CLIENT CONTRACT

Client protocol rules

  • Cold start: read /v1/stream/open, then subscribe with its seq in since.
  • Gap: keep the connection open and backfill with /v1/stream/intents?since=<last_seq>. A gap is not a reconnect signal.
  • Epoch: a changed stream_epoch means a full re-sync.
  • Heartbeat: expect one every 20 seconds and reconnect after 3 missed heartbeats.
  • Dual path: also run your own eth_getLogs floor or upstream poll, and deduplicate on id. This stream is an optimization, never the source of truth.
  • Pricing: treat summary as advisory. Decode and verify fill before pricing or execution.

09 / EDGE POLICY

Limits

ScopeLimit
Reads per IP20 requests/second for the open book and cursor feed, with burst 40 and no delay
Publish per IP5 requests/second, with burst 10 and no delay
Concurrent streams per IP20 WebSocket or SSE connections
Publish body256 KiB maximum
Native owner admissionPer-owner window, configured per served chain. Over budget returns 422 with owner admission rate limit exceeded.
Edge over budgetnginx has no custom limit status, so an over-budget client receives 503.

The edge allows CORS from any origin, the Content-Type header, and GET, POST, and OPTIONS. Preflight max age is 600 seconds. Health and read timeouts are 10 and 30 seconds, publish read timeout is 15 seconds, and stream read/send timeouts are 3600 seconds.

10 / OPERATIONS

Health

GET /v1/health returns 200 when the store is readable and the on-chain source is running. Otherwise it returns 503 with status: "degraded".

{"status":"ok","store_readable":true,"last_onchain_poll_completed_ms":1756800000123,"onchain_source_status":"running"}

last_onchain_poll_completed_ms is null before the first completed poll. onchain_source_status is running, stopped, or not_polled.