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

  1. Borsa gives you an API key.
  2. 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.
  3. You POST /intents. Borsa validates it, records the arrival price of every leg and starts working it.
  4. 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…" }
  • expiresAt is a Unix time in milliseconds, at most 366 days ahead.
  • nonce must be higher than the user's previous delegation to you (a timestamp works). A new delegation replaces the old one.
  • chainId is 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

FieldType
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

FieldDefault
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

MethodPath
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:

Typedata
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": … }.

StatusCode
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.