API Reference
Trades
Create

Create Trade

POST /api/v1/trades

Scope: trades:write

Submit a new spread trade. Returns immediately with a trade_id; execution happens asynchronously.

Request

{
  "market": "HYPE",
  "long_exchange": "lighter",
  "long_wallet": "0xYourWalletAddressHere",
  "short_exchange": "extended",
  "short_wallet": "0xYourWalletAddressHere",
  "target_spread": "0",
  "spread_condition": "gte",
  "target_size": "2",
  "execution_type": "maker_taker",
  "maker_leg": "long",
  "max_slippage": "0.005",
  "size_type": "base",
  "spread_type": "absolute"
}

Required fields

FieldTypeDescription
marketstringTrading pair symbol (e.g. HYPE, BTC, ETH)
long_exchangestringExchange for the long leg
long_walletstringWallet address for the long leg
short_exchangestringExchange for the short leg (must differ from long_exchange)
short_walletstringWallet address for the short leg
target_spreadstringTarget spread value (decimal string, in units of spread_type)
spread_conditionstringOne of gte, gt, lte, lt, eq
target_sizestringTotal size to fill (positive decimal)
execution_typestringmaker_taker
maker_legstringlong or short
max_slippagestringHedge price tolerance per retry step, as a fraction of price (0.005 = 0.5%). Each hedge retry is priced that far beyond the touch times the attempt number, up to 5× (so 0.005 reaches 2.5% on the fifth retry). 0 means the default, 1%. Must be at most 0.05 (5%); larger values are refused with a 400.

Optional fields

FieldTypeDefaultDescription
size_typestringbasebase (token amount) or notional (USD amount)
spread_typestringabsoluteabsolute (raw USD price difference), percentage (% of hedge reference price), bps (basis points of hedge reference price), or ticks (multiples of the coarser leg's tick size)
idempotency_keystring—See idempotency below — required via header or body
margin_modestringcrosscross or isolated
auto_adjust_leveragebooleanfalseLet this trade lower the account's per-market leverage when a venue's leverage-tier openable-notional cap blocks the target size. Off by default: an account's leverage is never changed silently
hedge_timeout_msinteger10000Hedge IOC confirmation timeout in milliseconds, bounded to [1000, 60000]
hl_action_budgetintegerthe trade's USD notional, at least 100How many Hyperliquid actions (place + modify + cancel, both legs) the trade may send before a Hyperliquid maker stops re-pricing. Must be positive. See Hyperliquid action cap
hedge_teardown_fillsbooleanfalseHedge maker fills recovered by a cancel's teardown reconcile instead of only reporting them
on_cancel_fillstringflattenWhat the cancel-path teardown does with a naked maker delta: flatten, hedge, or report — recorded as a teardown_correction event
reduce_onlybooleanfalseBoth legs close exposure the account already holds. Verified against each venue's own position before the trade starts: a leg that does not oppose an open position of at least target_size fails the trade instead of opening one (failure_code: "reduce_only_would_open"). A reduce_only trade is also accepted while the account's own kill switch is active — see Validations. On Extended it also lets the hedge leg close at a negative available balance; otherwise it does not relax the margin pre-flight — see Closing at a negative available balance

Accepted fields are the contract

The request decoder accepts exactly these 27 keys — the two tables above plus the aggressive-quoting fields documented below:

market, long_exchange, long_wallet, short_exchange, short_wallet,
target_spread, spread_condition, target_size, execution_type, maker_leg,
max_slippage, size_type, spread_type, idempotency_key, margin_mode,
auto_adjust_leverage, hedge_timeout_ms, hl_action_budget, hedge_teardown_fills,
on_cancel_fill, reduce_only, quote_mode, worst_spread, fast_move_guard,
fast_move_guard_bps, fast_move_window_ms, fast_move_settle_ms

Any other key is ignored and reported back in the response's warnings[] ("unknown request fields ignored: chunk_delay_ms, chunk_size"), so a client that sends a field this endpoint does not know finds out on the first call, not months later. Key matching is case-insensitive.

To fail instead, send X-Spreadr-Strict: 1 (or true). The request is then rejected with 400 before anything is validated or persisted:

{
  "error": "unknown request fields: chunk_delay_ms, chunk_size",
  "code": "unknown_fields",
  "fields": ["chunk_delay_ms", "chunk_size"]
}

New optional keys may be added to this list within /api/v1 without a version bump; a key is never removed without one.

reduce_only at the venue

A reduce_only trade sends its hedge orders reduce-only. If the position verified at admission has moved since (a liquidation, a reaper, another trade), the hedge venue itself refuses anything that would open a new one. When the hedge venue holds less than a hedge would close, the hedge closes what is left, and the trade ends with the rest recorded: failure_code: "hedge_rejected", a non-zero net_exposure, and failure_detail saying how much. The maker leg's resting quotes are not reduce-only yet.

A reduce_only trade never flattens its maker leg. Its maker fills reduce a position, so a flatten on that venue would close more of it and widen the gap between the legs instead of closing it. The same holds for any trade whose maker fill turns out to have reduced a position the maker venue already held, flagged or not: Spreadr never closes more of it, and never re-opens it.

Instead, the unhedged part of a closing fill is finished on the hedge venue: Spreadr sends one reduce-only order for it there, at the trade's max_slippage. That leaves the pair balanced and smaller, which is what a close is for. When a hedge failed after its retries, this extra attempt is skipped if the hedge venue refused deterministically (it would refuse again). Whatever the hedge venue does not take is recorded and left for you (failure_detail says "exposure remains"). The trade's hedge_abandoned event says which happened (close_finished_on_hedge). At cancel time this applies whatever on_cancel_fill says, except report, which leaves the residual to you.

reduce_only relaxes the margin pre-flight only where a venue is proven to admit reduce-only closes at a negative available balance — today, the hedge leg on Extended. (Closes on Hyperliquid and GRVT skip the balance check whether or not the trade is reduce_only.) See Closing at a negative available balance.

Idempotency (required)

Every create request must carry an idempotency key — either the X-Idempotency-Key header (preferred; takes precedence) or the idempotency_key body field. Missing key → 400. Keys must match ^[a-zA-Z0-9_-]{1,128}$ (a UUID works).

Replaying a key returns 200 OK with the original trade and "idempotent": true — no new trade is created:

{
  "trade_id": "00000000-0000-0000-0000-000000000000",
  "status": "active",
  "rate_budget": 20,
  "idempotent": true
}

Response

202 Accepted

{
  "trade_id": "00000000-0000-0000-0000-000000000000",
  "status": "pending",
  "rate_budget": 20
}

The rate_budget is the per-second request budget Spreadr has allocated to this trade against the maker exchange's per-account rate limit.

When the hedge exchange enforces a per-account limit that other legs of yours share, the response also carries hedge_rate_budget: the per-second budget allocated to this trade's hedge leg. Both budgets are re-divided live as your trades on those accounts start and finish.

warnings

The 202 (and the idempotent-replay 200) may carry warnings, an array of strings. Today it reports a Lighter maker leg that runs on a low stored volume quota (see Lighter maker quota), a Lighter leg admitted on the wallet's shared default API key, and any unknown request fields that were ignored (see Accepted fields). A non-empty array is something to look at, not a failure.

Aggressive quoting and the fast-move guard

quote_mode: "aggressive" (maker-taker only) pegs the maker up to, or across, the touch to fill now, bounded by worst_spread (same units as target_spread; omitted = peg to the touch). Because an aggressive order rests inside the spread, a fast move of the hedge reference can run it over before a re-quote lands. The fast-move guard watches for that:

FieldValuesDefault
fast_move_guardshadow (evaluate and record only), on (cancel the resting order when the hedge reference moves against it by the threshold inside the window, then hold quoting until the book settles), offshadow for aggressive trades
fast_move_guard_bpsexplicit trigger threshold, 0 < x <= 1000the resting order's distance to the hedge reference (its quote cushion), floored at 10 bps
fast_move_window_ms50–5000250
fast_move_settle_ms50–10000500 (a hold is capped at 3 s while the book stays fast)

Every trigger appears in GET /trades/{id}/events as a fast_move_guard event (move, threshold, references, action), hedge_outcome events carry guard_fill_class (after_trigger: a fill inside the settle window of a trigger; adverse: a fill whose move met the threshold with no trigger), and fast_move_guard_stats is written on the trade when it ends — including re_places and re_place_modifies, so the guard's share of the venue-action burn is attributable per trade: one trigger that cancels a resting order costs a cancel, the placement that replaces it once the hold ends, and the first modify that brings that placement to the touch. If the guard's cancel fails, the order is not re-quoted in the meantime: the guard re-sends the cancel every 250 ms until it lands or the order is gone, and each failed attempt counts in cancel_failures.

Hyperliquid action cap

Every trade on a wallet shares that wallet's Hyperliquid address budget: 10,000 requests plus one per USDC traded, and each order, modify and cancel costs one. A Hyperliquid maker follows its computed price, so a slow trade can spend a lot of it. Two 0.05 ETH closes once spent 10,972 actions, and one of them used up the address.

A Hyperliquid maker therefore re-prices only when it matters:

  • Toward the safe side: a move that would leave its order past the target (above it for a buy, below it for a sell) always goes out. So does any change of the order's size.
  • Toward the touch: an improving move goes out only when it moves the order by at least 2 bps, or brings it within 2 bps of the maker venue's touch. An aggressive maker pegged to the touch therefore still follows every move.

On the two closes above, this sends about three quarters fewer modifies.

A trade whose maker leg is on Hyperliquid or trade[xyz] therefore has a cap: hl_action_budget if you send it, otherwise the trade's USD notional (target size × the maker price it first quotes against), with a floor of 100. Since Hyperliquid credits one request per USDC traded, a trade that fills completely earns back about what the cap lets it spend.

  • What counts: every Hyperliquid action the trade's legs send, hedges included. hl_action_counts on the trade has the totals.
  • What it stops: only the maker's re-pricing, meaning a modify or any placement after the first. Cancels, the teardown and hedge orders always go out, even past the cap.
  • An entry that runs out cancels its maker order and ends failed, with failure_code: "hl_action_budget_exhausted" and whatever had filled, hedged. If nothing had filled, it is a no-fill. A frozen order would fill only if the market ran through it, and its hedge would then be priced off the moved market.
  • A reduce_only close that runs out is finished at market. Its order is moved once more, max_slippage (1% when 0) through the maker venue's touch, and sized to everything left. Its fills are hedged as usual, and the trade completes.

Either case writes an hl_action_budget_exhausted event. The trade record carries the cap that applied as hl_action_budget.

Closing at a negative available balance

reduce_only verifies that each leg reduces a position the account holds and exempts the trade from the account's kill switch. Every leg is still checked against the venue's available balance before anything rests. A leg that fully closes a position needs only a small fee headroom (the greater of 0.1% of notional and $1), and on most venues a close is refused while the venue reports a negative available balance. The trade then fails with failure_code: "negative_available_balance", and failure_detail names the venue and the balance.

Hyperliquid (including trade[xyz]) and GRVT skip the check for closes. Both admit an order that closes an existing position at any available balance, so a full close there is never refused for balance. For GRVT this was verified live on 2026-09-25: reduce-only and regular closes, same-market and cross-market, were all accepted at a negative balance.

The exception: the hedge leg of a reduce_only trade on Extended. Extended admits a reduce-only close at a negative available balance and rejects a regular one (verified live 2026-09-25). A reduce_only trade sends its hedge orders reduce-only, so its hedge leg on Extended is not refused for balance when it closes an existing position. The maker leg keeps the check: resting maker quotes are not reduce-only, and Extended would reject them at a negative balance. Put the Extended leg on the hedge side (maker_leg) when closing into a negative Extended balance.

Other venues (Lighter, Orderly, Aster, ApeX, Nado) keep the refusal until each is measured. The fix there is on the venue: add collateral or raise leverage until the available balance is positive, then send the close again. Any other pre-flight shortfall is insufficient_margin.

Validations

The server enforces these before accepting the trade:

  • Both exchanges must have active credentials linked for the specified wallets, and the wallets must belong to your account (403 otherwise).

  • System and both exchanges must not be in maintenance mode (503).

  • The global kill switch must not be active (503).

  • Your account's own kill switch must not be active (503, "code": "user_kill_switch_active") — unless the trade is reduce_only, which is accepted so a halted account can still close. The halt a single-leg liquidation writes says "neutralize via close", so it does not block the close itself. The claim is not taken on trust: the worker checks both legs against the venues' positions before anything rests and fails the trade if either leg would open.

  • long_exchange and short_exchange must differ.

  • Some venues, such as nado, can only take the maker leg. A trade whose hedge leg (the leg opposite maker_leg, after any quota routing below) is one of them returns 400 with "code": "hedge_venue_unsupported"; put that venue on the maker leg instead.

  • market must be recognized on both exchanges — not tradeable on one or both returns 400 with "code": "market_not_tradeable", ambiguous between the two returns 400 with "code": "market_ambiguous"; both carry "venues": [...] naming the exchange(s) the refusal is about and failure_detail with the sentence (use the canonical symbol from GET /api/v1/markets). Key on code, not on the wording.

  • Concurrent maker-trade cap per (wallet, maker exchange), counting only trades where the wallet is on the maker leg. The cap varies by venue (currently: Lighter 1; Hyperliquid, Orderly, Aster 2; Extended 3; trade[xyz] shares Hyperliquid's cap — all subject to change). Exceeding it returns 409 ("max N concurrent maker trades on {exchange} — already has M active").

  • Sufficient account-level rate budget on the maker exchange (429).

  • Hyperliquid address budget (any leg on hyperliquid or hl_xyz). Hyperliquid meters order actions per account: a lifetime allowance of 10,000 plus one per USDC of filled volume, which does not recover with time. The create is refused with 429 and "code": "hl_address_budget_low" before the trade can spend the last of it:

    • maker leg: when the account behind the maker wallet has fewer than max(3% of its cap, 2000) actions left;
    • hedge leg: when the account behind the hedge wallet has fewer than 2000 actions left. A hedge spends one or two actions per fill, but past the cap Hyperliquid admits one request per 10 s, so every hedge would fail. A hedge that does hit the cap mid-trade fails on its first rejection instead of retrying.

    A reduce_only trade is exempt — the remaining budget is exactly what a close needs. If Hyperliquid cannot be read the trade is admitted (the check never blocks on its own failure). Check the account's allowance with GET /api/v1/hl-address-budget/{wallet}.

Lighter maker quota

The server never refuses a trade over Lighter's volume quota. Lighter reports an account's remaining quota only in its replies to orders, so the server works from the last value a Spreadr worker saw for the maker wallet, and that value can be well below the real one. When it is under 500 and the maker leg is on lighter or lighter_rh, the maker moves to the other leg if that leg can take it: a different venue, or the other Lighter deployment when its own stored quota is at least 500. The trade then carries maker_leg_switch_reason: "lighter_quota"; the position and the spread are the same either way. If neither leg can take it, the trade runs as requested and the response's warnings says so. A worker that finds the quota really is that low slows the maker down and stops the trade early.

Example

curl -X POST https://api.spreadr.xyz/api/v1/trades \
  -H "X-API-Key: sprdr_..." \
  -H "X-Idempotency-Key: my-bot-001" \
  -H "Content-Type: application/json" \
  -d '{
    "market": "HYPE",
    "long_exchange": "lighter",
    "long_wallet": "0xYourWalletAddressHere",
    "short_exchange": "extended",
    "short_wallet": "0xYourWalletAddressHere",
    "target_spread": "0",
    "spread_condition": "gte",
    "target_size": "2",
    "execution_type": "maker_taker",
    "maker_leg": "long",
    "max_slippage": "0.005"
  }'