Coinbase, 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 Coinbase, and what a caller can therefore rely on
Credentials choose the endpoint; they do not gate it — with one exception. Coinbase
serves almost all market data publicly and authenticated. Pass credentials and this
package uses the authenticated path, which has the higher ceiling; pass none and it uses
the public one. Either way you get the same Core.Types.* back, and capabilities/0
tells you what the difference bought. get_top_of_book/2 is the one exception: the
venue publishes no public form of /best_bid_ask — confirmed live, 401 authenticated
and 404 at the /market/... path a caller would expect — so this one call is
{:error, {:missing_credentials, :coinbase}} without them rather than a nearer
substitute. Not {:refused, _}: the credential never left this process, so nothing at
Coinbase ever saw the request to decline it.
Nine candle widths, and 12h is not one of them. The shared vocabulary models it;
Coinbase does not serve it. Asking for a width Coinbase does not serve is an error,
never the nearest one — a caller handed one-hour bars labelled four-hour has every value
real and every label wrong, which is how it went unnoticed for weeks.
350 candles per request is a hard boundary. Measured: 350 minutes of one-minute
candles returns 349; 351 returns zero and an error, not the first 350. This package
refuses an over-wide range up front with {:error, {:range_too_wide, …}} rather than
sending it and handing back an empty result, because empty reads as "no data for this
period".
Coinbase publishes no rate-limit headers. Measured 2026-08-28: no x-ratelimit-*,
no retry-after. The ceilings in capabilities/0 are declared, and
measured_against says exactly where they came from.
Supervision
Add it to your own tree. Nothing starts on load — a consumer that has not asked for Coinbase must not find a socket open.
children = [{DpExchange.Coinbase, credentials: my_credentials()}]
Summary
Functions
Cancels the pending sweep — the venue takes no id.
Claims accrued Prime staking rewards for one wallet. This moves funds. See
DpExchange.Coinbase.Prime.claim_rewards/4.
Flattens an open position by having the venue place the closing order.
Commits a quoted conversion. This moves funds.
coverage/1, split by which DpExchange.Core.Types struct is actually arriving.
Creates a portfolio, which is what this venue calls a sub-account.
Deletes a portfolio. Irreversible from this package's side.
The venue's own account records — uuid, platform, portfolio, tradability.
Every balance the credential can see, one per account.
A conversion's current state.
Which margin window the account is in now, and whether the kill switches are on.
The fee schedule that applies to this credential.
The futures account's balances and margin.
One futures position by product id — expiry included.
The account's intraday margin setting.
The order book for symbol.
One funding source by id.
One portfolio's full breakdown — balances, positions and margin inside it.
Open futures positions in the CFM account.
What this API key is allowed to do.
The venue's own clock. Public.
Past fills for the credential.
What this account has traded.
Recent public trades — the tape.
The venue's own futures position rows, unnormalised.
Pending and processing sweeps out of the futures account.
The funding sources this account can move fiat through.
The portfolios this credential can address.
Not supported. Advanced Trade places one order per request.
Previews an order without placing it.
Prices an amendment to a working order without making it.
What a wallet-scoped Prime unstake would do, without doing it — see
DpExchange.Coinbase.Prime.preview_unstake_wallet/6.
What the venue will actually accept for symbol.
The validators a Prime staking transaction would touch, across a portfolio —
see DpExchange.Coinbase.Prime.query_transaction_validators/3.
Quotes a conversion. Nothing moves.
Renames a portfolio.
Changes the price or size of a working order, in place.
Schedules a sweep from the futures account to the spot one. This moves funds.
Sets the account's intraday margin setting. This changes how much leverage it gets.
Stakes amount of asset through Coinbase Prime. This moves funds.
One Prime wallet's staking state — see DpExchange.Coinbase.Prime.staking_status/4.
Whether the venue is reachable and the credential, if given, is accepted.
Moves funds between two of this account's portfolios.
Redeems amount of a staked asset through Coinbase Prime.
How far a Prime wallet's redemption has got — see
DpExchange.Coinbase.Prime.unstake_status/4.
Endpoints the venue does not serve, as distinct from ones this package has not ported.
Functions
@spec cancel_futures_sweep( map(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
Cancels the pending sweep — the venue takes no id.
@spec claim_rewards( DpExchange.Coinbase.Prime.credentials(), String.t(), String.t(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
Claims accrued Prime staking rewards for one wallet. This moves funds. See
DpExchange.Coinbase.Prime.claim_rewards/4.
A write, not a report. It does not say what has accrued; it moves what has. Not
get_staking_rewards/1 — that callback is declared unsupported on this venue
precisely because there is no read for what this writes; see venue_does_not_serve/0.
Flattens an open position by having the venue place the closing order.
See DpExchange.Coinbase.Rest.close_position/3, including why the returned order carries
no side.
Commits a quoted conversion. This moves funds.
See DpExchange.Coinbase.Rest.commit_conversion/3. The venue re-asks for both accounts and
this package fills neither in — opts[:from] and opts[:to] are required.
@spec coverage_by_kind(keyword()) :: %{ required(DpExchange.Core.Capabilities.data_kind()) => %{ required(DpExchange.Core.Venue.symbol()) => DpExchange.Core.Venue.route() } }
coverage/1, split by which DpExchange.Core.Types struct is actually arriving.
coverage/1 is honest but coarse: it answers "is anything arriving for this symbol" by
counting any payload at all, so a Core.Types.OrderBook update counts exactly the same
as a Core.Types.Quote — a symbol whose book is healthy and whose quotes have gone dark
looks identical to one where both are fine. That is not hypothetical here: level2
delivered over 11,000 frames across 406 symbols while ticker was dark on all but a
handful, and coverage/1 reported :stream for all 406, correctly by its own
definition and uninformative about the gap. Two DpCryptoManagement issues (#20, #22)
stayed unpinned for days because nothing distinguished "book healthy, ticker dark" from
"everything healthy".
This answers the same observed-arrival question coverage/1 does, partitioned by
DpExchange.Core.Capabilities.data_kind/0 instead of collapsed across it. The kind
comes from which struct arrived — %DpExchange.Core.Types.Quote{} is :quotes,
%DpExchange.Core.Types.OrderBook{} and %DpExchange.Core.Types.OrderBookDelta{} are
both :order_book — never from this venue's own channel names (level2, ticker),
which never cross the facade.
Optional on DpExchange.Core.Venue.coverage_by_kind/1 and implemented here because
Coinbase is the venue that motivated it. See
DpExchange.Coinbase.Feed's moduledoc, "coverage_by_kind/1" section, for how delivery
is tracked per kind.
Creates a portfolio, which is what this venue calls a sub-account.
opts[:name] is required. See DpExchange.Coinbase.Rest.create_portfolio/2.
Advanced Trade has no notion of creating an account — an account is opened by a person with Coinbase. A portfolio is the subdivision an API can make, and it is what this callback means here.
@spec delete_portfolio(map(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} | {:refused, term()}
Deletes a portfolio. Irreversible from this package's side.
See DpExchange.Coinbase.Rest.delete_portfolio/3 — the venue refuses while the portfolio
holds funds or open orders, which is the venue's guard and not this one's.
The venue's own account records — uuid, platform, portfolio, tradability.
Separate from get_balances/2 because a caller routing an order needs the uuid and a
caller sizing one needs the balance. opts[:uuid] reads a single account.
Every balance the credential can see, one per account.
See DpExchange.Coinbase.Rest.get_balances/2 — in particular why the total is the sum of
the venue's two numbers and nil when either is missing.
A conversion's current state.
See DpExchange.Coinbase.Rest.get_conversion/3. Both accounts are required query
parameters here — the venue's own rule, and unusual for a read.
@spec get_current_margin_window( map(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
Which margin window the account is in now, and whether the kill switches are on.
The fee schedule that applies to this credential.
See DpExchange.Coinbase.Rest.get_fees/2. Both the promotional tier and the tier without
the promotion travel, because they differ while one is running and it can end between two
calls.
@spec get_futures_balance_summary( map(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
The futures account's balances and margin.
See DpExchange.Coinbase.Rest.get_futures_balance_summary/2. Two accounts are named —
the spot one held with Coinbase Inc and the futures one held with Coinbase Financial
Markets — and only the second margins a position.
@spec get_futures_position(map(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} | {:refused, term()}
One futures position by product id — expiry included.
@spec get_intraday_margin_setting( map(), keyword() ) :: {:ok, String.t()} | {:error, term()} | {:refused, term()}
The account's intraday margin setting.
See DpExchange.Coinbase.Rest.get_intraday_margin_setting/2. UNSPECIFIED is the venue
declining to say, and is not STANDARD.
The order book for symbol.
See DpExchange.Coinbase.Rest.get_order_book/2 — including why the levels are not
re-sorted here, and why an undated book is refused.
One funding source by id.
See DpExchange.Coinbase.Rest.get_payment_method/3. This is the read;
list_payment_methods/2 is a snapshot.
@spec get_portfolio_breakdown(map(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} | {:refused, term()}
One portfolio's full breakdown — balances, positions and margin inside it.
See DpExchange.Coinbase.Rest.get_portfolio_breakdown/3. Not list_portfolios/1
narrowed to one: the listing names portfolios, this returns what is inside one.
Open futures positions in the CFM account.
See DpExchange.Coinbase.Rest.get_positions/2. :realised_pnl is nil because the venue
publishes a daily figure and this field means the position's; list_futures_positions/1
returns the venue's own rows, where it keeps its own name.
What this API key is allowed to do.
See DpExchange.Coinbase.Rest.get_key_permissions/2. Three separate permissions, and
can_transfer is the one that moves money. Carries the portfolio the key is scoped to.
The venue's own clock. Public.
See DpExchange.Coinbase.Rest.get_server_time/1. Worth reading because this venue's JWT
window is two minutes: a host clock further out than that produces authentication failures
that look like a credential problem.
Past fills for the credential.
See DpExchange.Coinbase.Rest.get_trade_history/2 — in particular why only FILL rows
come back unless opts[:trade_types] says otherwise.
What this account has traded.
See DpExchange.Coinbase.Rest.get_trade_volume/2. This package claimed until 2026-09-01
that Advanced Trade does not aggregate it; the transaction summary does, and the claim had
been made from the market volume endpoint's absence, which is a different question.
Recent public trades — the tape.
See DpExchange.Coinbase.Rest.get_trades/2: get_price/2 reads the same payload and
keeps only the newest print.
The venue's own futures position rows, unnormalised.
@spec list_futures_sweeps( map(), keyword() ) :: {:ok, [map()]} | {:error, term()} | {:refused, term()}
Pending and processing sweeps out of the futures account.
See DpExchange.Coinbase.Rest.list_futures_sweeps/2. A listed sweep has not happened yet.
The funding sources this account can move fiat through.
See DpExchange.Coinbase.Rest.list_payment_methods/2. verified, allow_deposit and
allow_withdraw disagree with each other routinely; presence is not usability.
The portfolios this credential can address.
See DpExchange.Coinbase.Rest.list_portfolios/2. A portfolio is an address, not a value,
and deleted ones stay in the listing because old orders still name them.
Not supported. Advanced Trade places one order per request.
POST /orders takes a single order and POST /orders/batch_cancel is a batch cancel —
the venue's only bulk order operation, and it destroys rather than creates. Calling
place_order/3 in a loop is what a consumer must do here, with the reconciliation that
implies.
Previews an order without placing it.
This used to read "this venue publishes no order-preview endpoint". It publishes
POST /api/v3/brokerage/orders/preview, and supports_order_preview was declared false
on the strength of that claim. Neither was checked against the venue's reference.
Prices an amendment to a working order without making it.
See DpExchange.Coinbase.Rest.preview_replace/4 — in particular why this is not
preview_order/3 with an order id.
@spec preview_unstake_wallet( DpExchange.Coinbase.Prime.credentials(), String.t(), String.t(), String.t(), Decimal.t(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
What a wallet-scoped Prime unstake would do, without doing it — see
DpExchange.Coinbase.Prime.preview_unstake_wallet/6.
A preview is not a reservation. Nothing is held, and the unbonding schedule returned is the schedule as of the moment it was asked.
What the venue will actually accept for symbol.
See DpExchange.Coinbase.Rest.quantization/2 — in particular why the price and quantity
increments are different fields.
@spec query_transaction_validators( DpExchange.Coinbase.Prime.credentials(), String.t(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
The validators a Prime staking transaction would touch, across a portfolio —
see DpExchange.Coinbase.Prime.query_transaction_validators/3.
A read, despite Prime taking the query as a POST body. opts[:query] is the venue's
own filter map and is sent as given — reshaping a vocabulary only Prime defines would
be a second place to be wrong about it.
Quotes a conversion. Nothing moves.
See DpExchange.Coinbase.Rest.quote_conversion/5. Coinbase names accounts by currency, and
expires_at is nil because Advanced Trade states none — which is "not stated", not
"open-ended".
Renames a portfolio.
See DpExchange.Coinbase.Rest.rename_portfolio/4. The only thing this edits is the name.
Changes the price or size of a working order, in place.
This used to read "this venue has no atomic replace; a caller cancels and re-places",
and called that a claim about risk. The risk was real and the claim was wrong: Coinbase
publishes POST /api/v3/brokerage/orders/edit, which amends without ever leaving the
order un-live. The window this package warned a caller about was one it was creating by
not implementing the endpoint that avoids it.
@spec schedule_futures_sweep( map(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
Schedules a sweep from the futures account to the spot one. This moves funds.
See DpExchange.Coinbase.Rest.schedule_futures_sweep/2 — omitting the amount sweeps every
available excess dollar, which is the venue's default and not this package's.
@spec set_intraday_margin_setting(map(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} | {:refused, term()}
Sets the account's intraday margin setting. This changes how much leverage it gets.
Stakes amount of asset through Coinbase Prime. This moves funds.
Prime, not Advanced Trade: a different host, a different signing scheme and a separate
credential triple. opts[:credentials] carries Prime's %{access_key:, passphrase:, signing_key:}; the CDP key pair the rest of this package uses is not accepted there.
opts[:portfolio_id] is required and is not defaulted. Missing it is
{:error, :missing_portfolio} before a request is made — picking the first portfolio
would stake in one the caller never named.
The scope follows what the caller said, and nothing more. With opts[:wallet_id]
this stakes on that wallet; without, it stakes across the portfolio. The two are
different operations, so neither is inferred from the other.
See DpExchange.Coinbase.Prime for the endpoints themselves and for what has and has
not been measured.
@spec staking_status( DpExchange.Coinbase.Prime.credentials(), String.t(), String.t(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
One Prime wallet's staking state — see DpExchange.Coinbase.Prime.staking_status/4.
Not get_staking_balances/1. That callback answers "every staked position, one
per asset" and this venue does not serve it — see venue_does_not_serve/0. This names
one wallet and reports that wallet's state, which is a narrower question answered
correctly rather than the wider one answered wrong.
Whether the venue is reachable and the credential, if given, is accepted.
See DpExchange.Coinbase.Rest.test_connection/2. Without credentials it reads the public
clock; with them it reads the key's permissions, which is both a reachability check and an
answer about what the key can do.
Moves funds between two of this account's portfolios.
See DpExchange.Coinbase.Rest.transfer_internal/4. Nothing leaves Coinbase, and both
portfolio uuids are required — opts[:from] and opts[:to], neither defaulted.
Redeems amount of a staked asset through Coinbase Prime.
Returns before the redemption completes. The asset unbonds on the chain's schedule
and arrives in parts; unstake_status/4 is what reports progress. A caller treating this
return value as settled will spend an asset it does not have yet.
Scope and credentials work exactly as they do on stake/3.
@spec unstake_status( DpExchange.Coinbase.Prime.credentials(), String.t(), String.t(), keyword() ) :: {:ok, map()} | {:error, term()} | {:refused, term()}
How far a Prime wallet's redemption has got — see
DpExchange.Coinbase.Prime.unstake_status/4.
This is the call that says a redemption is not finished. unstake/3 returns
before the asset has unbonded; a caller that never reads this reports a redemption as
complete the moment it was accepted.
Endpoints the venue does not serve, as distinct from ones this package has not ported.
Both answer {:error, :not_supported}, and a caller acts the same way on either — but
they mean different things to anyone deciding what to build next, so they are told apart
here rather than flattened into one list.
Every entry is recorded with its source and the date consulted in
docs/reference/coinbase/negative-claims.md.