Coinbase
What the Coinbase connector reads, with which permissions, what it never touches, how it syncs, and the facts it produces.
Last changed Markdown versionllms.txt
Source module: app/connectors/coinbase.py. Kind: facts only. Display name: "Coinbase".
Purpose
Reads the orders and fills of a Coinbase account through the Advanced Trade API, so that a trade an agent
places (through the Coinbase MCP server of Coinbase for Agents, orders_create, or the API directly) can be
verified under the coinbase_agent_trade library contract: did an order on the product the agent was told to
trade exist, once, for the notional asked, did it fill within the window, and was it not cancelled, rejected,
busted or corrected afterwards.
Coinbase's own controls run before the order: the portfolio the agent's key is scoped to, the permissions on the key, the spending cap on x402 payments. This connector adds nothing there. It reads Coinbase's record after the order, so that the agent's report of what it traded is checked against what Coinbase says it traded.
Auth and scopes
| Item | Value |
|---|---|
| Method | CDP API key (Coinbase Developer Platform, "Secret API Keys"); every request carries a two-minute ES256 JWT signed with the key's EC private key, as Coinbase's reference signs it (sub and kid the key name, iss cdp, nbf, exp now + 120 s, uri GET api.coinbase.com/<path>, a nonce header) |
| Credentials stored (encrypted) | key_name, private_key (PEM); optional portfolio_id; optional base_url |
| Least-privilege permissions for the key | View only. No Trade, no Transfer. Create the key with the signature algorithm set to ECDSA (Advanced Trade does not accept Ed25519 keys; the connector refuses one with the reason) and, when the portal offers it, restricted to the portfolio the agent trades in |
| Connection test | GET /api/v3/brokerage/orders/historical/batch?limit=1 |
| Non-secret config | portfolio_id: when set, only orders and fills whose retail_portfolio_id matches are read. Coinbase for Agents keeps each agent in its own portfolio; naming it keeps a person's own trades out of the agent's evidence |
The private key is stored only as ciphertext and is used only to sign tokens for read requests. It is never logged and never leaves the API process.
Fields pulled and why
| Endpoint | Fields | Used for |
|---|---|---|
GET /api/v3/brokerage/orders/historical/batch?start_date=since-7d&limit=100 (pages by cursor while has_next) |
order_id, client_order_id, product_id, side, status, order_type, order_configuration (quote_size, base_size, limit_price), filled_size, filled_value, average_filled_price, total_fees, number_of_fills, created_time, last_fill_time, last_update_time, retail_portfolio_id, order_placement_source, reject_reason |
Order facts, subject id, watermark |
GET /api/v3/brokerage/orders/historical/fills?start_sequence_timestamp=since-7d&limit=100 (pages by cursor) |
trade_id, order_id, product_id, side, trade_type, price, size, commission, trade_time, retail_portfolio_id |
Reversal facts |
Orders are re-read seven days behind the watermark on every sync: an order placed days ago can still fill, be
cancelled or have a fill reversed, and the facts for those are keyed on the order, so the re-read is idempotent.
A fills list the key cannot read (403) is noted on the sync and does not fail it; reversals are then not
observed, and the connection's notes say so.
The order's notional
Agents are asked to trade an amount in the quote currency ("buy $50 of TSLA"). The connector derives one
number, quote_value, from the order: the quote_size it was placed with when it was; otherwise
filled_value; otherwise, for a limit order placed by base size, base_size × limit_price. The contract
compares it with the action's amount.
What is hashed / never stored
- Nothing in these two lists identifies a person; no hashing is needed.
- Account balances, deposit and withdrawal history, payment methods, addresses, names, tax forms and anything outside the two endpoints above are never requested.
Incremental sync and watermark
- Orders:
start_date = watermark − 7 days(2020-01-01 on first sync), page size 100,cursorwhilehas_next. - Fills:
start_sequence_timestamp = watermark − 7 days, page size 100,cursorwhile pages are non-empty. - Watermark = maximum order
created_time. - Upserts are idempotent (by
raw_ref).
Rate-limit handling
Shared HttpClient (Retry-After, exponential backoff on 429/5xx, 6 attempts, immediate reauth failure
on 401/403 for the orders list).
Fact types produced
Subject id is Coinbase's product id (BTC-USD, TSLA-USD), subject kind product: the thing the agent was
told to trade, which is what its recorded action names. Every fact carries order_id, client_order_id,
side, order_type, quote_value and portfolio_id.
fact_type |
When | Extra attributes | raw_ref |
|---|---|---|---|
order_created |
Every order, at created_time; attributes refresh on every sync |
status (current), placement_source |
coinbase:order:{order_id} |
order_filled |
Status FILLED, at last_fill_time |
filled_size, filled_value, average_filled_price, total_fees, number_of_fills |
coinbase:order:{order_id}:filled |
order_failed |
Status CANCELLED, EXPIRED or FAILED, at last_update_time |
status, reject_reason |
coinbase:order:{order_id}:{status} |
order_reversed |
A fill whose trade_type is REVERSAL or CORRECTION, at trade_time |
trade_id, trade_type, price, size, commission |
coinbase:fill:{trade_id} |
Ordinary fills (trade_type FILL) do not become facts of their own: the order's fill is already
order_filled.
Claims produced
None.
Subject id format
The product id as Coinbase spells it. An agent that records a coinbase_agent_trade action uses the same
subject (the product_id it passes to orders_create); the recorder mapping in
recorder/python/examples/coinbase.json does this.
Known limitations
- A product the account has never traded has no record at all, so an agent's claim to have traded it, when
nothing was created, answers UNKNOWN (
identity_unmatched, "subject not found") rather than FAIL; once any order on the product exists, a missing one is a FAIL. The sandbox's broker seeds aproduct_statusrecord per product for this reason; a production workspace can push one through the facts API for the products its agents are allowed to trade. - The side (
BUYorSELL) is limited on the action's parameter and shown in the evidence; the engine's value comparisons are numeric, so the order's side is not compared with the request. order_placement_sourceis read and shown but its values for orders placed through the Coinbase MCP server are not documented (listed underunknown_fieldson the contract); nothing is keyed on it.- Balances and positions are not read: whether the account could afford the order is Coinbase's check before the order, not evidence of it afterwards.
- The connector has been tested against recorded responses in the shape of the reference, not yet against a live Coinbase account; the first live sync is on the backlog.