Strike integration
Exactly which Strike Finance interfaces strikeabox uses, and what for. Everything is read from or sent to Strike's own hosts, one per network, and nothing else is allowed (see Network allowlist).
Networks
| Network | Host |
|---|---|
| testnet | api-v2-testnet.strikefinance.org |
| mainnet | api.strikefinance.org |
A deployment names two networks, which may be the same:
- the execution network (
STRIKE_NETWORK, defaulttestnet): where Live orders are placed. Builder Connect, the trading REST API and the user stream go there, and Live mode shows and checks that network's own market: its mark price, order book and liquidity gate. - the data network (
STRIKE_DATA_NETWORK, default: the execution network): whose public market data drives practice mode. The chart, every practice quote and all practice settlement come from it, and so do the order book and statistics shown next to the practice board.
The hosted app executes on testnet, with no real funds, and uses mainnet's public market data for practice. Reading public market data never moves funds. Live trading on mainnet also needs LIVE_MAINNET_ENABLED=true.
| Data | Network | Read by |
|---|---|---|
| Mark price ticks: chart, quotes, practice settlement | data | the worker's collector (one connection, recorded in Postgres) |
| Order book and statistics next to the practice board | data | every api process (its own connection) |
| Live mode's market: price line, order book, best bid and ask, liquidity gate, expected fill | execution | every api process: a separate connection when the networks differ, the data network's connection when they are the same |
| Builder Connect, orders, account reads, the user stream | execution | api and worker |
<host> below stands for the host of the network in question.
Strike's own reference: docs.strikefinance.org.
At a glance
| Strike interface | Auth | Used for | Module |
|---|---|---|---|
Price stream wss://<host>/ws/price, <symbol>@markprice@1s | none | The mark price: chart, quotes, volatility and practice settlement (data network); Live mode's price line and gate (execution network); funding rate and next funding time | market/collector (worker), market/feed (api) |
Price stream, <symbol>@bookticker | none | Best bid and ask with sizes, for the liquidity gate and the live plan | market/collector (worker), market/feed (api) |
Price stream, <symbol>@depth | none | Order-book updates for the 20 best levels of each side | market/feed (api) |
Price stream, <symbol>@trade | none | Public trades for the market sheet | market/feed (api) |
Price stream, <symbol>@miniticker | none | 24 h last price, open, high, low and volume | market/feed (api) |
GET /price/v2/depth?symbol=&limit=1000 | none | The full order book: on every (re)connect and every 2 s | market/feed (api) |
GET /price/v2/ticker/bookTicker | none | A snapshot of the best bid and ask, every 5 s, because the stream only pushes on change | market/collector (worker), market/feed (api) |
GET /price/v2/trades?symbol=&limit=20 | none | The latest public trades, on every (re)connect | market/feed (api) |
GET /price/v2/premiumIndex?symbol= | none | Funding rate and next funding time, every 10 s; mark and index while the stream is silent | market/feed (api) |
GET /price/v2/openInterest?symbol= | none | Open interest, every 10 s | market/feed (api) |
GET /v2/time | none | Strike's clock, so signed requests carry a timestamp Strike accepts | strike |
GET /v2/markets | none | Market rules: price tick, size steps, minimum order value, price bounds, status | strike |
GET /v2/fee-tiers | none | Strike's fee schedule, for fee estimates in the live plan | strike |
POST /auth/builder/request-signature | none | Builder Connect step 1: the consent challenge | strike/connect (api) |
POST /auth/builder/verify-signature | none | Builder Connect step 2: submit the wallet signature, activate the API wallet | strike/connect (api) |
POST /v2/order/strategy | API wallet | Live entry: bracket order with take-profit and stop legs | strike/live |
POST /v2/order | API wallet | Reduce-only close at the time exit, or on an early close | strike/live |
DELETE /v2/order/cancel | API wallet | Cancel leftover take-profit or stop legs before a time-exit close | strike/live |
GET /v2/order?client_order_id= | API wallet | Look an order up before ever re-sending it | strike/live |
GET /v2/positions, /v2/balances, /v2/openOrders | API wallet | Reconcile positions and orders; the read-only account view | strike/live, strike/connect |
GET /v2/history/fill | API wallet | Fills of an order, when the stream missed them | strike/live |
GET /v2/symbols | API wallet | The account's per-symbol settings, including current leverage | strike/live |
User stream wss://<host>/ws/user-api, session.logon | API wallet | ORDER_TRADE_UPDATE and ACCOUNT_UPDATE events: fills, order states, balances, positions | strike/live (worker) |
Header X-Builder-Fee-Bps | on orders | The builder fee charged on each order | strike/live |
Every /v2/* and /auth/builder/* call and the user stream go to the execution network. Strike's REST 24 h ticker is not used: the 24 h values come from the miniticker stream.
Market data
Mark price, <symbol>@markprice@1s
The worker opens one connection to the data network's wss://<host>/ws/price and subscribes with Strike's Binance-style request and lowercase stream names:
{ "method": "SUBSCRIBE", "params": ["btc-usd@markprice@1s", "btc-usd@bookticker", "eth-usd@markprice@1s", "eth-usd@bookticker", "sol-usd@markprice@1s", "sol-usd@bookticker"], "id": 1 }Each event is a markPriceUpdate:
{ "e": "markPriceUpdate", "E": 1700000000000, "s": "BTC-USD", "p": "…", "i": "…", "r": "…", "T": 1700000000000 }strikeabox stores s, E (Strike's engine time, used as the tick time), p (mark price, kept as an exact decimal string), i (index price) and its own receive time. Updates arrive nominally once per second. Duplicates by (symbol, E) are dropped. Reconnects use backoff. If no tick arrives for 3 s, the market is stale: no quotes, no taps.
Every stored tick is checked. It is flagged, and then behaves exactly like a gap in the chart, quotes and settlement, when its mark price is not a positive number, its E is more than 2 s ahead of or 10 min behind the receive time, it jumps more than 1.5 % from the previous accepted tick (within 2.5 s), or it differs from the index price i by more than 5 %. The median of |received − E| over the last 30 ticks must stay within 2 s, or the market is stale until it recovers.
The recorded mark price is used for the chart, the volatility estimate, every practice quote and all practice settlement. r (funding rate) and T (next funding time) feed the market statistics.
The api's market feeds
Every api process also follows Strike's public market data itself, in memory: one /ws/price connection to the data network and, when the execution network is a different one, a second connection to it. Each subscribes, per market, to markprice@1s, bookticker, depth, trade and miniticker, and polls the REST endpoints below. Nothing here passes through Postgres, so any number of api instances each serve it correctly on their own. None of it is ever used for a practice quote, outcome or payout.
Mark ticks on these feeds pass the same quality checks as the collector's. When the networks differ, the execution network's accepted ticks of the last 15 minutes are kept in memory as Live mode's price line (/api/exec/:symbol/ticks). After a restart that line starts from the restart. Nothing is settled from it.
Order book, <symbol>@depth and GET /price/v2/depth
Each feed keeps a copy of every offered market's order book:
- The REST snapshot
GET /price/v2/depth?symbol=<symbol>&limit=1000returns{lastUpdateId, E, T, bids, asks}, each level[price, size]. It is read on every (re)connect, again a few seconds later, so the book is rebuilt from a snapshot taken after the connection opened, whenever an update cannot be applied, and every 2 s (BOOK_RESYNC_MS) in any case. - The
depthstream sends{"e": "depthUpdate", "E", "s", "U", "u", "b", "a"}and describes only the 20 best levels of each side. An update is applied only when itsuis newer than what the copy already reflects; a size of"0"removes the level. The 20 best levels are therefore current, and the levels below them are refreshed by the 2 s snapshot. - Strike's update ids increase across all markets, not one by one per market, so a missed update can't be detected from them: the periodic snapshot is what repairs it. After a reconnect, the copy stays stale until a snapshot is proven to postdate the connection.
- A copy that is not synced (being rebuilt, or not yet proven current after a reconnect), or whose last REST snapshot is older than 20 s (
BOOK_STALE_MS), is stale: the stream's updates confirm only the 20 best levels. The app draws it grey and labels it, and a stale copy of the execution network's book closes the liquidity gate.
Prices and sizes stay Strike's decimal strings; the per-row sums the board shows are exact.
Best bid and ask, bookTicker
The bookticker channel carries the best bid and ask with their sizes. It only pushes when the top of the book changes, so the same data is also read as a REST snapshot, GET /price/v2/ticker/bookTicker, every 5 s. The worker records the data network's best bid and ask next to its ticks. The api's execution-network feed uses them for the liquidity gate and the live plan: both best prices within 0.75 % of the mark, and on each side at least twice the box notional resting within 0.75 % of the mark, summed over every level of the order-book copy. A best bid and ask older than 10 s, a stale order-book copy or a stale mark price closes the gate.
Market statistics
| Value | From |
|---|---|
| 24 h last price, open, high, low, volume and quote volume | <symbol>@miniticker (24hrMiniTicker events) |
| Mark and index price | markprice@1s; GET /price/v2/premiumIndex while the stream is silent |
| Funding rate and next funding time | markprice@1s and GET /price/v2/premiumIndex (every 10 s) |
| Open interest | GET /price/v2/openInterest (every 10 s) |
| Latest public trades, newest first, up to 20 | <symbol>@trade, started from GET /price/v2/trades on each (re)connect |
Each value is null until Strike has sent it, and the server records when each group was received. Nothing is smoothed or estimated. The statistics shown are the data network's.
Market rules, GET /v2/markets
Public, read from the execution network. Per market: order_tick_price, order_limit_step_size and order_market_step_size, order_min_notional, order_min_price and order_max_price, status. strikeabox rounds sizes down to the step and refuses presets below the minimum notional. ADA-USD is excluded because its order_min_price is above its mark price on testnet.
Fee schedule, GET /v2/fee-tiers
Public, read from the execution network. Tiers with 30-day volume thresholds and taker and maker rates. Used for the estimated fees in a live plan. Actual fees always come from the fills Strike reports.
Builder Connect
strikeabox is registered as builder strikeabox. Connecting a user, on the execution network:
- The server generates a fresh Ed25519 key pair for the user (the API wallet).
POST /auth/builder/request-signaturewith{ address, chain, code: "strikeabox", fee_share_bps, public_key }.chainisethereum,solanaorcardano.fee_share_bpsis the maximum builder fee the user approves: 100, i.e. 1 % (STRIKE_BUILDER_FEE_CAP_BPS, default 100). Strike answers with anonceandmessage_to_sign. The nonce is single-use and expires after 5 minutes.- The browser shows
message_to_signexactly, then asks the user's wallet to sign it:- Ethereum:
personal_sign(EIP-191), hex signature; - Solana:
signMessage, base58 signature; - Cardano: CIP-30
signData, sent as"{coseSign1Hex}:{coseKeyHex}".
- Ethereum:
POST /auth/builder/verify-signaturewith the nonce andwallet_signature. Strike creates the account if needed, approves the builder, revokes any earlier API wallet for this account and builder, and returnsaccount_idand the new API wallet's id, public key and expiry.
The exact text users sign is on Connecting your wallet.
The private key is encrypted with AES-256-GCM under KEY_ENCRYPTION_KEY before it is stored. The additional authenticated data binds the user id and the public key. The key never reaches the browser and is never logged. See Security model.
Signed requests
Every request on behalf of a user carries four headers:
| Header | Value |
|---|---|
X-API-Wallet-Public-Key | the API wallet public key, 64 hex characters |
X-API-Wallet-Signature | Ed25519 signature of the message below, hex |
X-API-Wallet-Timestamp | Unix time in seconds |
X-API-Wallet-Nonce | a fresh UUID v4 per request |
The signed message:
{METHOD}:{PATH}:{TIMESTAMP}:{NONCE}:{BODY_HASH}METHODupper case;PATHincludes the query string, e.g./v2/order?client_order_id=sbx-…-e;BODY_HASHis the SHA-256 hex digest of the exact JSON body bytes sent; for a request without a body, the digest of the empty string.
The body is serialised once and the same bytes are hashed and sent. Unit tests check the signer against Strike's published message format, the empty-body hash and path-with-query cases.
Live orders
Entry: POST /v2/order/strategy
One bracket per live box:
- primary:
limit, time in forceIOC, at the best opposite price, capped at mark × (1 ± 0.005); a best price beyond that cap refuses the plan (SPREAD_TOO_WIDE), so the limit is the best opposite price; - take-profit leg at the box's near edge, and stop leg at the protective stop, both
working_type: mark_priceand reduce-only.
Strike places the legs when the primary fills and cancels one when the other triggers. A partial fill is kept, and its legs are sized to the filled size: Strike's legs carry the planned size, so strikeabox places reduce-only take-profit and stop orders for the filled size and cancels the original legs once those rest on Strike. No fill means NOT_FILLED.
Before the user confirms, the plan walks the execution network's order-book copy on the entry side, through every level priced no worse than that limit, and shows the expected average price, the slippage from the best level and whether the size would fill fully, partly or not at all. Because the limit is the best opposite price, a thin book often fills only partly. It is an estimate; the fills Strike reports decide.
Client order ids
Deterministic: sbx-{liveBoxId}-e (entry), -tp, -sl, and -x{n} for close attempts. Every command is written to a durable outbox before it is sent.
Lookup before resend: GET /v2/order?client_order_id=
Strike does not de-duplicate on client order id. After a timeout or a crash, the worker looks the order up by its client id before it would ever send it again. A command with no Strike record and no stream event within 8 s is treated as not placed.
Time exit: DELETE /v2/order/cancel then POST /v2/order
At the window end, the worker cancels the remaining legs and sends a reduce-only close, sized to the box's own remaining exposure (never to a larger account position). If the close doesn't complete, it retries with a wider price allowance from the book mid: 1 %, 3 %, then 5 %. If the position still can't be closed, the box becomes CLOSE_FAILED and the user is told how to close it in the Strike app; the worker then places a best-effort reduce-only protective stop for what remains and keeps checking the box. The same close procedure serves a close the user asks for (exitReason: "user").
Builder fee: X-Builder-Fee-Bps
Sent on every order request: STRIKE_BUILDER_FEE_BPS (default 5, i.e. 0.05 %, on the hosted deployment), never above the user's approved fee_share_bps. Strike requires it to be at most the user's approved fee_share_bps and the builder's registered fee share, and rejects the order otherwise. The fee is charged on fills, not on placement. The rate in force is stored on the live box when it is accepted and used for every order of that box, so a later change to the setting never reaches boxes that are already open.
Leverage: GET /v2/symbols
strikeabox reads the account's current leverage for the symbol and shows it in the plan. It never changes leverage.
User stream
The worker keeps one connection per connected user to the execution network's wss://<host>/ws/user-api and logs on with the API wallet:
signature = hex( Ed25519_Sign(privateKey, "session.logon:<timestamp in ms>:<apiKey>") )then subscribes to the user data stream. It consumes:
ORDER_TRADE_UPDATE: order lifecycle changes and fills, which drive the live box status, filled size, average prices and fees;ACCOUNT_UPDATE: balance and position changes.
Stream events are treated as the fast path. After every reconnect, the worker re-reads orders and positions over REST (GET /v2/order, GET /v2/positions) before trusting the stream again; boxes show RECONCILING meanwhile.
Errors that change state
| Strike response | strikeabox does |
|---|---|
401 with authentication failed or wallet expired or revoked | Marks the user's link revoked; Live mode asks them to reconnect. |
Order rejected (400) | Box REJECTED, with Strike's reason. |
| Timeout or connection loss | Lookup by client order id before any resend; RECONCILING until known. |
Network allowlist
apps/server/src/config.ts validates every Strike URL at start-up, each against its own network's host:
| Setting | Must be on |
|---|---|
STRIKE_REST_ORIGIN, STRIKE_USER_WS, STRIKE_EXEC_PRICE_WS | the execution network's host |
STRIKE_PRICE_WS, STRIKE_DATA_REST_ORIGIN | the data network's host |
Each must use https: or wss: as appropriate, with no custom port, and the REST origins with no path. Anything else, including the other network's host, stops the process. There is no fallback from one network to the other. The market feeds check their host again when they start, never follow a redirect, and refuse an answer that comes from any other origin. The only exception is loopback hosts for the mock server, allowed only when STRIKE_ALLOW_MOCK_HOSTS is set and NODE_ENV=test.
The mock Strike
packages/strike-mock implements the price stream (including the order book, trades and 24 h statistics), the REST API and the user stream for tests: fills, partial fills, no fills, rejections, dropped commands, timeouts, stream reconnects and close failures. It is a test dependency only and is never deployed. A pass against the mock doesn't show that the real Strike behaves the same way; see Testing for what runs against Strike testnet itself.