Back to blog
Build Your Own Agent11 min read

Trading Agent with Coinbase

A price check and a balance check are the easy part. Two guardrails run before Coinbase ever sees an order, and the agent never touches your real key. Here's how we wired it, bugs and all.

Abhishek Upadhyay
Abhishek Upadhyay
Trading Agent with Coinbase

Agent-Initiated Financial Actions Infrastructure: A Trading Agent with Coinbase

A trading agent is agent-initiated financial actions infrastructure in practice: an LLM decides to buy or sell, and something underneath has to hold a real key, check a real cap, and place a real order, all without the model ever touching the credential itself. Abstraxn's MCP layer doesn't care which exchange or chain that something talks to. The wallet, the policy check, and the chat loop stay identical no matter what sits underneath them. Coinbase is the tool we picked to prove that this time, the same way Firecrawl proved it for web research.

Watch: Build a Trading Agent with Abstraxn — Coinbase, spend guardrails, and Agent Kit in action.

Key Takeaways

  • A trading agent is a real-world test of agent-initiated financial actions infrastructure: an LLM decides, Abstraxn supplies the wallet, the guardrail, and the audit trail underneath it.
  • Coinbase Advanced Trade is wired in as eight MCP tools, price, balance, key permissions, preview, place/cancel/list orders, shared through the same packages/mcp config every example in this series uses.
  • Two Powers carry the weight for this agent: Wallet (your Coinbase key never reaches Abstraxn, only a short-lived token does) and Policy (a per-trade cap plus a daily account cap, both live, both checked before Coinbase is called).
  • The first real test hit a 403 Missing required scopes, not the error we were testing for. The fix, and the automation that now catches it before an order is even attempted, is below.
  • Clone examples/02-trading-agent, paste a Coinbase key, ask the agent to buy a dollar of BTC.

The first real test taught us two things. The guardrail has to live in Abstraxn's own code, not in whatever an exchange's dashboard claims. And the agent should never see the key that's doing the trading.

We set the CDP key up with Trade permission checked in the portal, Transfer left off, and pointed it at an account we knew had zero funds. Then we asked the agent to buy a dollar of BTC. coinbase_get_price came back clean: real bid/ask off the live order book. coinbase_get_balance came back clean too: an account we hadn't fully explored yet, a single INR wallet at zero. Then coinbase_place_order came back with 403 Missing required scopes.

Not INSUFFICIENT_FUND. Not the error we were testing for. A permissions error, before Coinbase ever got far enough to check whether we had money.

We pulled getApiKeyPermissions() straight from the Coinbase SDK to see what the key actually thought it could do: can_view: true, can_trade: false, can_transfer: false. The portal checkbox and the key's real scope had drifted. That's exactly why checkOrderPolicy runs before Coinbase is ever called, instead of assuming Coinbase's own settings are accurate: Abstraxn's guardrail can't depend on a portal checkbox, only on what Coinbase actually reports.

The short version: Abstraxn hosts eight Coinbase Advanced Trade tools (price, balance, key permissions, preview, place/cancel/list orders) as MCP tools, exposed to 02-trading-agent through the same packages/mcp config every example in this series shares. Abstraxn never holds your Coinbase key: this example mints its own short-lived bearer token from your key and sends only the token across. Every order runs through two spend caps before Coinbase is ever called. Clone examples/02-trading-agent. If Hello Wallet already works on your machine, this is one more pnpm --filter away, plus a Coinbase CDP key.

Checking price and balance is not the risky part: placing the order is

coinbase_get_price and coinbase_get_balance are read-only. Ask the agent either question a hundred times and nothing changes about your account. coinbase_place_order is different: it's the one call in this tool set that can move real money, however small the amount, the moment the key's scopes and the account's funds line up.

This is Abstraxn's job, not Coinbase's:

LayerRole
LLMInterprets intent, checks price and balance, explains the order it's about to place
Bearer token mintSigns a short-lived, single-call token from your Coinbase key, locally, in your own backend
Abstraxn MCPExposes coinbase_* tools, hosted by Abstraxn, never sees your key
Order policyCOINBASE_MAX_ORDER_USD: hard-caps quote size, checked before Coinbase is called
Coinbase CDP keyYours. Lives in your own .env. Never transmitted anywhere, only the token is

The cap runs inside the tool, before the network call: not as a wrapper around the LLM, not as a prompt instruction the model could talk itself out of.

Browser chat → /api/chat → mint a short-lived token from your Coinbase key   ← your own backend, fails loud if unset
                        → coinbase_place_order (Abstraxn MCP)                ← holds no Coinbase key
                            → per-trade cap check                           ← rejects locally if over cap
                            → Coinbase, using the token, never your key

Abstraxn's chat loop, wallet bootstrap, and MCP wiring are exactly what Hello Wallet and Firecrawl already use, untouched. What's new lives in two places: the Coinbase tools and the cap, hosted and enforced server-side in Abstraxn's own infrastructure, and, on this example's own side, one file that does all the token minting: lib/coinbase-byok-tool.ts.

Does Abstraxn ever hold your Coinbase key?

A server that trades on your behalf doesn't need to hold your key permanently to do it. It needs proof, for one call, that you said yes.

So the key never goes to Abstraxn. lib/coinbase-byok-tool.ts reads COINBASE_BYOK_API_KEY_NAME and COINBASE_BYOK_API_KEY_SECRET from this example's own .env. It mints a short-lived token locally for each Coinbase call and sends only that token to Abstraxn. The token expires in about two minutes and is good for exactly one call, whatever the agent just asked for, and nothing else. Abstraxn passes it straight through to Coinbase and never stores it.

Forget to set either env var and the app tells you plainly, right away, instead of failing quietly three tool calls later. There's no fallback key to reach for if that check fails, on purpose. coinbase_get_price is the one tool that's the exception: checking a price doesn't touch your account, so it needs nothing configured at all.

How many spend guardrails run before Coinbase sees an order?

The per-trade cap catches one order that's too big. It doesn't catch ten orders that are each individually fine. That's why Abstraxn Agent Kit also applies an account-level spend policy on boot, before the agent takes its first message:

GuardrailLives inCatches
Per-trade cap (COINBASE_MAX_ORDER_USD)Enforced by Abstraxn, inside coinbase_place_orderOne order over your comfort limit
Account cap ($50/day, hard-blocked)Abstraxn Agent Kit, applied once in lib/session.tsMany small orders that each pass the per-trade check but add up over a day
[abstraxn] Spend policy applied { agentId: '...', enabled: true, budgetUsd: '50', period: 'daily', hardBlock: true }

That line prints once, on the first request of a session. If you don't see it, the account-level cap isn't active, and you're relying on the per-trade cap alone.

The Four Powers behind agent-initiated financial actions infrastructure

Every agent Abstraxn provisions gets the same four things underneath it: a verifiable identity, a wallet it controls, a way to pay, and policy that keeps it in-bounds. A research agent leans on Identity and Payments, since it's authenticating to data sources and paying per request. A trading agent leans hardest on Wallet and Policy instead.

Wallet. Every agent in this series, including this one, gets its own on-chain wallet through Abstraxn WaaS the moment it's created, the same get_wallet_address tool Hello Wallet smoke-tests. This agent doesn't trade through that wallet: Coinbase is a separate account you already hold. But the same principle carries over from one to the other. Abstraxn holds infrastructure, never your keys, whether that key opens an Abstraxn wallet or a Coinbase account.

Policy. This is the per-transaction limit control, live today, doing double duty: checked once against a single order's size, and again as a rolling daily budget, both before Coinbase is ever called. Nothing broader than that runs here, and nothing broader is claimed.

Identity and Payments exist for every agent in this series but don't carry the weight for this one. This agent's identity is just yours, and Coinbase itself settles the trade, so agent-to-agent payment rails like x402 don't come into it for this use case.

Check the CDP key's actual scopes before you trust the portal checkbox

This is the step our own test skipped, and it cost us a confusing failure. Coinbase's CDP Portal lets you toggle Trade/Transfer permissions on a key, but the checkbox and the key's real, effective scope aren't necessarily the same thing the moment you generate it. Before wiring a key into anything that places orders, call:

const permissions = await client.getApiKeyPermissions();
// { can_view: true, can_trade: false, can_transfer: false }

If can_trade is false, every coinbase_place_order call fails with 403 Missing required scopes, regardless of account balance, regardless of the cap you've set. It looks like a code bug. It isn't. Fix the key's permissions in the CDP Portal first, then re-test.

Once trading is genuinely enabled, coinbase_get_balance on a real account can also surprise you. Ours started out showing a single INR fiat wallet. A day later it had auto-provisioned MATIC, ETC, and USDC crypto wallets, all still at zero. New crypto wallets can appear on the account over time as features get enabled, not just at signup. Results can genuinely change day to day, and it's worth re-fetching rather than caching.

That check used to be something you had to remember to run yourself. It isn't anymore. The agent now calls coinbase_get_key_permissions on its own at the start of a trading conversation, the tool-shaped version of the exact getApiKeyPermissions() call above, and it reports back canTrade: false before ever attempting an order, not after. It calls coinbase_preview_order the same way, before anything non-trivial. That's a real Coinbase-side estimate of what an order would cost, without placing it, so you see the number before you commit to it. If this had existed the day we hit that 403, the agent would have told us Trade wasn't enabled before it ever tried to spend anything.

None of this is a knock on Coinbase specifically. It's the reason Abstraxn's tool layer treats every exchange as untrusted state: verify what Coinbase actually says, every time, instead of caching what a dashboard or a docs page claims.

Further reading: Agent Kit overview · Spending limits

The code: the cap, then the tools, then the agent config

The guardrail is the part worth understanding first. It's deliberately small (a per-trade cap, not a full spend-policy engine), it's enforced server-side in Abstraxn's own infrastructure, and it runs before anything touches Coinbase. Given a quote size and the COINBASE_MAX_ORDER_USD cap you configured, it either lets the call through or rejects it locally with a clear error, before Coinbase is ever contacted.

coinbase_place_order calls this before it ever touches Coinbase. A rejected call never reaches Coinbase at all: nothing to audit, no rate limit spent, no order to cancel.

The tool set is a single named entry in Abstraxn's shared MCP config:

// packages/mcp/src/index.ts
coinbaseTrading: [
  "coinbase_get_price",
  "coinbase_get_balance",
  "coinbase_get_key_permissions",
  "coinbase_preview_order",
  "coinbase_place_order",
  "coinbase_get_order_status",
  "coinbase_cancel_order",
  "coinbase_list_recent_orders",
  "get_wallet_address",
]

get_wallet_address isn't Coinbase-specific: every example in this series shares it, since every agent still has an Abstraxn-provisioned wallet underneath, whether or not this particular agent ever uses it for a Coinbase trade.

And lib/agent.ts is still the only file that defines what the agent decides and when. It's a plain LLM trading agent config: no tool implementations here, just a tool set name and instructions in English:

// examples/02-trading-agent/lib/agent.ts
export const agentConfig: AgentConfig = {
  name: "Coinbase Trading Agent",
  tools: "coinbaseTrading",
  system: `You help users trade on Coinbase Advanced Trade (CEX, spot market orders).
At the start of a trading conversation, call coinbase_get_key_permissions once and tell the user upfront if canTrade is false, instead of only discovering it after a failed order.
Always check coinbase_get_price and coinbase_get_balance before placing an order.
Use coinbase_preview_order to show estimated cost before calling coinbase_place_order for anything non-trivial.
Every order is checked against a server-side per-trade USD cap before Coinbase is called.
If a Coinbase error like INSUFFICIENT_FUND comes back, explain it plainly; that is a normal, expected result on a test account.
Never claim an order filled unless coinbase_place_order or coinbase_get_order_status confirms it.`,
};

Run it

git clone https://github.com/Abstraxn-Labs/abstraxn-agent-examples
cd abstraxn-agent-examples
pnpm install
cp .env.example .env
pnpm run build:packages
pnpm --filter @abstraxn-examples/trading-agent dev

Environment variables

Everything below is required to actually run this end to end, and everything below lives in one place: this example's own .env. Abstraxn never asks you to configure anything on its side.

VariableWhereNotes
ABSTRAXN_API_KEYabstraxn-agent-examples/.envDashboard → Agentic Stack → Overview
ABSTRAXN_USER_IDENTITYabstraxn-agent-examples/.envAny stable identity string, e.g. an email
LLM_PROVIDER / LLM_API_KEY / LLM_MODELabstraxn-agent-examples/.envOne provider block: OpenAI, OpenRouter, Anthropic, or any OpenAI-compatible API, see docs/LLM-PROVIDERS.md
COINBASE_BYOK_API_KEY_NAMEabstraxn-agent-examples/.envYour CDP key name, organizations/.../apiKeys/...
COINBASE_BYOK_API_KEY_SECRETabstraxn-agent-examples/.envThe matching CDP private key. Mints a token per call, never sent anywhere itself

The per-trade cap (COINBASE_MAX_ORDER_USD) isn't something you set: Abstraxn enforces it on its side before your order ever reaches Coinbase.

Open http://localhost:3002 and try (this example runs on 3002; Hello Wallet is on 3000):

Buy $1 of BTC and explain the price.

Watch for the sequence: a key-permissions check, a price check, a balance check, then the order attempt. Read whatever comes back from Coinbase literally. INSUFFICIENT_FUND on a zero-funds account is success, not a bug to chase.

Before you go live

This talks to Coinbase's real, production API. There's no sandbox mode here: Coinbase's actual Advanced Trade sandbox is a separate, unauthenticated endpoint with static canned data and no price or key-permissions support, so it can't stand in for this demo. Do what we did instead: a real key, Trade ON, Transfer OFF, pointed at an account you know has zero funds. Confirm coinbase_get_key_permissions shows canTrade: true before pointing a key at an account you actually fund. Set both caps, the per-trade one and the $50/day account one, deliberately low for a first real run. Only raise them once you've watched a full price → balance → order cycle succeed and match what you expected.

Once this agent runs cleanly, the same scaffold covers the rest of this series. Hello Wallet is the five-minute version of everything above, just the wallet and the read-only tools, worth running first if you haven't. Firecrawl research swaps Coinbase for a paid web-scraping tool under the same policy check. Tx monitoring and the fraud and policy agent both build on the same guardrail pattern, watching and blocking instead of trading.

FAQ

Does this example execute live trades by default?

It can. coinbase_place_order places a real order on Coinbase. There's no simulation mode, but coinbase_preview_order gets you close: it prices the exact order you're about to place, without executing it. The real safety net is a per-trade USD cap plus a $50/day account cap, both checked before Coinbase is ever called.

How is a trade capped?

Two ways. coinbase_place_order checks COINBASE_MAX_ORDER_USD, enforced by Abstraxn, before it ever calls Coinbase, rejecting any single order over that size. Separately, Abstraxn Agent Kit applies a $50/day account-level cap on boot, so a string of small orders that each pass the per-trade check still can't drain the account in one day.

Which MCP tools does the trading agent use?

coinbase_get_price, coinbase_get_balance, coinbase_get_key_permissions, coinbase_preview_order, coinbase_place_order, coinbase_get_order_status, coinbase_cancel_order, and coinbase_list_recent_orders.

Does Abstraxn ever see my real Coinbase key?

No. This example mints a short-lived bearer token from your key locally, in your own backend, and sends only that token to Abstraxn's MCP tools. The token expires in about two minutes and is scoped to a single tool call. Abstraxn's MCP service holds no Coinbase key at all.

What happens if the account has no funds, or the API key lacks trade permission?

Both are normal, structured tool results, not crashes. An INSUFFICIENT_FUND error means the funds check failed on Coinbase's side. A Missing required scopes error means the key's Trade permission isn't actually enabled. The agent now checks this itself with coinbase_get_key_permissions before it ever tries to place an order, so you find out upfront instead of after a failed trade.

About the Author

Abhishek Upadhyay

Abhishek Upadhyay

Software Engineer

Abhishek Upadhyay is a Software Engineer at Abstraxn. He builds MCP tool integrations, wires payment protocols into agent workflows, and contributes to the open-source Build your Agent with Abstraxn series.