Spire ProtocolSPIRE PROTOCOLDOCS

HTTP API

The SDK is a wrapper over this. Anything the SDK does can be done with an HTTP client, which is what you want if you are not on JavaScript.

SettingValue
Base URL, mainnethttps://api.spireproto.xyz
Base URL, testnethttps://api.testnet.spireproto.xyz
VersionPath prefix /v1
Content typeapplication/json
Streamingwss://api.spireproto.xyz/v1/stream

Breaking changes get a new prefix. /v1 keeps working while /v2 exists, and additive fields are not breaking, so parse permissively. The full versioning rules are in the Changelog.

Authentication

Two headers on every request.

HeaderValue
X-Spire-VenueYour venueId
X-Spire-SignatureEIP-712 signature over the request body

Reads accept the venue header alone. Writes need both, and the signature covers the exact bytes you send, so sign after serialising.

curl https://api.spireproto.xyz/v1/windows/current \
  -H "X-Spire-Venue: $SPIRE_VENUE_ID"

Idempotency

Every write accepts Idempotency-Key. For POST /v1/fills it defaults to fillId.

Replaying a key with an identical body returns the original result and Idempotency-Replayed: true. Replaying with a different body is SPIRE-1004.

Errors

One envelope, always.

{
  "error": {
    "code": "SPIRE-3001",
    "message": "position limit exceeded",
    "detail": { "member": "0xBd4...", "limit": "12500000000000", "attempted": "13100000000000" }
  }
}
HTTPMeaning
400SPIRE-1xxx, malformed or invalid
401SPIRE-2xxx, authentication
409SPIRE-3xxx, limits and collateral
425SPIRE-4001, window is finalising, retry
422SPIRE-5xxx, settlement
429Rate limited, see Retry-After

Branch on error.code, not on the HTTP status. Full list in Errors.

Pagination

List endpoints take limit (max 500, default 100) and cursor, and return { items, cursor }. A null cursor means the last page.

POST /v1/fills

Novates a matched fill. The only write in the normal path.

curl -X POST https://api.spireproto.xyz/v1/fills \
  -H "X-Spire-Venue: $SPIRE_VENUE_ID" \
  -H "X-Spire-Signature: $SIG" \
  -H "Content-Type: application/json" \
  -d '{
    "fillId": "v7-9f31c2",
    "asset": "0xA1c...",
    "buyer": "0xBd4...",
    "seller": "0x39e...",
    "size": "250000000000000000000",
    "price": "187420000",
    "matchedAt": 1774483200,
    "window": "current"
  }'
{
  "obligationId": "0x8c1f...",
  "fillId": "v7-9f31c2",
  "windowId": 918342,
  "state": "novated",
  "novatedAt": 1774483201,
  "sides": [
    { "member": "0xBd4...", "direction": "receive", "size": "250000000000000000000", "cash": "-46855000000" },
    { "member": "0x39e...", "direction": "deliver", "size": "250000000000000000000", "cash": "46855000000" }
  ]
}

Field meanings are in Data model.

GET /v1/obligations/{obligationId}

Returns one obligation. 404 with SPIRE-1101 if the id is unknown.

GET /v1/obligations

Filters: member, asset, windowId, state, limit, cursor.

curl "https://api.spireproto.xyz/v1/obligations?member=0xBd4...&windowId=918342&state=netted" \
  -H "X-Spire-Venue: $SPIRE_VENUE_ID"

GET /v1/positions

The net view. This is the endpoint to poll if you do not want the websocket.

curl "https://api.spireproto.xyz/v1/positions?member=0xBd4...&asset=0xA1c...&windowId=918342" \
  -H "X-Spire-Venue: $SPIRE_VENUE_ID"
{
  "member": "0xBd4...",
  "asset": "0xA1c...",
  "windowId": 918342,
  "gross": "1000000000000000000000",
  "net": "250000000000000000000",
  "requiredMargin": "3748400000",
  "utilisation": 1840
}

Omit windowId for the open window. Omit asset to get every asset for that member.

GET /v1/collateral/{member}

{
  "member": "0xBd4...",
  "posted": [ { "asset": "USDC", "amount": "1000000000000" } ],
  "haircutValue": "1000000000000",
  "initialMargin": "80000000000",
  "defaultFund": "12000000000",
  "free": "908000000000",
  "limit": "12500000000000"
}

POST /v1/collateral/deposits

Body { member, asset, amount }. Returns the updated account. The amount counts after its haircut, listed in Parameters.

POST /v1/collateral/withdrawals

Body { member, asset, amount }. Comes out of free only. A withdrawal that would take free below zero is 409 with SPIRE-3002.

GET /v1/assets and GET /v1/assets/{address}

{
  "asset": "0xA1c...",
  "tier": 1,
  "initialMargin": 0.08,
  "maintenance": 0.06,
  "clearable": true,
  "tierEffectiveAt": 0
}

Cache this. It changes at window boundaries, never inside one.

GET /v1/windows/current and GET /v1/windows/{windowId}

{
  "windowId": 918342,
  "opensAt": 1774483000,
  "closesAt": 1774483300,
  "finalisedAt": 0,
  "state": "open",
  "obligationCount": 4127
}

GET /v1/proofs/{windowId}

The solvency proof for a closed window. Verifiable by anyone, no venue header required.

{
  "windowId": 918341,
  "commitment": "0x9a3b...",
  "proof": "0x...",
  "aggregateCollateral": "48210000000000",
  "aggregateObligations": "31447000000000",
  "verified": true
}

The proof asserts that collateral covers obligations without opening any member's position. See Solvency.

WSS /v1/stream

wscat -c "wss://api.spireproto.xyz/v1/stream" \
  -H "X-Spire-Venue: $SPIRE_VENUE_ID"

Subscribe after connecting:

{ "op": "subscribe", "channels": ["window", "obligation", "margin"], "member": "0xBd4..." }

Each message carries type, windowId and a payload matching the events listed in API reference. Delivery is at-least-once, so deduplicate on obligationId or on (member, windowId).

Heartbeat every 15 seconds. No frame for 40 seconds means reconnect.

Rate limits

Endpoint groupLimit
POST /v1/fills200 per second per venue
Reads50 per second per venue
Websocket connections8 per venue

429 carries Retry-After in seconds. The fills limit is per venue, not per member, so size your matcher accordingly.

Last updated 31 August 2026 Docs source on GitHub