Gemini¶
kind = "gemini" trades Gemini's linear perpetuals and spot through one WebSocket API; the example configuration is configs/gemini-sandbox.toml.
| Markets | linear perpetuals (btcgusdperp), spot (btcusd) |
| Order entry | WebSocket API; REST for a cancel while it is down |
| Environments | sandbox (the default), production |
| Keys | account-scoped key (account-...) with a time-based nonce |
| Replace | none: cancel and new |
| Dead man's switch | cancelOnDisconnect, on; optional heartbeat |
| Example config | configs/gemini-sandbox.toml |
Keys for the section are in Configuration: gemini.
Connections¶
| Channel | Endpoint | Purpose |
|---|---|---|
| md | ws_url + ?snapshot=-1 |
{sym}@depth@100ms, {sym}@bookTicker, {sym}@trade; time for the clock offset |
| order | ws_api_url (default ws_url) + ?cancelOnDisconnect=true |
authenticated at the upgrade; orders@account, balances@account, order.place, order.cancel |
| rest | rest_url |
the requests below |
| Request | Use |
|---|---|
GET /v1/symbols/details/{symbol} |
reference data |
POST /v1/orders |
key check at start-up, reconciliation |
POST /v1/positions |
reconciliation, with a perpetual subscribed |
POST /v1/mytrades |
trade replay, per symbol |
POST /v1/balances, /v1/margin |
balances |
POST /v1/perpetuals/fundingPayment?since=&to= |
funding |
POST /v1/order/cancel |
a cancel while the order connection is down |
POST /v1/order/cancel/session |
kill switch, order-connection loss |
POST /v1/heartbeat |
with heartbeat = true |
| Environment | REST | WebSocket |
|---|---|---|
| production | api.gemini.com |
ws.gemini.com |
| sandbox | api.sandbox.gemini.com |
ws.sandbox.gemini.com |
The URLs default to the sandbox, and testnet must match the hosts. The sandbox lists more perpetuals than production and returns nothing from the public fundingamount.
Keys and signing¶
REST requests are POSTs with an empty body. The JSON payload goes base64-encoded in X-GEMINI-PAYLOAD, signed with hex HMAC-SHA384 in X-GEMINI-SIGNATURE, next to X-GEMINI-APIKEY. The order connection signs a fresh nonce in the same headers on its upgrade request.
The WebSocket takes only account-scoped keys (account-...) with a time-based nonce; a master- key is refused at start-up. The connector sends venue-time milliseconds, each above the last, because the sandbox refuses a nonce that has not increased. One key serves one process: two clients on a key refuse each other's nonces.
Accounts¶
Perpetuals trade in a derivatives account, opened after Gemini's Derivatives Knowledge Assessment and funded from the exchange account, with a key scoped to it. With a perpetual configured, start-up asks /v1/positions. An exchange account answers AccountNotOfTypeRequired and fastmm-live exits 3; its perpetual orders would be refused with InsufficientFunds.
Instruments¶
| Field | Value |
|---|---|
| Contract | 1 base unit; quantities are base units, contract_multiplier 1 |
| Tick | quote_increment |
| Lot | tick_size, Gemini's name for the quantity step |
| Minimum | min_order_size |
| Base, quote | base_currency, quote_currency; GUSD or USDC for perpetuals, the currency PnL is booked in |
An inverse perpetual is refused. A status other than open, post_only or limit_only disables the instrument.
Market data¶
With snapshot=-1 the first depthUpdate after subscribing is the full book, and the rest are diffs.
| Diff | Action |
|---|---|
U ≤ last u < u |
applied; consecutive frames overlap at U = previous u |
u ≤ last u |
dropped |
U > last u |
gap: Resyncing, then unsubscribe and subscribe the depth stream |
A snapshot looks like any diff, so the first frame after the connection opens, or after the unsubscribe's reply, is taken as the snapshot. Past 512 levels a side the book keeps those nearest the touch. Production spot asks at 1e10 and above do not fit the 8-decimal fixed point and are left out.
The stream has no sequence number and the venue sends no heartbeat. The connector sends ping every ping_interval_ms and reconciles after every reconnect.
Orders¶
| Engine order | Gemini |
|---|---|
| limit | type LIMIT, timeInForce GTC |
| IOC, FOK | timeInForce IOC, FOK |
| post-only | timeInForce MOC |
| market | type MARKET |
| reduce-only | sent as a plain order; Gemini has none |
There is no amend, so supports_replace is off. order.cancel takes the venue's id only, as a string. A cancel of an order not yet named, by the order.place result or its NEW event, waits for it. With the order connection down, a new order is refused (TransportFull) and a cancel goes over REST.
Order events¶
orders@account sends an orderUpdate per change.
| Status | Engine event |
|---|---|
NEW |
ack |
PARTIALLY_FILLED, FILLED |
fill: t, L, Z this execution, z left, m maker; fee n on FILLED only |
CANCELED |
cancel ack with the cumulative Z |
CANCELED for MakerOrCancelWouldTake, ImmediateOrCancelWouldPost, FillOrKillWouldNotFill, SelfCrossPrevented, ExceedsPriceLimits |
OrderExpired |
REJECTED |
reject, reason in r |
A refused placement is answered by both a 400 reply and a REJECTED event; the engine gets one reject.
Reconciliation and fills¶
A reconciliation replays trades, then reads /v1/orders and /v1/positions as one snapshot (Reconciliation). A perpetual absent from the positions is flat, and spot has no position rows. A broken trade (break = full) is not booked.
The replay is POST /v1/mytrades per symbol: trades at or after timestamp (ms), at most 500 a page, listed newest first. Rows carry tid, order_id and client_order_id, so no order lookup is needed; the next page starts from the newest row (Execution replay).
Balances¶
| Source | Rows |
|---|---|
POST /v1/balances |
per asset: free available, locked amount - available, total and equity amount |
POST /v1/margin, with a perpetual |
account row: free available_margin, locked initial_margin, total and equity margin_assets_value, maintenance margin_maintenance_limit |
balances@account on the order connection |
updates; a perpetual fill asks for the snapshot again |
An exchange account answers /v1/margin with AccountNotOfTypeRequired and gets no account row. The snapshot's venue time is the reply's latest _timestamp; a push's is u.
Mark price and funding¶
| Stream | Carries |
|---|---|
{sym}@markPrice on wss://ws.gemini.com |
mark p every 5 s; i is the index times an undocumented per-symbol factor, so no index is sent |
{sym}@fundingAmount |
each minute: f, the estimated funding of one long contract over the hour in quote; rate f / p per i minutes (60); next funding T |
Both are subscribed for perpetuals in their own request and are not in the published docs. Neither sends a snapshot on subscribe, so funding arrives up to a minute after connect. There is no open interest (Mark, index and funding).
Funding payments come from perpetuals/fundingPayment: hourly transfers, Debit paid and Credit received, in assetCode. The query runs with every reconciliation's replay and once a minute. A transfer has no id, so its symbol and time are its id.
Dead man's switch¶
cancel_on_disconnect (on by default) opens the order connection with cancelOnDisconnect=true. The venue then cancels the orders placed on it when it closes.
A key with "Requires Heartbeat" has its orders cancelled after 30 s without an authenticated request. heartbeat = true sends POST /v1/heartbeat every 15 s, and a lapse trips DeadMansSwitchLost.
The kill switch and an order-connection loss send order/cancel/session: the key's orders on every symbol, since Gemini has no per-symbol cancel-all. A 429 is waited out. Gemini has no batch order entry.
Rate limits¶
| Limit | Value |
|---|---|
| REST, private endpoints | 600 a minute, 5 a second recommended; a burst of 5 queued, then 429 |
WebSocket, from conninfo |
3500 orders and 7000 request weight per 10 s |
client-side orders_per_second |
default 20 |
A 429 or -1003 pauses sending for 1 s.
Connection health¶
stale_ms defaults to 2000 ms. dead_ms is at least twice ping_interval_ms plus 5000 ms, 30000 ms by default.
Errors¶
| Error | Action |
|---|---|
RateLimit, 429, -1003 |
rate limit |
InvalidNonce |
clock resync |
InvalidSignature, InvalidApiKey, MissingRole, EndpointMismatch, 401, 403, -1002 |
fatal |
RemoteAddressForbidden |
hard stop |
InsufficientFunds, InvalidPrice, LimitPriceOffTick, InvalidQuantity, DuplicateOrder, MakerOrCancelWouldTake, ExceedsPriceLimits |
reject |
Not handled¶
The archived v1 and v2 streams, stop orders, positions@account (documented for prediction markets), OAuth, master keys and their account field.
Unconfirmed¶
- The sandbox's
/v1/mytradesreturns[]even for hours-old trades, so on the sandbox the replay finds nothing. A fill the order stream missed is then booked only as the cumulative quantity a snapshot reports. - Whether production's
mytradespages forward fromtimestamp, given the list is newest first. - The stream's fee
nwas 0 on every sandbox fill over the WebSocket, while a REST IOC was charged. - Whether
fundingPaymenttakessinceandtoin the query; perpetual order entry. - How soon
cancelOnDisconnectacts on a connection that dies without a FIN; after a SIGKILL the sandbox cancelled within 2 s. - Whether
orders@accountcovers orders placed over REST. - Whether
btcusdcperpandbtcgusdperpare one book.
Sources¶
Read 2026-09-30: developer.gemini.com, its OpenAPI spec specs/openapi/rest.yaml and AsyncAPI spec specs/asyncapi/websocket.yaml (0.10.7), and the support article "How is the Funding Amount calculated?".