DpExchange.Gemini (DpExchangeGemini v0.1.1)

Copy Markdown View Source

Gemini, behind the DpExchange facade.

⚠️ EXPERIMENTAL

This package has not run in production. While it is 0.x the API may change without a major version — pin all three segments. Maturity is declared per endpoint through capabilities/0; do not read this banner as your check.

This module is the entire public API of this package. Transport, signing, session handling and supervision are internal, and there is nothing here that returns them.

What is specific to Gemini, and what a caller can therefore rely on

Seven candle widths, and the venue's own documentation names three of them wrong. The accepted set is 1m 5m 15m 30m 1h 6h 1d in canonical form. Gemini's documentation lists values that its API rejects; this package sends what the venue accepts, measured. Asking for a width Gemini does not serve — 2h, 4h, 12h — is an error, never the nearest one.

The candle window is fixed and unbounded requests are refused. Gemini ignores start, end and limit entirely and returns a fixed window per width, from 1 day of one-minute bars to a year of daily ones. This package filters to your range, and refuses with {:error, {:range_unavailable, …}} when your range starts before the window can reach — rather than handing back a shorter period that reads as a complete answer.

Quote timestamps come from the venue's clock, or the call fails. Neither Gemini ticker publishes a quote timestamp — /v1/pubticker's only timestamp stamps its 24-hour volume window, about a minute stale. This package uses the venue's HTTP Date header and returns {:error, :missing_venue_timestamp} when it is absent. It never substitutes the local clock.

Gemini publishes no rate-limit headers. Measured 2026-08-28: a response carries date, x-request-id and x-envoy-upstream-service-time, and nothing else. It does publish its limits, in prose, which is better — and capabilities/0 carries all three GCRA parameters from the venue's own page rather than from a guess.

The demo environment is first-class here

Gemini runs a full exchange with test funds — its own words — where automated bots simulate order-book activity and new accounts are credited $100,000 USD, 1,000 BTC and 20,000 each of ETH, BCH, ZEC and LTC. Point this package at it with one option:

children = [{DpExchange.Gemini, environment: :sandbox}]

{:ok, quote} = DpExchange.Gemini.get_price("BTC-USD", environment: :sandbox)

Both REST and the WebSocket follow the setting. :production is the default, and deliberately so: a wrong default fails in only one direction. Meaning demo and getting production sends a real order to a real exchange; meaning production and getting demo gives obviously-wrong prices — the demo book is frequently crossed, and a frame captured 2026-08-28 carried a bid of 68169.88 against an ask of 64886.32.

Selecting it once for a whole process tree, rather than per call, goes through DpExchange.Core.Config — which resolves per process, so one async test can point at demo without redirecting every test running beside it.

You authenticate; this package signs

Account and trading are implemented, and credentials arrive as arguments — used to sign that one request and not kept:

{:ok, balances} = DpExchange.Gemini.get_balances(credentials, [])
{:ok, order} = DpExchange.Gemini.place_order(credentials, request, [])

What stays yours is credential storage and the choice of scheme. Gemini offers an API key pair and a full OAuth 2.0 authorization-code flow — app registration, a permanent client type, redirecting users to approve scopes, PKCE, and refreshing a 24-hour token — and which one an application uses is a decision about its users and its deployment. Name it with auth_scheme: :api_key | :oauth; with it absent, whichever you actually supplied is used. Credentials carrying both are refused rather than resolved, because sending both header families is AmbiguousAuthentication at the venue.

This package never reads a credential from the environment or a vault.

Gemini serves no market orders, and this package will not invent one

capabilities/0 declares supported_order_types: [:limit, :stop_limit]. The venue's own reasoning: market orders "provide you with no price protection". Its documented workaround is an immediate-or-cancel order "coupled with an aggressive limit price" — and that price is not one a package may choose for you. How aggressive is a question only the caller can answer, and the answer is money.

So order_type: :market is {:error, {:unsupported_order_type, :market}}. Ask for the behaviour explicitly, with your own number:

%{order_type: :limit, time_in_force: :ioc, price: my_price, }

Your API key's nonce mode is also something only you know. Gemini provisions keys in either time-based or incremental mode, and the two need differently-shaped nonces — seconds versus a strictly increasing value, with no single value satisfying both. The default is :time_based, the venue's own recommendation; pass nonce_mode: :incremental if that is how your key was made. A mismatch fails loudly on the first request.

Supervision

Add it to your own tree. Nothing starts on load — a consumer that has not asked for Gemini must not find a socket open.

children = [{DpExchange.Gemini, []}]

{:ok, quote} = DpExchange.Gemini.get_price("BTC-USD", [])
:ok = DpExchange.Gemini.subscribe(["BTC-USD"], to: self())