CP Wallet
Binary: cp-wallet · Package: @computeportal/cp-wallet
cp-wallet is Compute Portal's client-side wallet orchestrator. It pairs a browser wallet (WalletConnect v2), mints short-lived EIP-7702 session keys for unattended signing, and produces the X-Payment header that unlocks any x402-gated endpoint — all without the API or CP infrastructure ever touching your private keys.
Developer Preview
Live WalletConnect pairing and on-chain revocation are not yet wired in the shipped CLI. The entire surface is exercisable today via CP_WALLET_MOCK=1 against a mock adapter — this is the supported path for development, agent integration, and the MCP tool surface. Live-mode wiring (@walletconnect/sign-client adapter + revoke broadcaster) lands separately.
Why cp-wallet
The CP platform spends GPU time. Agents on top of CP need to spend small amounts of USDC against x402-gated endpoints. You don't want:
- An agent that stops every 30 seconds for you to click "sign" in a popup.
- A private key sitting in an agent's environment variable.
- A long-lived API key that can drain the wallet if leaked.
cp-wallet resolves this with a two-tier model:
- Main wallet pairs once via WalletConnect v2 — your keys stay in MetaMask / Rabbit / whichever wallet you use.
- Session key is minted via EIP-7702 with a cap, an expiry, and (optionally) a recipient allowlist. The agent calls
cp-wallet signwith that session key, no popups, until the cap or clock runs out.
If your wallet doesn't support 7702 yet, sign falls back to a per-request wallet prompt — no feature loss, just more clicks.
Install
bash
npm i -g @computeportal/cp-wallet
cp-wallet --helpNode 20+ required.
Quick tour
bash
# 1. Pair a wallet (QR → scan in MetaMask / Rabby / Rainbow)
export CP_WALLET_MOCK=1 # until live WC is wired, see note above
cp-wallet connect
# 2. Mint a session key: up to $20/req, pay only to the CP settlement wallet,
# good for 7 days
cp-wallet delegate --cap 20.00 --expires 7d --to 0xCP_PAYTO_ADDRESS
# 3. Point a 402-gated call at the signer
curl -s https://api.computeportal.io/v1/x402/sample-route -o /dev/null \
-w "%{http_code} %{header_json}\n" \
| tee /tmp/402.json
# → 402, body includes `accepts`
cp-wallet sign --accepts /tmp/402.json # prints X-Payment: ...
# 4. (optional) See what cp-wallet actually decided and why
cp-wallet history --decision approve --limit 5For MCP-driven agents, replace step 3-4 with cp-wallet mcp serve and let the model call wallet_sign_x402 directly — see MCP tools.
Where cp-wallet stores things
| What | Where | Permissions |
|---|---|---|
| Sessions, delegations, policy, audit log | ~/.config/computeportal/wallet-sessions.json and sibling files | 0600 |
| Session-key private material (one file per delegation) | macOS Keychain by default; ~/.config/computeportal/wallet-keystore/* if CP_WALLET_KEYSTORE=file | 0600 |
Override the config dir with CP_WALLET_CONFIG_DIR.
Keystore backends
CP_WALLET_KEYSTORE | Behavior |
|---|---|
| (unset, macOS) | macOS Keychain (probed at startup) |
file | 0600 plaintext file — opt-in; delegate refuses otherwise with keychain_unavailable |
memory | Test-only (ephemeral; never used in production code paths) |
The default refuses to silently write plaintext. If Keychain is unavailable (Linux, headless CI, locked session), you must explicitly set CP_WALLET_KEYSTORE=file to acknowledge the fallback.
CLI commands
Every command accepts --json to emit a stable, parseable envelope:
json
{ "ok": true, "version": "v1", "result": { /* command-specific */ } }On error:
json
{ "ok": false, "version": "v1", "error": { "code": "error_code", "message": "...", "details": {} } }Exit codes: 0 success, 1 runtime error, 2 usage error.
cp-wallet connect
Pair a wallet via WalletConnect v2. Prints a QR code and a wc:// URI to the terminal.
bash
cp-wallet connect [--json]JSON result
json
{
"session_id": "wc-sess-...",
"address": "0xabc...",
"chain_id": 8453,
"wallet_name": "MetaMask",
"expires_at": "2026-11-24T12:34:56Z"
}Errors: pair_timeout, connect_failed.
cp-wallet list
List paired wallet sessions, with the active one marked.
bash
cp-wallet list [--json]cp-wallet use <session-id>
Switch the active session. Later commands (sign, delegate) default to this session when --session isn't passed.
bash
cp-wallet use wc-sess-abc123cp-wallet disconnect <session-id>
Tear down a WalletConnect session and remove it from local storage.
bash
cp-wallet disconnect wc-sess-abc123 [--json]Errors: session_not_found, disconnect_failed.
cp-wallet delegate
Mint an EIP-7702 session-key delegation. The session key is generated locally, stored in the keystore, and authorized by the main wallet via a 7702 signature.
bash
cp-wallet delegate \
--cap <usd> \
--expires <duration> \
--to <0x-address> \
[--json]| Flag | Required | Example | Notes |
|---|---|---|---|
--cap | ✓ | 20.00 | Max USD per request; must be ≥ $0.01 |
--expires | ✓ | 7d, 72h, 30d | Max 30 days |
--to | ✓ | 0xCP_PAYTO… | Pay-to address this delegation is bound to |
JSON result
json
{
"delegation_id": "del_...",
"session_key_pub": "0x...",
"cap_per_request_usd": "20.00",
"pay_to": "0xCP_PAYTO…",
"expires_at": "2026-10-31T12:34:56Z",
"chain_id": 8453
}Errors: invalid_pay_to, keychain_unavailable, wallet_lacks_7702, cap_too_small, expiry_too_long, wallet_session_expired, delegate_failed.
If wallet_lacks_7702 is returned, delegation is impossible but sign still works — each request falls back to a per-request wallet prompt.
cp-wallet delegations
List all delegations (active + expired). Never returns private key material.
bash
cp-wallet delegations [--json]Columns: ID, CAP, PAY_TO, EXPIRES, STATUS.
cp-wallet sign
Sign an x402 accepts body and emit the X-Payment header. If an active delegation covers the request (chain, pay-to, amount, not expired), it signs with the session key — otherwise it prompts the paired wallet.
bash
cp-wallet sign --accepts <file|-> [--session <id>] [--json]Pass - for stdin:
bash
curl -sS https://api.computeportal.io/v1/sample-x402 -I \
| grep -i x-payment-required \
| base64 -d \
| cp-wallet sign --accepts -JSON result
json
{
"x_payment": "base64url-encoded-eip-3009-authorization...",
"session_id": "wc-sess-...",
"signer_address": "0x...",
"network": "base-mainnet",
"value": "20000000"
}Policy-driven rejection codes (returned as error.code):
| Code | Meaning |
|---|---|
cap_per_request_exceeded | Request amount > cap_per_request_usd |
cap_per_period_exceeded | Rolling-window spend would exceed cap_per_period_usd |
pay_to_not_allowed | Pay-to not in pay_to_allowlist |
chain_not_allowed | Session chain not in chain_allowlist |
policy_expired | Policy expires_at has passed |
Other errors: session_not_found, wallet_session_expired, user_rejected, sign_timeout, accepts_validation_error, sign_failed.
Scheme support. Both exact and upto schemes on EIP-3009 TransferWithAuthorization flow through unchanged — the on-chain primitive is the same; the scheme tag only tells the facilitator whether to settle equal to or at most the authorized value. When the accepts body offers both for the same chain, exact is preferred (strictly stronger cap).
cp-wallet policy
Local signing policy — a second layer of safety on top of the per-delegation cap. Policies are evaluated for every sign request, including delegated ones.
bash
cp-wallet policy show [--json]
cp-wallet policy set [flags] [--json]
cp-wallet policy clear --yes [--json]Settable fields
| Flag | Field | Example |
|---|---|---|
--cap-per-day <usd> | cap_per_period_usd + period_window_hours=24 | --cap-per-day 100.00 |
--cap-per-request <usd> | cap_per_request_usd | --cap-per-request 10.00 |
--period-hours <h> | period_window_hours | --period-hours 12 |
--allow-pay-to <0x…> | appends to pay_to_allowlist | repeatable |
--allow-chain <net> | appends to chain_allowlist | base-sepolia or base-mainnet |
--expires-at <iso> | expires_at | 2026-12-31T00:00:00Z |
policy set is merge semantics — unset flags leave existing fields alone. Use policy clear --yes to wipe.
cp-wallet history
Append-only audit log of every sign decision (approve + reject), including policy-reject reasons and settlement tx hashes once observed.
bash
cp-wallet history \
[--since <iso>] \
[--decision approve|reject] \
[--pay-to <0x…>] \
[--limit <n>] \
[--json]The audit log is written synchronously before the X-Payment header leaves the process, so a crash mid-sign cannot leave an authorized but unlogged payment.
cp-wallet revoke <delegation-id>
Revoke an EIP-7702 delegation on-chain. The keystore entry is purged before broadcast — if the broadcast fails, no key material remains locally, and you must delegate again to retry.
bash
cp-wallet revoke <delegation-id> [--dry-run] [--json]--dry-run prints the plan (chain, delegate=zero-address, estimated gas) without broadcasting.
Live broadcast is TODO
In the current preview, the CLI builds the revoke transaction and purges the keystore, but the broadcast step requires an injected opts.broadcast function. MCP callers can wire this; the standalone CLI will gain a --rpc-url/CP_WALLET_RPC_URL broadcaster path alongside live WalletConnect.
MCP tools
cp-wallet mcp serve exposes nine tools over stdio (JSON-RPC 2.0). Two are destructive and gated by the same two-step dry_run → confirm_token flow as the CP MCP server: mint a token with dry_run: true, replay with confirm_token to execute. Tokens are single-use, bound to the exact (tool, args) pair, and expire after 2 minutes.
| Tool | Kind | Purpose |
|---|---|---|
wallet_connect | mutating | Pair a wallet; returns session_id, address, chain_id, and a wc:// URI |
wallet_sessions_list | read | All sessions, active flag |
wallet_delegate | mutating | Mint 7702 session key — inputs: cap_usd, expires, pay_to, optional session_id |
wallet_sign_x402 | mutating | Sign an accepts body; returns header, signed_by: wallet | delegation |
wallet_delegations_list | read | All delegations, active flag |
wallet_policy_show | read | Current policy |
wallet_policy_set | destructive | Merge policy fields; requires dry_run → confirm_token |
wallet_history | read | Audit log — filter by since, decision, limit |
wallet_revoke | destructive | Revoke a delegation; requires dry_run → confirm_token |
Claude Desktop integration
~/Library/Application Support/Claude/claude_desktop_config.json:
json
{
"mcpServers": {
"cp-wallet": {
"command": "cp-wallet",
"args": ["mcp", "serve"]
}
}
}With this plus the hosted computeportal MCP server, an agent can:
- Call
cp_deployto deploy a GPU service. - Hit an x402-gated endpoint, get a 402.
- Call
wallet_sign_x402with the accepts body — no popup, no key leaves the host. - Retry the original call with the
X-Paymentheader.
The agent never sees your signing key; cp-wallet enforces policy locally.
Programmatic API
For non-CLI callers (test harnesses, custom clients):
ts
import { signAccepts } from "@computeportal/cp-wallet/x402/sign";
const { xPayment, signerAddress } = await signAccepts({
accepts, // the 402 response body
sessionId, // optional, defaults to active session
adapter, // WcAdapter (mock or live)
simulationFetch, // optional; otherwise falls back to platform RPC
});Under NODE_ENV=test, simulation is skipped unless simulationFetch is injected (otherwise unit tests would hit sepolia.base.org on every call).
Environment variables
| Variable | Purpose | Default |
|---|---|---|
CP_WALLET_CONFIG_DIR | Override config / keystore directory | ~/.config/computeportal |
CP_WALLET_KEYSTORE | Keystore backend: file or memory | OS default (Keychain on macOS) |
CP_WALLET_RPC_URL | RPC endpoint for sign-time simulation | Platform default per network |
CP_WALLET_MOCK | 1 = use mock WalletConnect adapter (required in preview) | unset |
Security model
- Keys never leave the host. The main wallet's key lives in your wallet app. The session key lives in your OS Keychain (or opt-in 0600 file). The CP API only ever sees signed EIP-3009 authorizations and settlement tx hashes.
- Caps are enforced locally before any signature is produced. The
wallet_sign_x402tool returnspolicy_rejectedbefore contacting the wallet. - Delegations expire. Max 30 days. Revoke writes a 7702 delegation to the zero address — after that transaction mines, the session key is dead on-chain even if the agent still has the local file (and the keystore entry is purged pre-broadcast anyway).
- Audit log is append-only and written before the signed header is returned. Crash recovery cannot forget a payment that happened.
- The CP MCP server never calls
wallet_*tools — the two MCP surfaces are separate processes with separate token models. An agent that compromises your CP API token still cannot authorize a payment.
Related
- x402 API reference — the headers
cp-wallet signproduces - CP MCP Server — manages infrastructure; this one manages payments
- cpctl CLI — the companion CLI for compute, not payments
- EIP-3009 · EIP-7702 · WalletConnect v2
