When to Use
Use this skill when a trading system that spoke to the retired Coinbase Pro API
(POST /orders on api.pro.coinbase.com, later api.exchange.coinbase.com) has to
place the same orders through the Coinbase Advanced Trade API v3
(POST https://api.coinbase.com/api/v3/brokerage/orders). Coinbase discontinued the
Pro API in favour of Advanced Trade and retired the Pro platform itself in November
2023; the Exchange (institutional) APIs are a separate product that continues to exist.
The migration looks like a field rename and mostly is. It covers the parts that are not, because each one silently changes what an order does rather than failing loudly:
- A flat body becomes a nested
order_configurationkeyed by type and time-in-force together — so a droppedtime_in_forceturns an IOC order into a resting one. - The legacy
stopfield (loss/entry) becomesstop_direction, and the mapping does not follow fromside. - A legacy market buy sized in quote currency (
funds) must becomequote_size, neverbase_size. - Authentication changes scheme entirely, not just credentials.
When NOT to Use
- When the official SDK is adequate.
coinbase-advanced-pyhandles JWT minting, transport and retries. Hand-rolling the payload is justified by a specific control requirement — an existing order model you are porting, or a translation layer you want under test — not by default. - For authentication. This skill's module builds request bodies only. Advanced Trade auth is a per-request ES256 JWT, a scheme this module deliberately does not implement; see Prerequisites.
- For Coinbase Exchange (institutional) or Coinbase International. Those are different APIs with their own order schemas. Nothing here applies to them.
- As a market-data or WebSocket migration. Advanced Trade's WebSocket feed has its
own auth and channel model. See
websocket-reconnection-with-state-recovery. - For order types with no legacy counterpart —
trigger_bracket_*,twap_limit_gtd,sor_limit_ioc,scaled_limit_gtc. These are new instructions to design deliberately, not the output of a translation.
Prerequisites
- A CDP API key created as ECDSA. Advanced Trade authenticates with a JWT signed ES256; Coinbase's documentation states Ed25519/EdDSA keys are not supported for this API surface. Legacy Coinbase API keys were deprecated on 5 February 2025, and legacy Pro keys were deactivated when Pro was retired — an old key/secret/passphrase triple cannot be made to work here.
- A JWT minted per request, carrying
kidand anoncein its header and auriclaim of the form{METHOD} {HOST}{PATH}, sent asAuthorization: Bearer <jwt>. The token expires two minutes after issue; one long-lived token reused across a session will start failing mid-run. - The legacy order fields you are porting, including
stop,time_in_force,cancel_after/funds— not justproduct_id/side/type/price/size. If your order model never persistedstopandtime_in_force, recovering them is the first migration task, before any translation code. - Python 3.10+. Standard library only (
decimal,uuid,logging). No HTTP client is bundled: the module translates, the caller signs and sends.
Workflow
-
Populate
LegacyProOrderRequestfrom the legacy body, not from a summary of it.stop,funds,time_in_forceandend_timeare the fields most order models drop, and they are exactly the ones that carry execution semantics. -
Translate with
CoinbaseAdvancedTradeAdapter.translate_order_request(). It returns the create-order body. Every case it cannot express faithfully raisesValueErrorrather than choosing a default:limit+ GTC →limit_limit_gtc(base_size,limit_price,post_only).limit+ FOK →limit_limit_fok.post_onlywith FOK is rejected: an order that must fill in full immediately while never taking liquidity cannot fill at all.limit+ GTT →limit_limit_gtd, andend_time(RFC3339) is then required.limit+ IOC → rejected. Advanced Trade has no plain limit-IOC configuration. The only IOC limit variant,sor_limit_ioc, routes through Smart Order Routing and is a different execution instruction. Re-express such orders by hand.market→market_market_ioc:funds→quote_size(BUY only),size→base_size. A SELL withfundsis rejected — Advanced Trade sizes market sells in base units only, so there is nothing to translate it to.stop→stop_limit_stop_limit_gtc/_gtd, withstop_directiontaken from the legacystopfield:loss→STOP_DIRECTION_STOP_DOWN,entry→STOP_DIRECTION_STOP_UP. A stop order withoutstopis rejected.
-
Decide the
client_order_idbefore you send, not after a failure. Supplyclient_oid. The adapter generates a UUID when it is absent and logs a warning, because a generated id is fresh on every call — so a retry after a timeout submits a second distinct order. A stable id is what makes re-submission safe. -
Send the body yourself to
POST /api/v3/brokerage/orderswith the bearer JWT. -
Parse with
parse_v3_response(), and read the body, not the status code. A rejection arrives assuccess: falseinside a response that may still be HTTP 200. The adapter raisesAdvancedTradeOrderRejected(aRuntimeErrorsubclass) carryingfailure_reason,error_detailsandraw_responseso the caller can classify the rejection. On success it returnsstatus="ACCEPTED"— acceptance, not a live order state. -
Reconcile before any re-submission. If the response was lost, ambiguous, or reported success without an
order_id, queryGET /api/v3/brokerage/orders/historical/batchbyclient_order_idand confirm the order's absence before sending again. Seeorder-placement-idempotency.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Inferring
stop_directionfromside. BUY →STOP_UP/ SELL →STOP_DOWNis right for the two common cases and inverts the trigger on the other two: a sell stop-entry sits above the market and a buy stop-loss sits below it. An inverted stop does not error — it triggers on the wrong side of the price, which for a protective stop means it never fires when it is needed. - Dropping
time_in_forceduring the flatten-to-nested rewrite. It disappears from the top level and reappears inside the configuration key, so a mechanical field-by-field port loses it and defaults everything to GTC. A legacy IOC or FOK order then rests on the book, holding exposure the strategy believes it never took. - Sending a market buy's quote amount as
base_size. Legacyfunds="500"means 500 USD; asbase_sizeit means 500 BTC. Advanced Trade accepts either sizing field for a BUY, so nothing rejects the mistake at the schema level. - Reusing legacy Coinbase Pro API keys. They cannot authenticate against Advanced
Trade, which needs an ECDSA CDP key and a per-request ES256 JWT rather than
CB-ACCESS-KEY/CB-ACCESS-SIGN/CB-ACCESS-PASSPHRASEHMAC headers. - Treating HTTP 200 as an accepted order. Advanced Trade returns business rejections
in the body with
success: false; a client that only checks the status code will record rejected orders as live positions. - Retrying a timed-out submission with a newly generated
client_order_id. The first request may have been accepted before the response was lost. A fresh id defeats the one duplicate-protection mechanism the API gives you; reconcile byclient_order_idfirst. - Formatting sizes with
str()on a float.str(1e-8)is'1e-08', which is not a decimal string. The adapter renders every numeric field throughdecimal.Decimalandformat(d, 'f'). - Assuming the adapter rounds to the product's increments. It does not — it never
fetches product metadata. Sizes and prices must already respect
base_increment,quote_incrementand the product's minimum size, or Coinbase rejects the order.
Verification
- Translate a legacy sell stop-entry (
side="sell",stop="entry") and confirm the output carriesSTOP_DIRECTION_STOP_UP, notSTOP_DOWN. - Translate a legacy limit order with
time_in_force="IOC"and confirm it raises rather than producinglimit_limit_gtc. - Translate a legacy market buy with
fundsand confirmquote_size— notbase_size. - Parse a
{"success": false, ...}body and confirmfailure_reasonanderror_detailssurvive onto the raised exception. - Run
python -m unittest discover -s skills/coinbase-advanced-trade-api-migration/scripts.