WalletConnect MCP HTTP API v0.6.0
The same tools the MCP server offers, as JSON routes for agents and backends that do not speak MCP. Machine-readable description: /api/v1/openapi.json.
Getting a token
OAuth 2.1 with PKCE on this origin. A token is bound to the resource https://mcp.walletconnect.com/api; an MCP token does not work here, nor the other way round. Send it as Authorization: Bearer …. One thing no integration can skip: a person opens the consent page in a browser and approves in their wallet. There is no API key.
1. Discovery
A call without a token answers 401 and names the resource metadata, which names the authorization server, which lists its endpoints.
GET https://mcp.walletconnect.com/api/v1
→ 401 WWW-Authenticate: Bearer realm="OAuth", resource_metadata="https://mcp.walletconnect.com/.well-known/oauth-protected-resource/api"
GET https://mcp.walletconnect.com/.well-known/oauth-protected-resource/api
→ { "resource": "https://mcp.walletconnect.com/api", "authorization_servers": ["https://mcp.walletconnect.com"] }
GET https://mcp.walletconnect.com/.well-known/oauth-authorization-server
→ { "authorization_endpoint": "https://mcp.walletconnect.com/authorize", "token_endpoint": "https://mcp.walletconnect.com/token",
"registration_endpoint": "https://mcp.walletconnect.com/register", "code_challenge_methods_supported": ["S256"], … }
2. A client identity
- A local agent or CLI registers itself at
https://mcp.walletconnect.com/registerwith a loopback redirect (http://localhost:<port>/…), exactly as Claude Code does. Other callbacks are refused unless they belong to a known MCP client. - A hosted app uses a client ID metadata document: its
client_idis an HTTPS URL on its own domain serving a JSON document withclient_name,redirect_urison that domain andtoken_endpoint_auth_method(nonewith PKCE, orprivate_key_jwt). No registration step; the consent page shows the domain next to the app's name. - A platform that can do neither (ChatGPT Actions, for one) gets a pre-registered client id and secret from the WalletConnect team.
POST https://mcp.walletconnect.com/register
{ "client_name": "my-agent", "redirect_uris": ["http://localhost:9999/cb"],
"grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }
→ { "client_id": "…", … }
3. The authorization request
Open this in the user's browser. code_challenge is the base64url SHA-256 of a random code_verifier you keep. resource picks the audience; a request that omits it gets the API.
GET https://mcp.walletconnect.com/authorize?response_type=code&client_id=…&redirect_uri=…&state=…
&code_challenge=…&code_challenge_method=S256
&scope=wallet:read wallet:write
&resource=https://mcp.walletconnect.com/api
The consent page is where the wallet comes in. The browser gets a signed cookie that is the user's identity, a new one if it has none. The page shows a WalletConnect QR; the user scans it with their wallet, and that session attaches to the user. Allow sends the browser back to your callback with code and state. A browser that already paired a wallet for an earlier app skips the QR, so one wallet serves every app the user authorizes. The user may also skip the wallet; see step 6.
A client that creates the pairing itself, a wallet for one, adds connect=skip to the request. The consent page then shows the decision alone and starts no pairing; Allow returns to your callback at once, and you pair through POST /api/v1/wallet/pairings with the token (step 6).
4. The code exchange
POST https://mcp.walletconnect.com/token
grant_type=authorization_code&code=…&redirect_uri=…&client_id=…&code_verifier=…&resource=https://mcp.walletconnect.com/api
(a confidential client adds client_secret, or uses HTTP basic auth)
→ { "access_token": "…", "token_type": "bearer", "expires_in": 3600, "refresh_token": "…",
"scope": "wallet:read wallet:write", "resource": "https://mcp.walletconnect.com/api" }
5. Lifetimes and refresh
An access token lives an hour. POST /token with grant_type=refresh_token, the refresh token and the same resource returns a new pair; the refresh token rotates, and the previous one stays valid until its replacement is first used, so a lost response can be retried. A grant that keeps refreshing never expires; one left idle for thirty days does. Revoke at the token endpoint with token=… (RFC 7009).
6. If the wallet is not connected yet
GET /api/v1/wallet says so. POST /api/v1/wallet/pairings starts a pairing and returns the wc: URI, a QR as a PNG under images, a hosted connect page and deep links for common wallets; show one of them to the user, then GET /api/v1/wallet?wait_seconds=55 in a loop until connected is true. The pairing URI is a bearer secret: anyone who scans it pairs their wallet to this user. A wallet that is itself the client compares the session topics in GET /api/v1/wallet with its own, and passes new_session: true when none is its own.
Quickstart
examples/quickstart.mjs in the repository does all of the above from Node with no dependencies, then reads the portfolio and, with --send, makes a transfer through prepare, confirm and poll. scripts/api-flow.sh is the same flow in curl.
How a write works
POST /api/v1/actions/transfers or /actions/swaps prepares an action and sends nothing: the result decodes every step, carries the raw transaction bytes and plan.hash. Compare it with what the user asked, then POST /api/v1/actions/{id}/confirm with the hash and expect, your own restatement (the keys are in plan.expectKeys). The server sends the first step to the user's wallet only if both match. Then GET /api/v1/actions/{id}?wait_seconds=55 until the status is confirmed, rejected, expired, failed or cancelled. Each call waits server-side for a change, so poll in a loop without sleeping.
Shapes
Every success is { text, data, images? }: one line for a human or an agent, the tool's data, and images when the tool produced one (the pairing QR). GET and DELETE take arguments from the query string, POST from a JSON body; a path parameter is the tool argument of the same name. Errors are { error: { code, message, issues? } }: 400 invalid_request, 401 from the OAuth layer, 404 not_found, 409 conflict (no wallet connected, an open action in the way, an expired or mismatched plan), 422 blocked (caps, unlisted tokens or chains, bad amounts or addresses), 500 internal.
Routes
meta · Server info, the tool list and the generic call
GET /api/v1Server infoVersion, the instructions an agent should follow, enabled chains, policy caps, tool names and where the OAuth resource metadata is.GET /api/v1/toolsList toolsEvery tool with its JSON Schema, in the shape an agent framework needs to register them as functions.POST /api/v1/tools/{name}Call a tool by nameThe same call as MCP tools/call: the body is the tool's arguments, the result is its text and data. Works for every tool, including ones without a route of their own. The data has the shape the tool's own route documents.
wallet · Pairing and session
GET /api/v1/wallet/pairings/{id}Pairing status · data: PairingStatusWhether the wallet approved the pairing from POST /wallet/pairings. Poll it, or call GET /wallet, which waits for the approval.POST /api/v1/wallet/pairingsConnect wallet · data: PairingStartReturns the connected wallet accounts, or starts a WalletConnect pairing and returns everything needed to complete it in chat: a QR code image, the raw wc: URI (the user can paste it into their wallet's WalletConnect screen), deep links for common mobile wallets, and a hosted page with a wallet picker. Show the QR or the link to the user and tell them to approve in their wallet, then call wallet_status, which waits for the approval. The pairing expires in 5 minutes and is a bearer secret: anyone who scans it pairs their wallet to this user, so show it only to the user. Pass new_session: true to connect a different wallet while one is already connected.GET /api/v1/walletWallet status · data: WalletStatusWallet connection status: accounts per chain, wallet name, walletLinks (the wallet app's native custom-scheme and universal https links), session expiry and any open actions. If a connect link is awaiting approval it waits for the wallet (up to 55 s) and returns as soon as it connects; call it again while still awaiting.DELETE /api/v1/wallet/sessionDisconnect wallet · data: DisconnectEnd the WalletConnect session with the user's wallet. Steps not yet sent to the wallet are cancelled.
portfolio · Balances
GET /api/v1/portfolioPortfolio · data: PortfolioAll token balances of the connected wallet on a chain with USD prices and values, from the Blockchain API. Read-only, no wallet approval needed.
actions · Prepare, confirm, poll and cancel
GET /api/v1/actionsOpen actions · data: ActionListActions that are prepared, awaiting the wallet or submitted. Terminal actions are not listed; fetch them by id.POST /api/v1/actions/transfersSend transfer · data: ActionSend native ETH or an ERC-20 token (USDC) from the connected wallet to an address (real funds). amount is a decimal string in whole units (e.g. "0.001" ETH or "5" USDC), never wei. The server builds the calldata. Before calling, state the amount, recipient and chain to the user. Returns a prepared action. Nothing is sent to the wallet yet: the result is a prepared action with each step decoded (asset, amount, recipient, chain, contract, and for a swap the quote and minimum out), the raw transaction bytes, and plan.hash. Compare it with what the user asked, tell the user in one line what will happen, then call confirm_action(action_id, plan_hash, expect) with expect filled from the user's request rather than copied from the plan. If anything differs, call cancel_action with the reason. Once confirmed, the wallet request expires in 5 minutes: tell the user to approve in their wallet, then call get_action_status(action_id) right away and keep calling it until the status is confirmed, rejected, expired, failed or cancelled. Do not wait for the user to say they approved. While a step is awaiting_wallet the action carries approveLinks (universal: https, native: custom scheme) that open the wallet app on that request: show the universal one to a user on their phone, or open the native one first if you can open URLs on their device.POST /api/v1/actions/{action_id}/confirmConfirm action · data: ActionSend a prepared action to the wallet. Pass plan.hash from the action you reviewed and expect: your own restatement of what the user asked, as strings, with the keys in plan.expectKeys. For kind transfer: kind, chain, asset, amount, to. For kind swap: kind, chain, from_asset, to_asset, amount_in, min_out, recipient (min_out from the plan, everything else from the user's request). The server refuses, and sends nothing, if the hash differs from the prepared plan, if any expect field disagrees with the plan, or if the plan expired. On success the first step is in the wallet: tell the user to approve, then call get_action_status right away and keep calling it until the status is terminal.GET /api/v1/actions/{action_id}Action status · data: ActionWait for and return the state of an action and each of its steps: prepared (nothing sent; confirm_action or cancel_action it), awaiting_wallet, submitted (tx hash known), confirmed, rejected, expired, failed or cancelled. Call it right after confirm_action, without asking the user. It waits server-side (up to 55 s) and returns as soon as something changes; a prepared action returns at once. If the action is not terminal yet, call it again immediately. Multi-step actions advance on their own: the next step is sent to the wallet when the previous one confirms. While a step is awaiting_wallet the result carries approveLinks that open the wallet app on that request; show the universal one to a user on their phone.POST /api/v1/actions/{action_id}/cancelCancel action · data: ActionCancel a prepared action, or the steps of a running action that have not been sent to the wallet yet. A request already showing in the wallet cannot be recalled; the user rejects it there. Give the reason, for example what in the plan differed from the user's request; it is written to the audit log.POST /api/v1/actions/swapsSwap on Uniswap · data: ActionSwap one asset for another on Uniswap V3 (real funds), from and to the connected wallet. from and to are asset symbols (call swap_assets for what a chain offers); amount is a decimal string in the from asset. The server picks the best fee tier, sets a minimum output (quote minus max slippage, default 0.5%) and a 10-minute deadline, and runs the swap against the chain before preparing it. Selling an ERC-20 is a two-step action: an exact-amount approval, then the swap, which the server sends once the approval confirms. Selling the chain's native asset is one step. Before calling, state the amount and both assets to the user. Returns a prepared action. Nothing is sent to the wallet yet: the result is a prepared action with each step decoded (asset, amount, recipient, chain, contract, and for a swap the quote and minimum out), the raw transaction bytes, and plan.hash. Compare it with what the user asked, tell the user in one line what will happen, then call confirm_action(action_id, plan_hash, expect) with expect filled from the user's request rather than copied from the plan. If anything differs, call cancel_action with the reason. Once confirmed, the wallet request expires in 5 minutes: tell the user to approve in their wallet, then call get_action_status(action_id) right away and keep calling it until the status is confirmed, rejected, expired, failed or cancelled. Do not wait for the user to say they approved. While a step is awaiting_wallet the action carries approveLinks (universal: https, native: custom scheme) that open the wallet app on that request: show the universal one to a user on their phone, or open the native one first if you can open URLs on their device.
swaps · Quotes
GET /api/v1/swaps/assetsSwappable assets · data: SwapAssetsRead-only: which assets can be swapped on which chains, with their decimals and token addresses. Availability differs by chain, so check here before quoting an asset the user named. No wallet approval needed.GET /api/v1/swaps/quoteSwap quote · data: QuoteRead-only: what a swap would return right now on Uniswap V3. from and to are asset symbols (call swap_assets for what a chain offers); amount is a decimal string in the from asset. The server tries every fee tier and quotes the best pool. No wallet approval needed.