Gemini's authenticated endpoints — internal. Balances, orders and trade history.
The boundary, precisely
The host authenticates. This module makes authenticated requests with what the host
hands it. Those are different jobs and the difference is the whole design: a caller
passes credentials into every one of these functions, they are used to sign that one
request, and nothing is kept. This module never obtains a credential, never stores one,
never refreshes one, and never decides which authentication scheme applies — see Auth,
which refuses to guess.
Which means trading works exactly as the facade intends: the host does auth, calls
place_order/3 with credentials, and gets a Core.Types.Order back.
Gemini has no market orders, and this package will not fake one
From the venue's own page:
The API doesn't directly support market orders because they provide you with no price protection. Instead, use the "immediate-or-cancel" order execution option, coupled with an aggressive limit price (i.e. very high for a buy order or very low for a sell order), to achieve the same result.
That advice is not something a package may take on a caller's behalf. "An aggressive limit price" means inventing a number the caller did not supply and sending it to a live exchange as a real order. How aggressive? Ten percent through the book? Fifty? The package cannot know, the caller never said, and the difference is money.
So order_type: :market is {:error, {:unsupported_order_type, :market}} — the venue
does not serve it, and the nearest thing requires a price only the caller can choose. A
caller who wants that behaviour asks for it explicitly, with their own limit:
%{order_type: :limit, time_in_force: :ioc, price: my_aggressive_price, …}This is the family's named failure mode in its most expensive form. Every other instance in this codebase costs a wrong number in a chart; this one costs a fill at a price nobody chose.
Order execution options are mutually exclusive
If you specify more than one option (or an unsupported option) in the options array, the exchange will reject your order.
So time_in_force maps to exactly one option, or none:
time_in_force | Gemini option | Meaning |
|---|---|---|
:gtc (default) | none | fills what it can, rests the remainder on the book |
:post_only / :maker_or_cancel | maker-or-cancel | adds liquidity only, cancels if it would take |
:ioc | immediate-or-cancel | takes what it can, cancels the rest |
:fok | fill-or-kill | fills entirely or cancels entirely |
No option may be combined with a stop-limit order.
A cancelled order is not a failed request
MOC, IOC and FOK orders that do not fill come back 200 with "is_cancelled": true.
That is the venue answering successfully; the order simply did not rest. It maps to
status: :cancelled on a {:ok, order}, never to an error — a caller that treated it as
a failure would retry an order the venue already handled.
Auth failures are refusals, not errors
Measured 2026-08-28 against the demo environment: an unauthenticated POST to any private
endpoint returns 401 MissingSecurityHeaders. Gemini's own error table documents
MissingApikeyHeader at 400, so the documented codes and the live ones disagree —
the fourth documentation divergence found on this venue.
Either way 400, 401 and 403 are permanent for the request as sent: retrying the
identical bytes cannot succeed. They are {:refused, reason}. A caller whose token
expired refreshes it and calls again with new credentials, which is a different request —
not a retry of this one.
Summary
Functions
Cancels one order by the venue's order id.
The account's own record of itself — name, type, and the roles the key carries.
Every currency the account holds, with what is available and what is on hold.
The fee tier this account trades at, from /v1/notionalvolume.
One order's current state.
Every order currently resting on the book for this account.
Past fills for a symbol.
Transfers in and out of the account.
Places an order.
Confirms the credentials reach the venue, using its own heartbeat endpoint.
Functions
@spec cancel_order(map(), String.t(), keyword()) :: {:ok, DpExchange.Core.Types.Order.t()} | {:error, term()} | {:refused, term()}
Cancels one order by the venue's order id.
The account's own record of itself — name, type, and the roles the key carries.
@spec get_balances( map(), keyword() ) :: {:ok, [DpExchange.Core.Types.Balance.t()]} | {:error, term()} | {:refused, term()}
Every currency the account holds, with what is available and what is on hold.
The fee tier this account trades at, from /v1/notionalvolume.
Returned as the venue states it — basis points, per maker/taker, alongside the notional volume that determined the tier. Nothing is converted to a rate, because the venue's own units are what a caller will reconcile against.
@spec get_order(map(), String.t(), keyword()) :: {:ok, DpExchange.Core.Types.Order.t()} | {:error, term()} | {:refused, term()}
One order's current state.
@spec get_orders( map(), keyword() ) :: {:ok, [DpExchange.Core.Types.Order.t()]} | {:error, term()} | {:refused, term()}
Every order currently resting on the book for this account.
@spec get_trade_history( map(), keyword() ) :: {:ok, [DpExchange.Core.Types.Fill.t()]} | {:error, term()} | {:refused, term()}
Past fills for a symbol.
Gemini requires a symbol here — there is no all-symbols variant — so a caller asking for everything is asking for one request per symbol, and it is theirs to decide whether to.
Transfers in and out of the account.
@spec place_order(map(), map(), keyword()) :: {:ok, DpExchange.Core.Types.Order.t()} | {:error, term()} | {:refused, term()}
Places an order.
request is a map carrying at least :symbol, :side, :quantity and :price.
:order_type defaults to :limit; :time_in_force defaults to :gtc. A
:client_order_id is passed through when given and is strongly recommended by the venue.
Refuses rather than substituting:
order_type: :market— the venue serves none, and the documented workaround needs a limit price only the caller can choose- an unknown
:time_in_force— the venue rejects an unsupported option outright - a missing price on a type that requires one
Confirms the credentials reach the venue, using its own heartbeat endpoint.
A real round trip rather than a guess: /v1/heartbeat is authenticated, so a success
proves the key, the signature and the nonce mode are all right. It also resets the
session's cancel-on-disconnect timer, which is a side effect worth knowing about — on a
key provisioned with Requires Heartbeat, calling this keeps open orders alive.