DpExchange.Coinbase (DpExchangeCoinbase v0.1.27)

Copy Markdown View Source

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 {:refused, :missing_credentials} without them rather than a nearer substitute.

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.

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 the venue will actually accept for symbol.

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.

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.

Endpoints the venue does not serve, as distinct from ones this package has not ported.

Functions

cancel_futures_sweep(credentials, opts)

@spec cancel_futures_sweep(
  map(),
  keyword()
) :: {:ok, map()} | {:error, term()} | {:refused, term()}

Cancels the pending sweep — the venue takes no id.

See DpExchange.Coinbase.Rest.cancel_futures_sweep/2.

close_position(credentials, symbol, opts \\ [])

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.

commit_conversion(id, opts)

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.

coverage_by_kind(opts \\ [])

@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{} is :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.

create_account(opts)

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.

delete_portfolio(credentials, portfolio_uuid, opts)

@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.

get_accounts(credentials, opts)

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.

get_balances(credentials, opts)

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.

get_conversion(id, opts)

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.

get_current_margin_window(credentials, opts)

@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.

See DpExchange.Coinbase.Rest.get_current_margin_window/2.

get_fees(credentials, opts)

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.

get_futures_balance_summary(credentials, opts)

@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.

get_futures_position(credentials, product_id, opts)

@spec get_futures_position(map(), String.t(), keyword()) ::
  {:ok, map()} | {:error, term()} | {:refused, term()}

One futures position by product id — expiry included.

See DpExchange.Coinbase.Rest.get_futures_position/3.

get_intraday_margin_setting(credentials, opts)

@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.

get_order_book(symbol, opts \\ [])

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.

get_payment_method(credentials, id, opts)

One funding source by id.

See DpExchange.Coinbase.Rest.get_payment_method/3. This is the read; list_payment_methods/2 is a snapshot.

get_portfolio_breakdown(credentials, portfolio_uuid, opts)

@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.

get_positions(opts)

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.

get_roles(opts)

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.

get_server_time(opts \\ [])

@spec get_server_time(keyword()) ::
  {:ok, map()} | {:error, term()} | {:refused, term()}

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.

get_trade_history(credentials, opts)

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.

get_trade_volume(credentials, opts)

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.

get_trades(symbol, opts \\ [])

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.

list_futures_positions(opts)

@spec list_futures_positions(keyword()) ::
  {:ok, [map()]} | {:error, term()} | {:refused, term()}

The venue's own futures position rows, unnormalised.

See DpExchange.Coinbase.Rest.list_futures_positions/2.

list_futures_sweeps(credentials, opts)

@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.

list_payment_methods(credentials, opts)

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.

list_portfolios(opts)

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.

place_orders(credentials, requests, opts)

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.

preview_order(credentials, request, opts \\ [])

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.

preview_replace(credentials, order_id, changes, opts \\ [])

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.

quantization(symbol)

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.

quote_conversion(from, to, amount, opts)

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".

rename_account(id, name, opts)

Renames a portfolio.

See DpExchange.Coinbase.Rest.rename_portfolio/4. The only thing this edits is the name.

replace_order(credentials, order_id, changes, opts \\ [])

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.

schedule_futures_sweep(credentials, opts)

@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.

set_intraday_margin_setting(credentials, setting, opts)

@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.

See DpExchange.Coinbase.Rest.set_intraday_margin_setting/3.

stake(asset, amount, opts)

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.

test_connection(credentials, opts)

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.

transfer_internal(asset, amount, opts, request_opts)

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.

unstake(asset, amount, opts)

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; DpExchange.Coinbase.Prime.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.

venue_does_not_serve()

@spec venue_does_not_serve() :: [{atom(), arity()}]

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.