build-x402-server

작성자: coinbase

x402 프로토콜로 HTTP 라우트에 대한 요금을 청구하고 CDP 관리 지갑에서 USDC를 수신하는 코드를 작성합니다. TypeScript(Express, Hono, Next.js) 및…을 다룹니다.

npx skills add https://github.com/coinbase/cdp-sdk --skill build-x402-server

Build an x402 server

Take the user from an unprotected HTTP route to one that answers 402 Payment Required and settles a real payment into a CDP-managed wallet.

Resolve the Decisions table below before writing any code, then read only the language and framework subsections you resolved to in step 3.

When not to use this skill

  • The user sells through Coinbase Business. The Business Checkouts API returns one checkout with a hosted payment URL for people and a payable x402_url for agents, and they run no server at all. Check for this early: for that user it is a genuinely better answer than anything below.
  • The user is charging for an MCP tool rather than an HTTP route. See Charge over MCP.
  • The user is the one paying. Use the build-x402-client skill.
  • The user wants a deployed money-making service with no code. Use the agentic-wallet monetize-service skill. It is the nearest neighbour to this skill and the most likely mis-selection.

Decisions

Resolve every row before writing code. Detect first; only ask when detection is ambiguous.

DecisionHow to detectAsk only ifDefault
Languagepackage.json -> TypeScript. pyproject.toml / requirements.txt -> Python.Both present, or neitherAsk
FrameworkRead deps for express, hono, next, fastapi, flask.No server framework presentAsk; suggest Express or FastAPI
Wiring approachAn existing x402ResourceServer or paymentMiddleware call -> facilitator swap.Greenfield
Route config sourceAn x402.config.json already in the project -> config file.Inline in code
Receiver walletThe user supplied a payTo address -> use it.CDP-provisioned wallet
Networkenvironment: "development" selects testnets.Never assume mainnetdevelopment
SchemeFixed price -> exact. Metered or usage-based -> upto.exact

Two hard rules, not preferences:

  1. Never move a server to mainnet unless the user asks in the current turn. That puts real payers in front of a route that may not be ready.
  2. If the user supplies a payTo address, echo it back for confirmation before writing it. A typo'd receiver sends every future payment somewhere unrecoverable.

Steps

1. Confirm credentials

Before installing anything, check the environment for CDP_API_KEY_ID, CDP_API_KEY_SECRET, and CDP_WALLET_SECRET. The API key authenticates the server to the CDP Facilitator; the wallet secret provisions the wallet that receives payments, and is only needed when the user has not supplied a payTo of their own. Send them to API key authentication if they have no key. Also confirm the runtime: Node.js 22 or later, or Python 3.10 or later.

2. Install

Pick the line matching the Decisions table. @x402/core, @x402/evm, @x402/svm, and @x402/extensions are optional peer dependencies of the CDP SDK, so they are not installed for you, and all four are needed even for an EVM-only server because @coinbase/cdp-sdk/x402 imports them at module load. Only the framework and its adapter change between the three TypeScript lines.

# TypeScript, Express
npm install express @coinbase/cdp-sdk @x402/core @x402/evm @x402/svm @x402/extensions @x402/express

# ...or Hono:    hono @hono/node-server, and @x402/hono in place of @x402/express
# ...or Next.js: next, and @x402/next in place of @x402/express

# Python
pip install "cdp-sdk" "x402[evm,svm,fastapi]" uvicorn   # FastAPI
pip install "cdp-sdk" "x402[evm,svm,flask]"             # Flask

3. Price the route

Three things are needed from the user before writing anything. Ask for whichever cannot be inferred: which routes to charge for, the price per call, and a one-line description of what each route returns. The description is not decoration — it is what buyers see when the service is listed for discovery, so a vague one costs the user customers later.

State the containment rule plainly: only routes named in the config are protected, everything else stays free. That is the sentence that stops someone paywalling /health.

Read only the subsections matching the language and framework resolved above.

TypeScript

createX402Server provisions the receiver wallet, wires the CDP Facilitator, registers the schemes and extensions, and returns an object any x402 framework adapter accepts. It is async — await it before app.use.

import { createX402Server } from "@coinbase/cdp-sdk/x402";
import { paymentMiddlewareFromHTTPServer } from "@x402/express";
import express from "express";

const app = express();

const server = await createX402Server({
  environment: "development", // testnets and test funds
  routes: {
    "GET /report": { price: "$0.01", description: "Generate a concise research report" },
  },
});

app.use(paymentMiddlewareFromHTTPServer(server));
app.get("/report", (_req, res) => res.json({ report: "..." }));

app.listen(8402, () => console.log(`Receiving payments at ${server.payToEvmAddress}`));

Two variants on that shape:

  • The user already runs x402. Do not rewrite their server. Replace the facilitator argument with createCdpFacilitatorClient() from @coinbase/cdp-sdk/x402 — same return type, so nothing else in their code moves. This path needs a payTo address, and the factory is synchronous.
  • Routes belong in a file. Pass configPath: "./x402.config.json" instead of routes. Inline routes win per key when both are given, which is how you keep a shared file and still special-case one route in code. Keep credentials in environment variables, not the file.

Hono is the Express code with @x402/hono in place of @x402/express and serve({ fetch: app.fetch, port }) in place of app.listen. The server object is identical.

Next.js is the one genuine exception. App Router route files re-evaluate, so build the server once in its own module and import it from the handler:

// app/x402.ts — note the /server subpath: the client ExactEvmScheme needs a signer
import { x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { createCdpFacilitatorClient } from "@coinbase/cdp-sdk/x402";

export const server = new x402ResourceServer(createCdpFacilitatorClient()).register(
  "eip155:84532",
  new ExactEvmScheme(),
);

// app/api/report/route.ts
import { withX402 } from "@x402/next";
export const GET = withX402(handler, { accepts: [...], description: "..." }, server);

Gotchas worth stating once:

  • Register the middleware before the protected handlers.
  • Omitting environment means mainnet.
  • Under "development", routes default to both Base Sepolia and Solana Devnet.

Usage-based pricing (upto) only when the user asks for it. The route takes scheme: "upto" and a price that acts as a ceiling; the handler calls setSettlementOverrides(res, { amount }) with the amount actually used before sending the body. amount is a string, and it accepts atomic units ("100000" is $0.10 in 6-decimal USDC), a dollar price ("$0.05"), or a percentage of the authorized ceiling ("50%") — pick whichever the usage calculation produces naturally. upto is EVM-only, so under "development" it resolves to Base Sepolia alone.

Python

There is no createX402Server in Python, so assemble the pieces by hand. It is two halves, and naming them is what keeps the Python version from reading as long and arbitrary:

  1. A CDP wallet to receive payments, resolved from cdp.evm.get_or_create_account(...).address.
  2. The x402 Foundation middleware, pointed at the CDP Facilitator with create_facilitator_config().
from cdp.x402 import create_facilitator_config
from fastapi import FastAPI
from x402.http import HTTPFacilitatorClient, PaymentOption
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
from x402.http.types import RouteConfig
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.server import x402ResourceServer

PAY_TO = "0x1234567890123456789012345678901234567890"  # Your EVM address to get paid on Base Sepolia
NETWORK = "eip155:84532"  # Base Sepolia

server = x402ResourceServer(HTTPFacilitatorClient(create_facilitator_config()))
server.register(NETWORK, ExactEvmServerScheme())

routes = {
    "GET /report": RouteConfig(
        accepts=[PaymentOption(scheme="exact", pay_to=PAY_TO, price="$0.01", network=NETWORK)],
        mime_type="application/json",
        description="AI-generated report",
    ),
}

app = FastAPI()
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)

Run it with uvicorn.run(app, port=8402).

The sharpest edge is resolving PAY_TO. CdpClient is an async context manager, but the route config above is module-level and synchronous, which is why the examples resolve the receiver once at import time with asyncio.run(resolve_pay_to()). That works when the module is the entry point. Under an ASGI server that imports it from inside a running event loop, it raises RuntimeError, and the user needs a lifespan hook instead.

Flask is the same code with three substitutions: x402ResourceServerSync and HTTPFacilitatorClientSync in place of the async pair, and payment_middleware(app, routes=routes, server=server) from x402.http.middleware.flask, which is a function that mutates the app rather than a middleware class. Handing Flask the async x402ResourceServer raises a TypeError.

Two more gotchas: PaymentOption is a dataclass whose scheme, pay_to, price, and network have no defaults, so a missing one is a TypeError at construction — which, with a module-level route map, means the server refuses to import rather than failing a request later. And this path is EVM-only, with no Solana option.

4. Confirm the route is protected

Start the server, then from a second terminal:

curl -i http://localhost:8402/report
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwi...

This is the cheap checkpoint before any money moves, and it needs no buyer. Do not skip to step 5.

5. Take a real payment

Either testing path works:

  • Point the agentic-wallet pay-for-service skill at http://localhost:8402/report.
  • Build a buyer with the build-x402-client skill and point it at the same URL.

Success is HTTP 200 on the buyer side. The buyer wallet needs testnet USDC first, which is step 4 of the client skill — link it rather than re-teaching funding here.

Troubleshooting

SymptomCauseFix
Route returns 200 with no paymentRoute key does not match the real method and path, or middleware was registered after the handlerCompare the key to the handler; move app.use above it
402 with no PAYMENT-REQUIRED headerThe middleware was never reachedCheck registration order and the mount path
Verification passes, settlement failsBuyer and server are on different chainsMatch the buyer's network to the one in the 402
Auth error at startupCDP_API_KEY_* not visible to the processCheck how the process loads its environment, not just .env
Payments land somewhere unknownA CDP wallet was provisioned and the printed payTo was never recordedRead it back from server.payToEvmAddress and save it

Runnable examples

TypeScript, under https://github.com/coinbase/cdp-sdk/blob/main/examples/typescript/x402/servers/: express/server.ts (all three approaches), express/x402.config.json and express/x402.config.schema.json, hono/server.ts, next/app/api/report/route.ts, mcp/server.ts.

Python, under https://github.com/coinbase/cdp-sdk/blob/main/examples/python/x402/servers/: fastapi/server.py, flask/server.py, bazaar.py, mcp/server.py.

After the first payment

  • Make the endpoint findable: Get discovered. TypeScript's createX402Server handles it automatically; Python needs manual metadata like bazaar.py above
  • What settled the payment: CDP Facilitator
  • Other networks, schemes, receivers, lifecycle hooks: Production configuration
  • Charging for MCP tools: Charge over MCP
  • Mainnet: drop environment: "development" and confirm with the user first

coinbase의 다른 스킬

git.repo-manager
coinbase
git.repo-manager — AI 에이전트를 위한 설치 가능한 스킬로, coinbase/cds에서 게시했습니다.
official
agentic-wallet
coinbase
awal CLI를 통한 암호화폐 지갑 작업 — 로그인, 잔액 확인, USDC/ETH/POL/SOL 전송, 토큰 거래, 지갑 충전, x402 결제 프로토콜 사용 등
official
authenticate-wallet
coinbase
이메일 OTP 기반 지갑 인증으로 검증 및 상태 확인을 제공합니다. 2단계 로그인 절차: 이메일로 6자리 OTP를 받기 위해 시작한 후, flowId와 코드로 인증을 완료합니다. 명령어 실행 전 셸 인젝션을 방지하기 위해 이메일, flowId, OTP에 대한 입력 검증 규칙이 포함되어 있습니다. 동반 CLI 명령어를 통해 상태 확인, 잔액 조회, 주소 검색 및 지갑 창 접근을 제공합니다. 모든 명령어는 기계 판독 가능한 출력을 위해 --json을 지원합니다...
official
fund
coinbase
Coinbase Onramp 또는 직접 전송을 통해 USDC를 지갑에 입금합니다. 사용자가 사전 설정된 금액($10, $20, $50) 또는 사용자 지정 값을 선택하고 Apple Pay, 직불카드, 은행 송금 또는 Coinbase 계정 자금 조달 중에서 선택할 수 있는 보조 UI를 엽니다. 다양한 결제 수단을 지원하며 정산 시간이 다릅니다: 카드 및 Apple Pay는 즉시, ACH 은행 송금은 1~3일 소요됩니다. Base 네트워크에서 USDC로 자금을 입금하며, 또는 사용자는 npx awal@2.0.3...을 통해 지갑 주소로 직접 USDC를 보낼 수 있습니다.
official
monetize-service
coinbase
x402 프로토콜을 통해 다른 에이전트가 발견하고 결제할 수 있는 유료 API 엔드포인트를 배포합니다. HTTP 402 결제 프로토콜을 사용하여 Base에서 요청당 USDC를 청구하며, 클라이언트는 서명된 트랜잭션으로 결제하고 API 키나 계정이 필요하지 않습니다. 검색 확장을 선언하면 엔드포인트를 x402 Bazaar에 자동으로 등록하여 에이전트가 발견할 수 있도록 합니다. Express 미들웨어를 사용하여 엔드포인트당 여러 가격 계층, 와일드카드 경로 및 여러 결제 옵션을 지원합니다. @x402/express 및 @x402/core 기반으로 구축되었습니다...
official
pay-for-service
coinbase
Base에서 x402 프로토콜을 통해 자동 USDC 결제로 유료 API를 호출합니다. x402 지원 엔드포인트에 HTTP 요청(GET, POST 등)을 실행하며, USDC 결제가 자동으로 처리됩니다. 메서드, JSON 본문, 쿼리 매개변수 및 사용자 정의 헤더를 통해 요청을 사용자 지정할 수 있습니다. 결제 제어 기능이 포함되어 있어 요청당 최대 USDC 금액을 설정하고 상관 ID로 관련 작업을 그룹화할 수 있습니다. 지갑 인증과 충분한 USDC 잔액이 필요하며, 셸을 방지하기 위해 모든 사용자 입력을 검증합니다...
official
query-blockchain-data
coinbase
Base에서 CDP SQL API를 통해 x402로 온체인 블록체인 데이터를 조회합니다. 사용자나 본인이 디코딩된 블록에 대한 온체인 정보를 확인하고자 할 때 사용하세요.
official
query-onchain-data
coinbase
Base에서 SQL을 사용하여 온체인 데이터를 쿼리하고, 쿼리당 x402 결제를 적용합니다. CoinbaseQL을 통해 디코딩된 이벤트, 트랜잭션 및 블록에 접근할 수 있습니다. CoinbaseQL은 조인, CTE, 서브쿼리 및 표준 함수를 지원하는 ClickHouse 기반 SQL 방언입니다. 세 가지 주요 테이블을 사용할 수 있습니다: base.events(디코딩된 스마트 컨트랙트 로그), base.transactions(전체 트랜잭션 데이터), base.blocks(블록 메타데이터). 이벤트 쿼리에서 전체 테이블 스캔을 피하기 위해 인덱싱된 필드(event_signature, address, block_timestamp)에 대한 필터링이 필요합니다.
official