Create Trade
POST /api/v1/tradesScope: 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
| Field | Type | Description |
|---|---|---|
market | string | Trading pair symbol (e.g. HYPE, BTC, ETH) |
long_exchange | string | Exchange for the long leg |
long_wallet | string | Wallet address for the long leg |
short_exchange | string | Exchange for the short leg (must differ from long_exchange) |
short_wallet | string | Wallet address for the short leg |
target_spread | string | Target spread value (decimal string, in units of spread_type) |
spread_condition | string | One of gte, gt, lte, lt, eq |
target_size | string | Total size to fill (positive decimal) |
execution_type | string | maker_taker |
maker_leg | string | long or short |
max_slippage | string | Hedge 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
| Field | Type | Default | Description |
|---|---|---|---|
size_type | string | base | base (token amount) or notional (USD amount) |
spread_type | string | absolute | absolute (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_key | string | — | See idempotency below — required via header or body |
margin_mode | string | cross | cross or isolated |
auto_adjust_leverage | boolean | false | Let 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_ms | integer | 10000 | Hedge IOC confirmation timeout in milliseconds, bounded to [1000, 60000] |
hl_action_budget | integer | the trade's USD notional, at least 100 | How 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_fills | boolean | false | Hedge maker fills recovered by a cancel's teardown reconcile instead of only reporting them |
on_cancel_fill | string | flatten | What the cancel-path teardown does with a naked maker delta: flatten, hedge, or report — recorded as a teardown_correction event |
reduce_only | boolean | false | Both 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_msAny 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:
| Field | Values | Default |
|---|---|---|
fast_move_guard | shadow (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), off | shadow for aggressive trades |
fast_move_guard_bps | explicit trigger threshold, 0 < x <= 1000 | the resting order's distance to the hedge reference (its quote cushion), floored at 10 bps |
fast_move_window_ms | 50–5000 | 250 |
fast_move_settle_ms | 50–10000 | 500 (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_countson 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, withfailure_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_onlyclose that runs out is finished at market. Its order is moved once more,max_slippage(1% when0) 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 (
403otherwise). -
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 isreduce_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_exchangeandshort_exchangemust differ. -
Some venues, such as
nado, can only take the maker leg. A trade whose hedge leg (the leg oppositemaker_leg, after any quota routing below) is one of them returns400with"code": "hedge_venue_unsupported"; put that venue on the maker leg instead. -
marketmust be recognized on both exchanges — not tradeable on one or both returns400with"code": "market_not_tradeable", ambiguous between the two returns400with"code": "market_ambiguous"; both carry"venues": [...]naming the exchange(s) the refusal is about andfailure_detailwith the sentence (use the canonical symbol fromGET /api/v1/markets). Key oncode, 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
hyperliquidorhl_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 with429and"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
2000actions 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_onlytrade 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 withGET /api/v1/hl-address-budget/{wallet}. - maker leg: when the account behind the maker wallet has fewer than
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"
}'