Skip to main content
This section is for teams building their own trading product on VOOI. If you just want to trade, sign up on Ultra or use an existing broker’s app. Brokers provision users on VOOI and read their trading statistics. Users hold their own private keys and session tokens — brokers never see either.
Access is invite-only. Contact our team to get a broker key.

Trust model

VOOI is authentication-agnostic by design. You — the broker — are the sole authority over user identity. Whatever auth method your users see (wallet signature, email, SSO, Passkeys, or carrier pigeon) lives entirely in your app; VOOI neither implements nor sees those flows. The contract between your backend and VOOI is one Ed25519 keypair per user. You generate or collect it however fits your auth model, register the public half with VOOI, and VOOI then executes for whoever signs with the matching private key. VOOI has no opinion about who that signer is. Two consequences to surface to your own users:
  • You can fully impersonate any of your users. PUT /broker/users/public-key lets you rotate any of your users’ keys to one you control, after which POST /user/tokens authenticates as them. This is a direct consequence of you being the identity authority, not a gap — users trusting your app are trusting you accordingly.
  • Mapping your external identities to userIds is your job. VOOI exposes no lookup by wallet address, email, public key, or anything other than userId itself. Persist the userId against whatever external identity your auth flow produces; on reconnect or re-login, reuse it rather than re-registering.

What brokers can and can’t do

Brokers are read-only on statistics and write-only on public keys:
  • Can register users, set or rotate their Ed25519 public keys, configure per-exchange builder / integrator defaults, and pull their trading statistics (orders and trades).
  • Cannot see user session tokens, private keys, wallet addresses, account balances, positions, or any data a user holds privately.

Flow

The end-to-end flow for onboarding and signing in a user:
1

Obtain an Ed25519 keypair for the user

Derive it from a wallet signature, generate it server-side, or let the user generate and keep it — whichever fits your auth model (see Trust model). You only need the public half for registration.
2

Register the user

Your backend calls POST /broker/users with the public key. VOOI returns a userId (UUID) that belongs to the user forever.
3

(Optional) Rotate the public key later

Call PUT /broker/users/public-key whenever the user regenerates their keypair. Rotation automatically revokes that user’s unnamed session tokens.
4

User signs requests and obtains a token

The user signs a message with the private key and calls POST /user/tokens to get a vooi_… bearer token. See API Tokens for the signature format.
5

User trades; broker observes

The user places orders, opens positions, and trades against VOOI directly. Your backend reads their statistics from GET /broker/orders and GET /broker/trades.

Quickstart

Authenticate every /broker/* request with your broker key in the X-Broker-Key header. Keep this key server-side — never ship it in a client app.

1. Register a user

Store the returned id — it’s the user’s permanent UUID:
Registration is not idempotent. Every successful POST /broker/users call returns a new userId. Dedupe on your side: map whatever identity your auth flow produces (wallet address, email, SSO subject, …) to the userId VOOI returned, and only call once per user. On reconnect or re-login, look up the existing userId from your mapping and reuse it — VOOI has no recovery path if you lose it.publicKey is optional in the request. A user registered without one cannot mint tokens — POST /user/tokens returns 400 Public key not set until you attach one with the PUT call below.

2. Rotate the public key (when needed)

Responds 204 No Content on success. Pass "publicKey": null to clear it (the user will not be able to mint new tokens until a key is set again). Submitting the user’s existing key is a no-op — no tokens are touched. Rotation affects that user’s outstanding tokens as follows:
  • Session tokens (unnamed) are deleted. The next request carrying one gets 401 Invalid or expired session.
  • Named tokens are left alone. They keep working until their own expiresAt. If you need a hard re-auth, enumerate them with the user’s bearer via GET /user/tokens and delete each one.
  • Historical orders, trades, and positions are untouched. Rotation only changes signature verification for future createToken / deleteToken calls.

3. Read user activity

All broker read endpoints use cursor pagination and share a { cursor, items } envelope — cursor is null on the final page; pass it back unchanged on the next request.
  • GET /broker/orders and GET /broker/trades — filters: userId, exchange, from, to, cursor, limit (1–1000, default 100). Ordered newest first; ties are broken deterministically so cursor paging stays stable. When both from and to are set, the window must not exceed 30 days.

4. Read statistics

Several endpoints cover stats — they answer different questions: GET /broker/stats and GET /broker/users/stats require from and to as Unix milliseconds, hard-reject with 400 Bad Request if to - from > 30 days, and (for users/stats) reject to set in the future. Ranges crossing the 30-day boundary must be split into multiple calls. GET /broker/users/top-volume is the long-window companion to users/stats: it takes ISO dates (default the last 30 days, maximum range 180 days, not earlier than 2026-01-01), pages up to limit 1–1000 (default 100), and accepts an optional exchanges filter. Reach for users/stats when you need tradesCount or a strict 30-day executed-volume window, and users/top-volume when you want a deeper leaderboard across a longer period. The three /broker-statistics/daily-* endpoints take from and to as Unix milliseconds, both inclusive. from defaults to 30 days back (1 day at hour resolution), to defaults to now, and from cannot be earlier than 2026-01-01. They also take a resolution of day (UTC calendar day, the default) or hour (UTC clock hour). from and to snap to interval boundaries, and hour limits the range to 7 days. Missing intervals come back as amount: 0, so items always has one row per interval in the window. Optional filters: exchange narrows to one venue, baseSymbolPrefix and baseSymbolNotPrefix keep or drop trades by base-symbol prefix (case-insensitive — useful for isolating a Hyperliquid HIP-3 DEX such as xyz:), and usersStoplist excludes up to 100 user IDs.

Per-user and per-list statistics

The endpoints above aggregate across all of your users or rank them. Two other shapes are available when you have specific users in hand. All of them share the from / to / resolution conventions described above. For one user, by query parameter (userId): For a list of users, by JSON body carrying a usersList of 1–100 entries (each an id plus an optional startedFrom timestamp clamping how far back that user’s trades are counted): Both accept an optional exchanges array to count trades only on those venues. Excluded venues still appear in the response with zero amounts.

Broker vs user read scope

Brokers read only what these endpoints return: aggregate and per-day stats, per-user stats, and (userId-filterable) order / trade history. Per-user positions, balances, open orders, and account settings are not exposed on the broker API — that data lives behind the user’s bearer token on the /exchange routes.

Configure exchange defaults

Each venue passes a “broker” record on every order — a builder address (or integrator account index for Lighter and Robinhood) plus fees in basis points. Binance and MEXC are the exceptions: their broker records are attribution identifiers only, with no fee fields. Brokers can register one default per venue so that orders from any of their users automatically carry those values. When a default is set, it overrides the per-order broker field on POST /exchange/orders — your users cannot opt out from the client side. Without a default, the order’s own broker is used (or no builder if it was omitted). Settings are versioned rather than edited in place. Each write adds a record effective from that moment; the most recent record is the one in force, and statistics apply whichever record was active when each order was created. Past figures therefore stay stable when you change your fees.
  • POST /broker/exchanges — add a settings record for one venue, effective now. Responds 201. Body fields:
    • exchange — aster, hyperliquid, lighter, robinhood, extended, binance, or mexc.
    • identifier — the EVM builder address (Aster, Hyperliquid), the integrator account index (Lighter, Robinhood), or, on Extended, the Builder clientId from the Extended UI API page (a positive-integer string, not an EVM address or account index). On Binance, identifier is an attribution tag (up to 24 characters from A–Z a–z 0–9 . / : _ -) prepended to auto-generated client order IDs. On MEXC it is the Broker_ID MEXC issued you. Neither Binance nor MEXC takes fee fields.
    • marketFeeBps — fee in basis points, as a positive decimal string, charged on orders the venue executes as market orders. Trigger orders count as market on Hyperliquid, Aster, and Ondo.
    • limitFeeBps — the same, for orders the venue executes as limit orders. On Lighter and Robinhood the two are applied as the taker and maker fees respectively.
    • maxFeeBps — fee cap in basis points, used as the default during user-side approval. Both marketFeeBps and limitFeeBps must be ≤ maxFeeBps; the call returns 400 otherwise. On Hyperliquid this becomes the cap baked into the user’s approveBuilderFee signature, so raising the cap later requires the user to re-sign. On Aster, the fees still cannot exceed the cap baked into the user’s Aster registration.
  • GET /broker/exchanges — list the settings currently in force. Each row carries identifier, marketFeeBps, limitFeeBps, and maxFeeBps; the fee fields are null on venues that have no builder fee.
  • DELETE /broker/exchanges/{exchange} — deactivate the venue. Responds 204 No Content. The exchange stops being listed by GET /broker/exchanges, new orders carry no builder attribution, and builder approval falls back to caller-supplied identifier and maxFeeBps. Earlier records remain in effect for past statistics.

Approve flow fallback

A user trading through your app only has to run the user-side approve flow when your broker actually uses builder fees on a venue — see Approve a builder fee on Hyperliquid. One pair of endpoints serves all supported venues: Pass exchange (hyperliquid, lighter, robinhood, or aster) in the body. When a default is configured here, the user-side prepare call can omit identifier and maxFeeBps and picks them up from the broker record; if they are passed explicitly they must match the configured defaults exactly, or the call returns 400. Without a broker default, both fields are required — typically the case for users connecting their own wallets directly. The signer differs by venue: Hyperliquid and Aster expect an EIP-712 signature; Lighter and Robinhood expect a hex message signed with an Ethereum wallet. For Aster, an account that approved a builder during Aster registration can re-run this flow later to raise the cap or rotate the builder without re-registering. Every other venue is an exception: none of Extended, Binance, Bybit, MEXC, or Ondo needs a user-side builder-fee approval. Extended’s builder code applies automatically per order, and Binance and MEXC attribution is identifier-only — so the approve endpoints accept only hyperliquid, lighter, robinhood, and aster.

API reference

Full request and response schemas for every broker endpoint are in the API reference.

Next steps

End-user API tokens

How your users sign requests and obtain session tokens

Connecting exchanges

Aster, Hyperliquid, Lighter, Robinhood, Extended, Binance, Bybit, MEXC, and Ondo key setup