Execution API
Borsa Execution API: send an order intent for your users (one coin, a pair or a basket) and Borsa works it on Hyperliquid with market, chase or TWAP execution, keeping legs balanced. Onboarding, endpoints, events and fees.
The Execution API lets any app route its users' Hyperliquid orders through Borsa's execution engine. You send an intent: what to buy and sell, how much, the worst price you accept and a deadline. Borsa works it on the user's own Hyperliquid account and reports every fill back to you.
Use it when sending one taker order per leg isn't good enough:
- Chase: rest a post-only order at the best bid or ask and follow the book, so fills are maker fills instead of crossing the spread.
- TWAP: spread a large order over minutes or days, each slice taker or chase.
- Multi-leg balance: a pair or basket (up to 4 legs) where no leg runs ahead of the others by more than a tolerance you set, and a leg left behind is pulled level with a taker order.
- Price protection: every leg has a hard worst price (slippage bound and/or limit price). Nothing fills beyond it.
Orders are placed on the user's own account through an agent the user approves. Borsa never holds funds and can't withdraw: Hyperliquid agents can only trade.
How It Works
- Borsa gives you an API key.
- Each user approves, inside your app, three things once (see Onboarding a User): the Borsa execution agent, the Borsa builder fee, and a signed delegation that lets your app send intents for them.
- You
POST /intents. Borsa validates it, records the arrival price of every leg and starts working it. - You follow progress with
GET /intents/{id}, the event stream, or both.
Base URL: https://app.borsa.cc/api/exec/v1
Every request carries your key:
Authorization: Bearer bx_<yourid>_<secret>
Keep the key on your server. Requests are limited to 20 per second per key.
Onboarding a User
Call GET /users/{address} to see what a user still needs:
{
"user": "0x6858…41ea",
"ready": false,
"agent": { "address": "0x4a9a…44c8", "name": "Borsa Cloud", "approved": false, "validUntil": null },
"builder": { "address": "0x74c3…4bd0", "maxFeeRate": "0.025%", "approved": true, "feeBps": 25 },
"delegation": { "active": false, "expiresAt": null }
}
ready is true once all three are in place. Add ?fresh=1 right after the user signs something (answers are cached for 30 seconds).
1. Approve the agent
The user signs Hyperliquid's approveAgent action with their main wallet, using agent.address and agent.name exactly as returned. Every user has their own agent address. With the Hyperliquid SDK (@nktkas/hyperliquid):
await exchange.approveAgent({ agentAddress: status.agent.address, agentName: status.agent.name });
Users who already run Borsa Cloud strategies have this agent approved and skip this step. Hyperliquid allows a few named agents per account; approving a new name when they are all taken replaces one of the user's existing agents, so tell the user which name they are approving.
2. Approve the builder fee
Orders carry Borsa's builder code. The user signs approveBuilderFee with builder.address and builder.maxFeeRate:
await exchange.approveBuilderFee({ builder: status.builder.address, maxFeeRate: status.builder.maxFeeRate });
3. Sign the delegation
The delegation is an EIP-712 message that lets your app, and only your app, send intents for the user. Get the exact payload, ask the user to sign it with their main wallet, and post the signature:
GET /delegations/typed-data?user=0x…&expiresAt=<ms>&nonce=<n>&chainId=<wallet chain id>
{
"domain": { "name": "Borsa Execution", "version": "1", "chainId": 42161 },
"types": {
"Delegation": [
{ "name": "user", "type": "address" },
{ "name": "integrator", "type": "string" },
{ "name": "expiresAt", "type": "uint64" },
{ "name": "nonce", "type": "uint64" }
]
},
"primaryType": "Delegation",
"message": { "user": "0x…", "integrator": "yourid", "expiresAt": "1799999999999", "nonce": "1" }
}
POST /delegations
{ "user": "0x…", "expiresAt": 1799999999999, "nonce": 1, "chainId": 42161, "signature": "0x…" }
expiresAtis a Unix time in milliseconds, at most 366 days ahead.noncemust be higher than the user's previous delegation to you (a timestamp works). A new delegation replaces the old one.chainIdis whatever chain the user's wallet is on; it only has to match what was signed.DELETE /delegations/{address}revokes it and cancels the user's working intents.
Info
A user can always stop everything on their side by removing the agent in Hyperliquid's API settings.
Sending an Intent
POST /intents
{
"user": "0x6858c42ceb3596b256ca0f547c76e2ad178a41ea",
"clientId": "order-7781",
"algo": "chase",
"legs": [
{ "coin": "BTC", "side": "buy", "notionalUsd": "5000" },
{ "coin": "ETH", "side": "sell", "notionalUsd": "5000" }
],
"maxSlippageBps": 30,
"durationSec": 300,
"onDeadline": "market",
"maxImbalancePct": 20
}
The response is the intent (status 201, see The Intent Object). Posting the same clientId again returns the existing intent with status 200, so retries are safe.
Legs
| Field | Type | |
|---|---|---|
coin | string | Market name as on Hyperliquid: BTC, xyz:XYZ100 for HIP-3 perps, HYPE/USDC for spot. |
side | buy / sell | |
size | decimal string | Size in the base asset. Set either size or notionalUsd. |
notionalUsd | decimal string | USD value, turned into a size at the arrival mid. |
limitPx | decimal string | Optional worst price for this leg. |
reduceOnly | boolean | Perps only. A leg with no position left to reduce counts as done. |
Each leg must be worth at least 10 USD (Hyperliquid's minimum order). One coin per leg, up to 4 legs.
Parameters
| Field | Default | |
|---|---|---|
algo | market | market, chase or twap (below). |
maxSlippageBps | 50 | Worst price per leg, in basis points from its arrival mid. With limitPx, the tighter of the two applies. Max 1000. |
durationSec | 60 market, 300 chase | How long the intent may work. Required for twap (10 s to 7 days). |
onDeadline | cancel, market for twap | At the deadline: cancel stops and leaves the rest unfilled; market sends the rest as taker orders, still within the worst price. |
slices | about one per 30 s | TWAP only: number of slices (2 to 1000). Lowered automatically so each slice is worth at least 10 USD. |
sliceAlgo | chase | TWAP only: how each slice is worked, chase or market. |
repostMs | 2000 | Chase: minimum time between reprices of a resting order (min 1000). |
maxImbalancePct | 20 | Multi-leg: how far (in % of its own size) any leg may get ahead of the slowest leg. |
catchUp | taker | Multi-leg: a leg behind by the full tolerance is filled with a taker order after catchUpAfterSec. wait never forces it. |
catchUpAfterSec | 30 | |
clientId | Your id, up to 64 characters, unique per API key. |
Algorithms
market: every leg is sent at once as an immediate-or-cancel order priced at its worst price, so it takes whatever liquidity is inside the bound. Anything the book can't fill inside the bound is retried every couple of seconds until durationSec.
chase: each leg rests a post-only order at the best bid (buy) or best ask (sell). When the touch moves, the order is cancelled and reposted at the new touch, at most every repostMs. It never posts beyond the worst price: if the touch moves past it, the leg waits. Fills are maker fills.
twap: the size is released in slices equal steps over durationSec. Each released step is worked as a chase or market order. A slice that isn't done when the next one is released carries over. With onDeadline: "market" (the default) whatever is left at the end is taken.
Keeping Legs Balanced
With two or more legs, each leg's working order is capped so that its progress (filled as a share of its size) stays within maxImbalancePct of the slowest leg. With the default 20%, a pair starts by resting 20% of each leg; once the slower leg catches up, the next 20% is released.
If one leg fills and the other doesn't, the leader stops and waits. After catchUpAfterSec the laggard crosses the spread for exactly the amount needed to bring it level (catchUp: "taker"). A smaller maxImbalancePct means tighter balance and more, smaller orders.
Market legs are sent together, so they don't need the cap.
The Intent Object
{
"id": "0b3f…",
"clientId": "order-7781",
"user": "0x6858…41ea",
"status": "working",
"error": null,
"algo": "chase",
"params": { "maxSlippageBps": 30, "durationSec": 300, "onDeadline": "market", "maxImbalancePct": 20, "…": "…" },
"builderFeeBps": 25,
"createdAt": 1791969952467,
"deadline": 1791970252467,
"endedAt": null,
"legs": [
{
"coin": "BTC", "side": "buy",
"targetSize": "0.06", "filledSize": "0.012", "filledNotionalUsd": "997.21",
"avgPx": 83101, "arrivalMid": 83105.5, "worstPx": 83354.8, "slippageBps": -0.54,
"working": { "px": "83101", "size": "0.012" },
"done": false
}
]
}
| Status | |
|---|---|
working | Being worked. |
completed | Every leg filled. |
expired | Deadline reached with something unfilled ( onDeadline: "cancel", or the book stayed beyond the worst price). |
cancelled | Cancelled by you, or by revoking the delegation. |
failed | Hyperliquid rejected an order ( error says why, e.g. not enough margin). Other legs are stopped. |
slippageBps is the fill price against the leg's arrival mid: positive is worse, negative is better. builderFeeBps is in tenths of a basis point (25 = 0.025%).
Endpoints
| Method | Path | |
|---|---|---|
GET | /users/{address} | Readiness: agent, builder fee, delegation. |
GET | /delegations/typed-data | The EIP-712 payload to sign. |
POST | /delegations | Store a signed delegation. |
DELETE | /delegations/{address} | Revoke it and cancel the user's working intents. |
POST | /intents | Create an intent. |
GET | /intents | List: ?user=, ?status= (active or a status), ?before=<createdAt>, ?limit= (max 200). |
GET | /intents/{id} | One intent. |
POST | /intents/{id}/cancel | Cancel: resting orders are pulled and fills settled; the status turns cancelled within a second or two. |
GET | /intents/{id}/fills | Every fill: size, price, maker or taker, Hyperliquid order id, builder fee. |
GET | /events | Event log page: ?after=<event id>&limit= (max 500). |
GET | /events/stream | The same events live, as Server-Sent Events. |
GET | /revenue | Volume, builder fees and your share: ?from=&to= (ms, default last 30 days). |
Events
Every change is an event with an increasing id, kept for 7 days:
| Type | data |
|---|---|
intent.created | The intent. |
intent.fill | { fillId, leg, coin, side, size, px, liquidity, oid } |
intent.status | The intent, whenever its status changes. |
GET /events/stream replays everything after ?after= (or the Last-Event-ID header, which EventSource sends on reconnect) and then streams new events. Store the last id you processed and you never miss or double count one. A comment line is sent every 15 seconds to keep the connection open.
id: 412
event: intent.fill
data: {"id":412,"type":"intent.fill","intentId":"0b3f…","ts":1791969955000,"data":{"leg":0,"coin":"BTC","side":"buy","size":"0.012","px":83101,"liquidity":"maker","oid":4012…}}
Errors
Errors are JSON: { "error": "<code>", "detail": … }.
| Status | Code | |
|---|---|---|
400 | invalid_intent | detail.field names the problem. |
400 | unknown_market | |
401 | unauthorized | Missing or wrong API key. |
403 | delegation_required | No active delegation from this user to you. |
428 | agent_not_approved | detail has the agent address and name to approve. |
428 | builder_fee_not_approved | detail has the builder address and fee rate. |
429 | rate_limited / too_many_active_intents | Up to 20 working intents per user per integrator. |
503 | paused / no_market_data | Try again shortly. |
Fees
Orders pay Hyperliquid's normal trading fees plus Borsa's builder fee, the same as trading on Borsa: 0.025%, or 0.01% on HIP-3 markets (0.01% everywhere for accounts that approved Borsa's builder fee before October 14, 2026). Borsa's volume and brsHYPE staking fee reductions don't apply to API orders. Integrators earn an agreed share of the builder fees their users generate; GET /revenue shows the running total.
Example: Pair Trading
A pair trade is two legs with opposite sides. Long SOL against ETH, 2,000 USD each, worked passively for up to 10 minutes and finished with takers if needed:
curl -X POST https://app.borsa.cc/api/exec/v1/intents \
-H "Authorization: Bearer $BORSA_KEY" -H "content-type: application/json" \
-d '{
"user": "0x6858c42ceb3596b256ca0f547c76e2ad178a41ea",
"clientId": "pair-sol-eth-0193",
"algo": "chase",
"durationSec": 600,
"onDeadline": "market",
"maxSlippageBps": 40,
"maxImbalancePct": 15,
"legs": [
{ "coin": "SOL", "side": "buy", "notionalUsd": "2000" },
{ "coin": "ETH", "side": "sell", "notionalUsd": "2000" }
]
}'
Closing the pair is the same intent with the sides swapped and "reduceOnly": true on both legs. For a large pair, "algo": "twap" with "durationSec": 3600 spreads it over an hour while keeping the legs within 15% of each other the whole way.
Warning
Two intents on the same coin for the same user work independently and can trade against each other's purpose (one buying while the other sells). Keep one working intent per coin per user.
Getting Access
Ask the Borsa team for an API key. Tell us the name to show users and where you'd like revenue share paid.