Send USDC on Base, read balances, and pay x402-protected resources using a Coinbase CDP server wallet (v2). Use when the operator needs the agent to make USD...
---
name: cdp-wallet
description: Send USDC on Base, read balances, and pay x402-protected resources using a Coinbase CDP server wallet (v2). Use when the operator needs the agent to make USDC payments, donations, or x402 settlements on Base without managing private keys directly. Wraps the official @coinbase/cdp-sdk into five CLI subcommands — address, balance, send-usdc, history, pay-x402 — that an agent can invoke directly. Wallet keys live in Coinbase's TEE infrastructure and are addressed by name, so the same wallet persists across container restarts. The pay-x402 subcommand handles the full x402 protocol negotiation (HTTP 402 → EIP-712-signed authorization → resubmit) using the same wallet.
version: 0.2.2
license: MIT-0
metadata:
author: Ales375
source: "https://github.com/Ales375/openclaw-cdp-wallet-skill"
openclaw:
homepage: "https://github.com/Ales375/openclaw-cdp-wallet-skill"
requires:
bins:
- node
- npm
env:
- CDP_API_KEY_ID
- CDP_API_KEY_SECRET
- CDP_WALLET_SECRET
primaryEnv: CDP_WALLET_SECRET
env:
- name: CDP_API_KEY_ID
description: "Coinbase CDP API key ID for server wallet access."
required: true
sensitive: true
- name: CDP_API_KEY_SECRET
description: "Coinbase CDP API key secret paired with CDP_API_KEY_ID."
required: true
sensitive: true
- name: CDP_WALLET_SECRET
description: "Coinbase CDP wallet secret used to authorize signing operations."
required: true
sensitive: true
- name: CDP_NETWORK
description: "Target EVM network for address, balance, send-usdc, and history. Defaults to base."
required: false
- name: CDP_ACCOUNT_NAME
description: "Human-readable CDP account name. Reusing the same name resolves to the same wallet."
required: false
- name: BASE_RPC_URL
description: "Optional override for the Base RPC URL used for on-chain reads and receipt polling."
required: false
compatibility: Requires Node.js 22+, an internet connection, and three CDP credentials (CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET) set in the environment.
---
# cdp-wallet
A small wrapper around the [Coinbase CDP server wallet v2 SDK](https://docs.cdp.coinbase.com/server-wallets/v2/introduction/welcome) that exposes the operations an autonomous agent actually needs: get an address, check a balance, send USDC, look at transfer history, and pay x402-protected resources. Nothing else.
The wallet is a CDP server wallet — keys are generated and held inside AWS Nitro Enclaves on Coinbase's infrastructure, never on the operator's machine, and signing happens by API call against those held keys. The wallet is identified by a human-readable name (`openclaw-default` by default), not a seed phrase, so a fresh container with the same env vars resolves to the same wallet on first call. This is the right shape for unattended scheduled agents on Railway, Fly, Hetzner, etc.
## When to use
- The agent needs to send USDC on Base (donation, peer-to-peer payment).
- The agent needs to pay an x402-protected resource (gated APIs, paid evidence access, agentic-market services).
- The agent needs to know how much USDC or ETH it has before making a decision.
- The agent needs to inspect recent USDC activity on its wallet (audit, idempotency check).
## When not to use
- The agent needs to send a token other than USDC. This skill is intentionally USDC-only; the underlying SDK supports more, but the surface here is deliberately small. Wrap the SDK directly if you need swaps, ERC-20 transfers of other tokens, smart accounts, or paymaster/gasless flows.
- The agent runs on a single trusted machine and the operator wants self-custody. CDP server wallets put trust in Coinbase's TEEs. If that trust assumption is wrong for your use case, use a self-custodial path (viem EOA, Bankr, etc.) instead.
- Solana. Base only.
## Setup
### 1. Get CDP credentials
Sign in at [portal.cdp.coinbase.com](https://portal.cdp.coinbase.com), create a CDP API key, and generate a Wallet Secret. You'll have three values:
- `CDP_API_KEY_ID`
- `CDP_API_KEY_SECRET`
- `CDP_WALLET_SECRET`
The Wallet Secret is the credential that authorizes signing operations against the keys held in CDP's TEEs. Without it, the agent can read but cannot move funds.
### 2. Install
```sh
git clone https://github.com/Ales375/openclaw-cdp-wallet-skill.git
cd openclaw-cdp-wallet-skill
npm install
```
For OpenClaw, point the skill loader at this directory or symlink `~/.openclaw/skills/cdp-wallet → /path/to/openclaw-cdp-wallet-skill`. For Hermes, place under `~/.hermes/skills/cdp-wallet`. For Claude Code or any other agentskills.io-compatible runtime, drop into the configured skills path.
### 3. Configure
Create a `.env` based on `.env.example`:
```
CDP_API_KEY_ID=...
CDP_API_KEY_SECRET=...
CDP_WALLET_SECRET=...
CDP_NETWORK=base # or base-sepolia for testing
CDP_ACCOUNT_NAME=openclaw-default # any name; same name → same wallet across runs
```
For Railway / Fly / Hetzner: set the same variables as service env, no `.env` needed.
### 4. First run — get the wallet address
```sh
npm run address
```
Output:
```json
{"ok":true,"address":"0x...","network":"base","account_name":"openclaw-default"}
```
Same call on a fresh container with the same env vars returns the same address. The wallet is created on first call and looked up on every call after that.
### 5. Fund the wallet
Send USDC on Base (and a small amount of ETH for gas, or use gasless paths externally) to the printed address. The minimum useful balance depends on the agent's purpose; for donations of $5 USDC at a time, fund $50–100 USDC plus $1 of ETH.
For testnet (`CDP_NETWORK=base-sepolia`), the SDK exposes faucet methods — extend this skill if needed; this minimal version doesn't include a faucet command.
## How the agent invokes it
Every subcommand prints a single JSON line to stdout and exits 0 on success or 1 on error. The agent should parse the JSON and act on the `ok` field.
### `address`
```sh
node src/index.js address
```
Prints the wallet's EVM address. Useful before donating so the agent can register itself with services that require a `wallet_address`.
### `balance`
```sh
node src/index.js balance
```
Prints ETH and USDC balances:
```json
{
"ok": true,
"address": "0x...",
"network": "base",
"eth": "0.001234",
"usdc": "42.500000",
"raw": { "eth_wei": "1234000000000000", "usdc_atoms": "42500000" }
}
```
Always check balance before sending. The agent's planning logic should treat the `usdc` string as the spendable amount in human units.
### `send-usdc <to> <amount>`
```sh
node src/index.js send-usdc 0xRecipientAddress 5.00
```
Sends 5 USDC on Base to `0xRecipientAddress`. Waits for one confirmation by default.
Success:
```json
{
"ok": true,
"tx_hash": "0xabc...",
"status": "confirmed",
"explorer": "https://basescan.org/tx/0xabc...",
"from": "0x...",
"to": "0xRecipientAddress",
"amount_usdc": "5.00",
"network": "base"
}
```
Submitted but confirmation timed out (rare; means the chain is congested or RPC is slow):
```json
{ "ok": true, "tx_hash": "0xabc...", "status": "submitted_unconfirmed", ... }
```
In this case the transaction is on-chain but the CLI gave up waiting for the receipt. The agent should poll the explorer or call `history` after a short delay to confirm.
Failures (validation, CDP API error, on-chain revert):
```json
{ "ok": false, "error": "...", "phase": "submit" }
```
### `history --limit <N>`
```sh
node src/index.js history --limit 10
```
Returns the last N USDC Transfer events involving this wallet (in or out), looking back ~24h on Base by default. Use `--lookback <blocks>` to extend. Pure on-chain read against Base RPC; doesn't depend on any CDP-side history API.
```json
{
"ok": true,
"address": "0x...",
"count": 3,
"transfers": [
{
"direction": "out",
"from": "0x...",
"to": "0xRecipient",
"amount_usdc": "5.000000",
"block_number": "12345678",
"tx_hash": "0xabc...",
"explorer": "https://basescan.org/tx/0xabc..."
}
]
}
```
### `pay-x402 <url>`
```sh
node src/index.js pay-x402 https://api.example.com/protected
node src/index.js pay-x402 https://api.example.com/protected -H "Authorization: Bearer abc123"
node src/index.js pay-x402 https://api.example.com/protected -X POST -d '{"k":"v"}' -H "Content-Type: application/json"
```
Calls an x402-protected URL. The x402 protocol is an HTTP-native payment scheme: the server returns a 402 with payment requirements, the client signs an EIP-712 authorization, the server's facilitator settles on-chain, the resource is returned. From the agent's perspective, this is one call — the negotiate-sign-resubmit dance happens transparently.
Options:
- `-X, --method <method>` — HTTP method, default `GET`.
- `-H, --header <name: value>` — request header. Repeat the flag for multiple headers.
- `-d, --body <body>` — request body string. Only valid with non-GET methods.
Success:
```json
{
"ok": true,
"status": 200,
"content_type": "application/json",
"body": { "...": "the resource" },
"body_truncated": false,
"settlement": {
"transaction": "0xabc...",
"amount": "10000",
"network": "eip155:8453",
"...": "facilitator-defined fields"
},
"settled_amount_usdc": 0.01
}
```
The `settlement` object is the decoded `PAYMENT-RESPONSE` header from the server. `settled_amount_usdc` is a convenience conversion assuming USDC (6 decimals); ignore it if the resource server settled in a different asset.
Body handling:
- If `Content-Type` is `application/json`, the body is parsed and embedded as a JSON object/array.
- Otherwise the body is returned as a string.
- If the body exceeds 200,000 characters, it's returned as `{_truncated: true, _length, preview}` and `body_truncated: true`. Increase the limit by editing the skill source if needed.
Failure (server returned non-2xx, or request failed):
```json
{
"ok": false,
"error": "x402 endpoint returned 401 Unauthorized",
"status": 401,
"content_type": "application/json",
"body": { "error": "..." },
"body_truncated": false,
"settlement": null,
"settled_amount_usdc": null
}
```
Network choice. The x402 protocol is network-aware — the resource server tells the client which network to pay on (e.g., `eip155:8453` for Base mainnet, `eip155:84532` for Base Sepolia). `pay-x402` honours whatever the server requests. The `CDP_NETWORK` env var does NOT constrain `pay-x402`; it only affects `address`, `balance`, `send-usdc`, and `history`. If the operator wants to restrict which networks the agent will pay on, do that at the prompt or persona level, not in the skill.
The signer is the same CDP wallet used by `send-usdc`. The agent's address as `wallet_address` (in zooidfund's case) and the address that signs the x402 payment authorization are the same address — no mismatch risk.
## Failure modes the agent should know about
- **Missing env vars** → `ok: false, error: "Missing required env: ..."`. The agent has no recovery path for this; the operator must fix the deployment.
- **Invalid recipient address** → `ok: false`. The agent should validate addresses before invoking `send-usdc`.
- **Insufficient USDC balance** → CDP returns an error from `submit` phase. The agent should call `balance` first.
- **Insufficient ETH for gas** → same. The wallet needs a small ETH balance unless the operator has wired a paymaster.
- **OFAC-screened recipient** → CDP refuses to submit. Skip that recipient rather than retry.
- **`status: "submitted_unconfirmed"`** → tx is on-chain, the CLI just didn't see the receipt within the timeout. Don't re-send; poll `history` or the explorer.
- **CDP API outage** → transient, retry with backoff.
- **`pay-x402` returns ok:false with a 402 status** → the x402 client could not satisfy the server's payment requirements (e.g., server requested an unsupported network/scheme, or the wallet lacked funds). Inspect the response body for the server's `accepts` array — that lists what the server would accept. If the agent's wallet can't satisfy any option, give up rather than retry.
- **`pay-x402` returns ok:false with phase: "request"** → request never completed (network failure, DNS, TLS, malformed URL not caught by upfront validation). Retry once with backoff before giving up.
- **`pay-x402` succeeds but `settlement` is null** → the resource was returned without a `PAYMENT-RESPONSE` header. This usually means the resource was free (server didn't gate it after all) or the server doesn't echo settlement details on this path. Treat as success.
## Why CDP server wallets and not other paths
CDP server wallets v2 was chosen because: keys never reach the operator's filesystem (eliminates a whole class of leak); wallet creation is programmatic and idempotent (no interactive OTP, no seed phrase to lose); the SDK is first-party and current; and the policy engine can layer spending caps on top of this skill at the CDP-account level if the operator wants infrastructure-enforced limits in addition to whatever the agent enforces in its own logic.
The skill deliberately does not enforce per-transaction limits, daily caps, or whitelists in code. Those belong either in the CDP policy engine (for hard infrastructure-level guarantees that survive a compromised agent) or in the agent's own reasoning layer (for soft limits that depend on context). Putting them in the skill code creates the worst of both worlds — bypassable from the agent if it controls the env, but invisible to the operator if it doesn't.
## Security notes
- Use a dedicated OpenClaw agent or workspace for this skill. Do not share one funded wallet across unrelated agents or tenants.
- The Wallet Secret is the most sensitive credential; protect it with the same care as a private key. Anyone with all three env vars can move the wallet's funds.
- This skill prints the wallet address but never the keys (the SDK doesn't expose them).
- Output JSON includes addresses, amounts, and tx hashes only — no secrets are logged.
- If you suspect credential compromise, rotate the Wallet Secret in the CDP portal immediately. Existing transactions can't be reversed but new ones become impossible.
don't have the plugin yet? install it then click "run inline in claude" again.
explicitly structured intent, inputs, procedure (5 steps), decision points (9 branches for edge cases), output contract (JSON schemas for each subcommand), and outcome signals for agent behavior validation; added edge cases like rate limits, RPC latency, OFAC screening, and x402 network mismatch.
Use this skill when your agent needs to send USDC on Base, check balances, inspect transfer history, or pay x402-protected APIs without managing private keys. The skill wraps the Coinbase CDP server wallet v2 SDK into five CLI subcommands (address, balance, send-usdc, history, pay-x402) that an agent can invoke directly. Wallet keys live in Coinbase's AWS Nitro Enclaves and are addressed by name, so the same wallet persists across container restarts. The skill is intentionally USDC-only and Base-only; if you need other tokens, other networks, or gasless flows, wrap the SDK directly instead.
External connection: Coinbase CDP API
Three environment variables (all required, all sensitive):
| Var | Purpose | Setup |
|---|---|---|
CDP_API_KEY_ID |
Server wallet API access key ID | create in portal.cdp.coinbase.com |
CDP_API_KEY_SECRET |
Server wallet API access key secret | paired with CDP_API_KEY_ID above |
CDP_WALLET_SECRET |
Authorization credential for signing operations | generate in CDP portal, protects fund movement |
Optional environment variables:
| Var | Purpose | Default |
|---|---|---|
CDP_NETWORK |
Target EVM network (base, base-sepolia) | base |
CDP_ACCOUNT_NAME |
Human-readable wallet name; same name resolves to same wallet across runs | openclaw-default |
BASE_RPC_URL |
Override the Base RPC URL for on-chain reads and receipt polling | Coinbase-managed RPC |
System requirements:
Setup steps:
.env file:CDP_API_KEY_ID=...
CDP_API_KEY_SECRET=...
CDP_WALLET_SECRET=...
CDP_NETWORK=base
CDP_ACCOUNT_NAME=openclaw-default
git clone https://github.com/Ales375/openclaw-cdp-wallet-skill.git
cd openclaw-cdp-wallet-skill
npm install
~/.openclaw/skills/cdp-wallet for OpenClaw, ~/.hermes/skills/cdp-wallet for Hermes, or your configured skills path).npm run address to initialize the wallet and print its address.Step 1: Get the wallet address
node src/index.js address
Input: none (reads env vars)
Output:
{
"ok": true,
"address": "0x...",
"network": "base",
"account_name": "openclaw-default"
}
The wallet is created on first call if it doesn't exist. Subsequent calls with the same CDP_ACCOUNT_NAME return the same address.
Step 2: Check balance before any transaction
node src/index.js balance
Input: none (reads env vars and calls on-chain RPC)
Output:
{
"ok": true,
"address": "0x...",
"network": "base",
"eth": "0.001234",
"usdc": "42.500000",
"raw": {
"eth_wei": "1234000000000000",
"usdc_atoms": "42500000"
}
}
Use the usdc string field (human units) for planning logic. The raw object contains atomic units (wei for ETH, 6-decimal for USDC).
Step 3a: Send USDC (optional, only if sending)
node src/index.js send-usdc <recipient_address> <amount_usdc>
Input:
recipient_address: EVM address starting with 0x, validated by CDP before submissionamount_usdc: decimal amount (e.g., 5.00, 0.50)Output on success:
{
"ok": true,
"tx_hash": "0xabc...",
"status": "confirmed",
"explorer": "https://basescan.org/tx/0xabc...",
"from": "0x...",
"to": "0xRecipientAddress",
"amount_usdc": "5.00",
"network": "base"
}
Output on submission without confirmation (rare, chain congestion):
{
"ok": true,
"tx_hash": "0xabc...",
"status": "submitted_unconfirmed",
"from": "0x...",
"to": "0xRecipientAddress",
"amount_usdc": "5.00",
"network": "base"
}
Output on failure:
{
"ok": false,
"error": "...",
"phase": "submit|sign|validate"
}
Waits for one on-chain confirmation (receipt) by default; times out after ~60 seconds. If the tx is on-chain but unconfirmed, call history shortly after to verify.
Step 3b: Check transfer history (optional, for auditing)
node src/index.js history --limit 10 [--lookback 5760]
Input:
--limit N: return last N USDC Transfer events (default 10)--lookback <blocks>: search back N blocks instead of default (roughly 24h on Base, ~5760 blocks)Output:
{
"ok": true,
"address": "0x...",
"network": "base",
"count": 3,
"transfers": [
{
"direction": "out",
"from": "0x...",
"to": "0xRecipient",
"amount_usdc": "5.000000",
"block_number": 12345678,
"tx_hash": "0xabc...",
"explorer": "https://basescan.org/tx/0xabc..."
},
{
"direction": "in",
"from": "0xSender",
"to": "0x...",
"amount_usdc": "100.000000",
"block_number": 12345600,
"tx_hash": "0xdef...",
"explorer": "https://basescan.org/tx/0xdef..."
}
]
}
Pure on-chain read against Base RPC; independent of CDP's backend.
Step 4: Pay x402-protected resources (optional, only if accessing gated APIs)
node src/index.js pay-x402 <url> [-X <method>] [-H "header: value"] [-d <body>]
Input:
url: x402-protected endpoint (e.g., https://api.example.com/protected)-X, --method <method>: HTTP verb (GET, POST, etc.; default GET)-H, --header "name: value": request header; repeat flag for multiple headers-d, --body <string>: request body for non-GET methodsOutput on success:
{
"ok": true,
"status": 200,
"content_type": "application/json",
"body": { "resource": "data" },
"body_truncated": false,
"settlement": {
"transaction": "0xabc...",
"amount": "10000",
"network": "eip155:8453"
},
"settled_amount_usdc": 0.01
}
Output on failure (server returned 402 or non-2xx, or request failed):
{
"ok": false,
"error": "x402 endpoint returned 401 Unauthorized",
"status": 401,
"content_type": "application/json",
"body": { "error": "..." },
"body_truncated": false,
"settlement": null,
"settled_amount_usdc": null
}
The x402 protocol negotiates automatically: server returns 402 with payment requirements, CLI signs an EIP-712 authorization with the CDP wallet, server's facilitator settles on-chain, then the actual resource is returned. From the agent's perspective it is a single call.
If ok: false with no phase field (likely bootstrap error):
CDP_* env vars are set and not empty. If any are missing, the operator must fix the deployment; the agent has no fallback.If balance returns usdc: "0.000000":
send-usdc and expect failure, or skip the send and move on. Retry checking balance after a few seconds if a recent deposit should have arrived (RPC latency).If send-usdc returns phase: "validate":
If send-usdc returns phase: "sign":
CDP_WALLET_SECRET or the deployment will fail on every send attempt.If send-usdc returns phase: "submit" (e.g., insufficient USDC, insufficient ETH, OFAC screening):
balance and halt the send. For OFAC, skip that recipient permanently; there is no override.If send-usdc returns status: "submitted_unconfirmed":
history to confirm the tx is included, or check the explorer directly.If history returns an empty transfers array:
--lookback to search further back in time.If pay-x402 returns ok: false, status: 402:
accepts array, which lists acceptable networks, assets, and settlement schemes. If none match what the wallet can provide (e.g., server requires Polygon but wallet is on Base), skip the request. If the wallet lacks sufficient funds to cover the server's asking price on the correct network, top up and retry once.If pay-x402 returns ok: false, status: null or a network error:
If pay-x402 returns ok: true but settlement: null:
PAYMENT-RESPONSE header. This usually means the resource is free (server gates didn't trigger) or the server doesn't echo settlement. Treat as success; the response body contains the resource.If CDP_NETWORK is set to base-sepolia for testing:
address, balance, send-usdc, and history subcommands use the testnet. The pay-x402 subcommand always honours the server's requested network (set in the x402 payment requirements) and ignores CDP_NETWORK, so it may still pay on mainnet if the server requests it. If you want to restrict networks at runtime, do that in the agent prompt or persona, not in the skill.Every subcommand prints a single JSON object to stdout and exits with code 0 on success or 1 on failure.
Success shape (all subcommands):
{
"ok": true,
...subcommand-specific fields...
}
Failure shape (all subcommands):
{
"ok": false,
"error": "human-readable error message",
...optional context fields like phase, status, body...
}
Common fields across success responses:
address: the wallet's EVM address (all subcommands except pay-x402)network: the network name (base, base-sepolia) for address, balance, send-usdc, history; not present for pay-x402send-usdc success:
tx_hash: on-chain transaction hashstatus: "confirmed" or "submitted_unconfirmed"explorer: link to BaseScan transaction pagefrom, to, amount_usdc: transaction detailshistory success:
count: number of transfers in the responsetransfers: array of transfer objects, each with direction (in/out), from, to, amount_usdc, block_number, tx_hash, explorerpay-x402 success:
status: HTTP status code (should be 2xx)content_type: response MIME typebody: parsed JSON object (if content-type is application/json) or string (otherwise)body_truncated: boolean, true if body was truncated to 200k charssettlement: decoded PAYMENT-RESPONSE header from server, or null if not presentsettled_amount_usdc: convenience USDC conversion of settlement amount (assumes 6 decimals), or nullpay-x402 failure:
status: HTTP status code (may be null for network errors)body, body_truncated, content_type: error response from the server, if availablesettlement: nulladdress: agent confirms wallet is created and prints address to log; agent can now register that address with external services or fund the account.balance: agent reads usdc field and adjusts its transaction planning; if balance is low, it skips sends; if balance is adequate, it proceeds.send-usdc: agent observes ok: true and status: "confirmed", then updates its internal state (e.g., donation recorded, payment sent). If status: "submitted_unconfirmed", agent waits and polls history to confirm before marking as done. If ok: false, agent logs the error and skips that send.history: agent iterates the transfers array and updates an internal ledger or idempotency check; if a previously-sent tx appears, the agent knows it was included on-chain.pay-x402: agent receives ok: true and extracts the resource from body; if ok: false, agent reports the reason (server rejected payment requirements, network unreachable, insufficient wallet balance) and either retries or gives up based on the error type.