x402 결제 2026 v2 구현과 정산 검증
x402 v2로 API에 소액 결제를 붙이는 Node.js 실습입니다. Base Sepolia에서 결제 조건 선택, 서명 재요청, 정산 검증을 수행하고 재전송과 메인넷 전환 조건을 점검하세요.
- 수취 주소와 테스트 결제 지갑을 준비하고 공식 x402 SDK를 설치하세요.
- Base Sepolia용 결제 미들웨어를 연결하고 무결제 요청의 402 응답을 확인하세요.
- 클라이언트에서 허용한 체인·토큰·금액·수취인에 맞는 조건만 선택해 서명하세요.
- 정산 응답의 거래 해시로 USDC Transfer 이벤트를 조회하세요.
- 실패 차단과 중복 요청 복구를 시험한 뒤 메인넷 설정을 검토하세요.
x402 결제는 API의 402 응답에 결제 조건을 담아 구현합니다. Node.js와 테스트 지갑을 준비하세요. 먼저 Base Sepolia에서 v2 서명과 정산을 검증합니다. 유료 응답은 정산 성공 뒤에 전달하도록 설정하세요.
금액·체인 정보는 출처에 연결한 x402·Base·Circle 공식 문서 기준입니다.
실습 준비와 설치
API를 수정할 수 있는 개발 환경과 테스트 지갑을 준비하세요. 예제는 Node.js 22와 x402 SDK 2.28.0을 기준으로 구성했습니다. 설치 구조는 공식 판매자 가이드를 따릅니다.
- 결제를 받을 EVM 지갑 주소를 준비하세요.
- 결제용 테스트 지갑을 별도로 준비하세요.
- 결제용 지갑에 Base Sepolia 테스트 USDC를 확보하세요.
- 아래 명령으로 실습 프로젝트를 만드세요.
- 이후의 TypeScript 파일을 같은 폴더에 저장하세요.
mkdir x402-demo
cd x402-demo
npm init -y
npm pkg set type=module
npm install --save-exact [email protected] [email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected]
npm install --save-dev --save-exact typescript tsx @types/node @types/express
수취 서버에는 수취 지갑의 개인키가 필요하지 않습니다. 결제용 개인키는 클라이언트 프로세스에서만 읽도록 구성하세요. 지갑 서명 방식은 공식 구매자 가이드를 기준으로 합니다.
이 예제의 검증 범위는 타입 검사와 모의 결제 흐름입니다. 실제 테스트넷 송금까지 완료된 예제는 아닙니다. 아래 실행 절차에서 온체인 결과를 직접 확인하세요.
x402 v1과 v2 비교
v2를 구현할 때는 헤더와 본문 필드를 함께 맞춰야 합니다. 헤더 이름만 바꾸면 데이터 구조가 맞지 않습니다. 다음 표는 공식 이전 가이드를 기준으로 합니다.
| 구분 | v1 | v2 |
|---|---|---|
| 프로토콜 버전 | x402Version: 1 |
x402Version: 2 |
| 최초 결제 조건 | 402 응답의 JSON 본문 |
PAYMENT-REQUIRED 응답 헤더 |
| 서명한 결제 데이터 |
X-PAYMENT 요청 헤더 |
PAYMENT-SIGNATURE 요청 헤더 |
| 정산 결과 |
X-PAYMENT-RESPONSE 응답 헤더 |
PAYMENT-RESPONSE 응답 헤더 |
| Base 메인넷 식별자 | base |
eip155:8453 |
| Base Sepolia 식별자 | base-sepolia |
eip155:84532 |
| 결제 조건의 금액 필드 | maxAmountRequired |
amount |
| 리소스 설명 | 결제 조건 안의 resource 등 |
최상위 resource 객체 |
| 결제 페이로드의 조건 | 최상위 scheme, network 등 |
선택한 조건을 담는 accepted 객체 |
| TypeScript 패키지 예 | x402-express |
@x402/express |
v1의 결제 조건 전달 위치는 HTTP v1 사양에 명시돼 있습니다. v2의 표준 전달 위치는 PAYMENT-REQUIRED 헤더입니다. 구현체가 본문도 반환할 수 있지만 헤더를 기준으로 처리하세요.
HTTP v2 사양은 상태 코드를 다음과 같이 설명합니다.
The server indicates payment is required using the HTTP 402 "Payment Required" status code.
출처: x402 HTTP v2 사양
v2 결제 헤더는 JSON을 Base64로 인코딩한 값입니다. Base64는 암호화 방식이 아닙니다. 외부 서비스에서는 HTTPS를 사용하세요.
HTTP 402 요청과 결제 순서
이 실습은 EVM exact 방식의 기본 승인 흐름을 사용합니다. exact는 정해진 금액을 결제하는 방식입니다. 순서는 x402 v2 핵심 사양에 따릅니다.
- 클라이언트가 결제 정보 없이 유료 API를 요청합니다.
- 서버가 HTTP 402와
PAYMENT-REQUIRED를 반환합니다. - 클라이언트가
accepts에서 허용된 조건을 선택합니다. - SDK가 선택한 조건에 맞는 결제 승인을 서명합니다.
- 클라이언트가
PAYMENT-SIGNATURE를 붙여 재요청합니다. - 서버가 facilitator의
/verify로 승인을 검증합니다. - 검증을 통과하면 서버가 리소스 처리를 실행합니다.
- 서버가
/settle로 정산을 요청합니다. - 정산 성공 뒤 유료 본문과
PAYMENT-RESPONSE를 전달합니다.
이 순서는 모든 결제 방식에 공통인 고정 순서가 아닙니다. v2에는 정산 시점이 다른 흐름도 정의돼 있습니다. 아래 코드는 기본 authorization 흐름만 허용합니다.
Resource server와 facilitator의 역할
Resource server는 가격과 콘텐츠 접근 조건을 결정합니다. Facilitator는 결제 검증과 정산을 담당합니다. 역할 구분은 공식 facilitator 문서를 기준으로 합니다.
| 구성 요소 | 맡는 일 | 성공으로 간주하면 안 되는 상태 |
|---|---|---|
| 클라이언트 | 조건 선택과 결제 승인 서명 | 서명 생성만 완료된 상태 |
| Resource server | 가격 선언과 유료 응답 통제 | 클라이언트가 성공했다고 주장하는 상태 |
Facilitator /verify
|
서명과 결제 조건의 유효성 확인 |
isValid: true만 받은 상태 |
Facilitator /settle
|
이 실습에서는 온체인 결제 실행과 결과 반환 | 전송했지만 결과를 확인하지 못한 상태 |
/verify는 온체인 상태를 변경하지 않는 검사입니다. 따라서 검증 성공은 입금 완료를 뜻하지 않습니다. 검증 이후에도 잔액이나 승인 상태가 달라질 수 있습니다.
검증이나 정산이 실패하면 유료 본문을 차단하세요. 성공을 확인하지 못했을 때 차단하는 처리를 fail-closed라고 합니다. 이 동작은 공식 Express 구현에서도 확인할 수 있습니다.
Base 메인넷과 테스트넷 비교
첫 실행에서는 Base Sepolia만 사용하세요. Circle은 테스트넷 USDC에 금융 가치가 없다고 명시합니다. 주소는 Circle의 USDC 컨트랙트 목록을 기준으로 합니다.
| 항목 | Base 메인넷 | Base Sepolia 테스트넷 |
|---|---|---|
| 용도 | 실제 자산 결제 | 구현과 장애 처리 시험 |
| Chain ID | 8453 |
84532 |
| x402 v2 네트워크 | eip155:8453 |
eip155:84532 |
| USDC 컨트랙트 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| 공개 RPC | https://mainnet.base.org |
https://sepolia.base.org |
| 토큰의 금융 가치 | 실제 금융 가치가 있는 자산 | 금융 가치 없음 |
Chain ID와 RPC는 Base 네트워크 문서에서 확인하세요. 같은 지갑 주소라도 체인이 다르면 잔액이 별개입니다. 테스트넷 성공을 실제 매출로 기록하면 안 됩니다.
테스트 USDC는 Circle Testnet Faucet에서 요청할 수 있습니다. 네트워크는 Base Sepolia로 선택하세요. 수령 주소에는 결제용 테스트 지갑 주소를 입력합니다.
Node.js 유료 API 구현
유료 경로 앞에 paymentMiddleware를 연결하세요. 아래 예제는 호출당 0.001 USDC를 요구합니다. 금액은 공식 판매자 가이드의 예시 가격을 사용합니다. (확인값 호출당 0.001 USDC · 출처 docs.x402.org · 2026-10-28 확인)
서버 파일
다음 코드를 server.ts로 저장하세요. 서버는 로컬 인터페이스에서만 요청을 받습니다. 수취 주소는 PAY_TO 환경변수에서 읽습니다.
import express from "express";
import { getAddress, zeroAddress } from "viem";
import { paymentMiddleware } from "@x402/express";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const resourceServer = new x402ResourceServer(
new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }),
).register(network, new ExactEvmScheme());
const app = express();
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "private, no-store");
next();
});
app.use(paymentMiddleware({
"GET /paid-data": {
accepts: [{ scheme: "exact", price: "$0.001", network, payTo }],
description: "Sample paid JSON",
mimeType: "application/json",
},
}, resourceServer));
app.get("/paid-data", (_req, res) => {
res.json({ content: "Paid sample content" });
});
app.listen(4021, "127.0.0.1");
https://x402.org/facilitator는 공식 가이드의 테스트용 주소입니다. 이 예제에서는 Base Sepolia 정산에 사용합니다. 운영용 facilitator 선택은 판매자 가이드의 메인넷 절차를 확인하세요.
가격 계산 예시
예제 가격은 USDC 최소 단위로 1000입니다. USDC의 소수 자릿수는 6자리입니다. SDK의 가격 변환은 EVM 서버 구현을 기준으로 합니다.
0.001 USDC × 10^6 = 1000 최소 단위
| 확인할 값 | 이 예제의 기대값 |
|---|---|
서버 설정의 price
|
"$0.001" |
402 결제 조건의 amount
|
"1000" |
결제 조건의 asset
|
Base Sepolia USDC 컨트랙트 |
결제 조건의 payTo
|
설정한 수취 주소 |
결제 조건의 network
|
eip155:84532 |
price는 SDK 설정값입니다. 실제 v2 결제 조건에는 amount가 전달됩니다. 정수 금액은 문자열이나 bigint로 비교하세요.
서버 실행과 402 확인
첫 번째 Bash 터미널에서 서버를 실행하세요. 입력하는 값은 개인키가 아닌 공개 수취 주소입니다. 실행 중에는 이 터미널을 유지합니다.
read -r -p '수취 지갑 주소: ' PAY_TO
export PAY_TO
npx tsx server.ts
두 번째 터미널에서 무결제 요청을 보내세요. 아래 명령은 결제를 서명하지 않습니다. HTTP 상태와 디코딩한 결제 조건을 출력합니다.
node --input-type=module <<'JS'
const response = await fetch("http://127.0.0.1:4021/paid-data");
const header = response.headers.get("PAYMENT-REQUIRED");
console.log(response.status);
console.log(header && JSON.parse(
Buffer.from(header, "base64").toString("utf8")
));
JS
정상적인 무결제 응답은 HTTP 402입니다. 출력된 조건을 위 표와 대조하세요. 유료 콘텐츠가 바로 반환되면 경로 보호 설정부터 수정합니다.
결제 조건 선택과 클라이언트 구현
허용한 조건에만 서명하도록 클라이언트를 제한하세요. 이 예제는 Base Sepolia USDC의 정해진 가격만 허용합니다. 결제 옵션 필터는 공식 x402Client 구현의 registerPolicy를 사용합니다.
- 아래 코드를
client.ts로 저장하세요. - 다음 절의
check-transfer.ts도 저장하세요. - 두 파일을 준비한 뒤 실행 절차를 진행하세요.
import { writeFile } from "node:fs/promises";
import { getAddress, zeroAddress, type Hex, type Hash } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { decodePaymentResponseHeader } from "@x402/core/http";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { checkTransfer } from "./check-transfer.js";
const key = process.env.EVM_PRIVATE_KEY ?? "";
if (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error("Invalid test key");
const signer = privateKeyToAccount(key as Hex);
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const token = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const client = new x402Client().register(network, new ExactEvmScheme(signer));
client.registerPolicy((version, options) => version === 2
? options.filter(r =>
r.scheme === "exact" && r.network === network &&
r.asset.toLowerCase() === token.toLowerCase() &&
r.payTo.toLowerCase() === payTo.toLowerCase() && r.amount === "1000" &&
(!r.extra?.assetTransferMethod || r.extra.assetTransferMethod === "eip3009") &&
(!r.extra?.paymentFlow || r.extra.paymentFlow === "authorization"))
: []);
const paidFetch = wrapFetchWithPayment(fetch, client);
const response = await paidFetch("http://127.0.0.1:4021/paid-data", {
redirect: "error",
});
const raw = response.headers.get("PAYMENT-RESPONSE");
if (!raw) throw new Error(`No settlement receipt: HTTP ${response.status}`);
const settlement = decodePaymentResponseHeader(raw);
await writeFile("settlement.json", JSON.stringify(settlement, null, 2));
if (!response.ok || !settlement.success || settlement.network !== network) {
throw new Error("Payment unresolved; inspect settlement.json before retrying");
}
const evidence = await checkTransfer(
network, settlement.transaction as Hash, signer.address, payTo, 1000n,
);
await writeFile("transfer.json", JSON.stringify(evidence, null, 2));
console.log(await response.text());
wrapFetchWithPayment가 402 처리와 서명 재요청을 담당합니다. 사용 방식은 공식 fetch 예제를 따릅니다. 허용된 결제 조건이 없으면 결제 생성을 중단합니다.
AI 에이전트에서도 이 클라이언트를 사용할 수 있습니다. 다만 호출당 가격 제한은 누적 지출 제한이 아닙니다. 에이전트의 반복 호출 예산은 별도로 관리하세요.
USDC Transfer 이벤트 검증
정산 결과의 거래 해시로 USDC 전송 로그를 확인하세요. 거래 성공 상태만으로 수취인까지 확인되지는 않습니다. Transfer 필드는 ERC-20 표준을 기준으로 합니다.
검증에는 다음 항목이 필요합니다.
| 검증 항목 | 비교 대상 |
|---|---|
| RPC의 Chain ID | 서버가 허용한 체인 |
거래 영수증의 status
|
성공 상태 |
| 이벤트를 발생시킨 주소 | 해당 체인의 공식 USDC 컨트랙트 |
Transfer.from |
확인된 결제자 |
Transfer.to |
서버가 정한 수취 주소 |
Transfer.value |
서버가 요구한 최소 단위 금액 |
아래 코드를 check-transfer.ts로 저장하세요. 조회는 viem의 getTransactionReceipt를 사용합니다. 이벤트 해석은 parseEventLogs를 기준으로 합니다.
import {
createPublicClient, http, getAddress, parseEventLogs,
erc20Abi, type Address, type Hash,
} from "viem";
import { base, baseSepolia } from "viem/chains";
const configs = {
"eip155:84532": {
chain: baseSepolia,
rpc: "https://sepolia.base.org",
token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
},
"eip155:8453": {
chain: base,
rpc: "https://mainnet.base.org",
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
},
} as const;
export async function checkTransfer(
network: keyof typeof configs, hash: Hash,
payer: Address, payTo: Address, amount: bigint,
) {
const cfg = configs[network];
const rpc = createPublicClient({ chain: cfg.chain, transport: http(cfg.rpc) });
if (await rpc.getChainId() !== cfg.chain.id) throw new Error("Wrong chain");
const receipt = await rpc.getTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Transaction failed");
const logs = parseEventLogs({
abi: erc20Abi,
eventName: "Transfer",
logs: receipt.logs.filter(
log => getAddress(log.address) === getAddress(cfg.token),
),
strict: true,
});
const transfer = logs.find(log =>
getAddress(log.args.from) === getAddress(payer) &&
getAddress(log.args.to) === getAddress(payTo) &&
log.args.value === amount,
);
if (!transfer) throw new Error("Matching USDC Transfer not found");
return {
network, transaction: hash, logIndex: transfer.logIndex,
blockHash: receipt.blockHash, blockNumber: receipt.blockNumber.toString(),
};
}
이 함수는 해당 거래의 USDC 전송 사실을 확인합니다. 특정 API 주문에 귀속됐는지는 별도로 확인해야 합니다. 운영 서버에서는 저장된 주문 조건을 비교값으로 사용하세요.
클라이언트가 올린 transfer.json을 접근 허가 증거로 믿으면 안 됩니다. 이 예제의 서버는 facilitator의 검증·정산 결과를 사용합니다. 독립 조회를 접근 허가 조건으로 삼으려면 서버에서 실행해야 합니다.
영수증 조회 실패는 곧 결제 실패를 뜻하지 않습니다. RPC 오류라면 저장된 거래 해시를 다시 조회하세요. 추가 확인 수와 체인 재구성 대응은 운영 정책으로 정합니다.
테스트 결제 실행과 결과 확인
파일을 모두 저장한 뒤 테스트 지갑으로 실행하세요. 타입 검사는 서명이나 송금을 수행하지 않습니다. 아래 명령은 앞서 설치한 TypeScript 도구를 사용합니다.
- 다음 명령으로 타입을 검사하세요.
- 두 번째 터미널에 서버와 같은 수취 주소를 입력하세요.
- 테스트 결제 지갑의 개인키를 숨김 입력하세요.
- 클라이언트를 한 번 실행하세요.
- 저장된 정산 결과와 전송 로그를 확인하세요.
npx tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext server.ts client.ts check-transfer.ts
read -r -p '같은 수취 지갑 주소: ' PAY_TO
read -r -s -p '테스트 결제 지갑 개인키: ' EVM_PRIVATE_KEY
export PAY_TO EVM_PRIVATE_KEY
npx tsx client.ts
unset EVM_PRIVATE_KEY
| 결과물 | 의미 | 확인할 내용 |
|---|---|---|
settlement.json |
서버가 반환한 정산 결과 |
success, network, transaction
|
transfer.json |
RPC 조회에서 찾은 전송 기록 | 체인, 거래 해시, 로그 위치, 블록 정보 |
| 콘솔의 유료 본문 | 이 클라이언트의 검증 이후 출력한 데이터 | 요청한 콘텐츠인지 확인 |
클라이언트를 새로 실행하면 새 결제가 발생할 수 있습니다. 이 최소 예제에는 주문별 중복 방지 저장소가 없습니다. 재시도 기능은 다음 절의 조건을 갖춘 뒤 추가하세요.
검증 실패 조건별 처리
실패 시험에서는 유료 본문이 노출되지 않는지 확인하세요. HTTP 상태만 검사하면 차단 누락을 놓칠 수 있습니다. 기준 동작은 공식 Express 미들웨어를 참고합니다.
| 시험 조건 | 기대하는 처리 | 추가 확인 |
|---|---|---|
| 결제 헤더 없음 | 402와 결제 조건 반환 | 유료 본문 없음 |
| 다른 토큰·수취인 제시 | 클라이언트 정책에서 거절 | 서명 생성 안 함 |
| 잘못된 서명 | 검증 실패로 차단 | 유료 핸들러 실행 안 함 |
| 잔액 부족 | 검증 또는 정산 실패로 차단 | 정산 성공으로 기록 안 함 |
| 정산 결과가 실패 | 유료 응답 차단 | 준비된 본문도 전송 안 함 |
| facilitator 응답 지연 | 성공 미확인 상태로 보류 | 새 승인 자동 생성 금지 |
| RPC 영수증 조회 오류 | 거래 해시를 보관하고 재조회 | 결제 실패로 단정 금지 |
기본 승인 흐름에서는 정산 전에 핸들러가 실행됩니다. 정산 실패가 외부 작업까지 되돌려 주지는 않습니다. 이 예제처럼 부수 효과 없는 조회부터 적용하세요.
이메일 발송이나 외부 주문 생성은 별도 설계가 필요합니다. 결제와 작업 상태를 함께 기록하세요. 실패 시 재실행 범위를 정해야 중복 작업을 줄일 수 있습니다.
정산 후 응답 유실과 재전송 복구
결제 완료와 콘텐츠 전달 완료를 별도 상태로 저장하세요. 결제는 끝났지만 응답 연결이 끊길 수 있습니다. 이런 상황을 처리하는 근거는 Payment-Identifier 확장 문서입니다.
동일 작업을 다시 요청해도 중복 처리하지 않는 성질을 멱등성이라고 합니다. payment-identifier는 이를 위한 결제 식별자를 제공합니다. 식별자만 추가해도 자동으로 완성되는 기능은 아닙니다.
중복 결제와 재전송 비교
같은 서명의 재사용 방지와 주문 중복 방지는 구분해야 합니다. EIP-3009는 사용한 승인 nonce의 재사용을 막습니다. 이 동작은 ERC-3009 사양에 정의돼 있습니다.
| 상황 | 온체인 승인 관점 | API 운영 관점 |
|---|---|---|
| 같은 승인을 다시 정산 | 이미 사용한 nonce 검사 대상 | 기존 결과 조회 필요 |
| 같은 주문에 새 승인 생성 | 별개의 승인으로 처리될 수 있음 | 중복 결제 방지 필요 |
| 정산 성공 후 응답 유실 | 자산 이동은 완료될 수 있음 | 콘텐츠 재전달 필요 |
| 정산 요청 후 시간 초과 | 결과가 아직 불명확함 | 보류 상태와 정산 확인 필요 |
다음 절차는 공식 확장을 운영 저장소에 연결하는 설계안입니다. 앞의 최소 코드에는 포함돼 있지 않습니다. 운영에 적용하려면 영속 저장소와 동시 요청 제어를 추가하세요.
- 서버에서
payment-identifier확장 지원을 선언하세요. - 클라이언트에서 논리적 요청의 식별자를 보관하세요.
- 서버에서 식별자를 결제자와 요청 내용에 연결하세요.
- 처리 중 상태를 원자적으로 기록해 동시 실행을 막으세요.
- 정산 성공 시 거래 증거와 응답 결과를 저장하세요.
- 재요청의 권한과 내용이 같으면 저장된 결과를 반환하세요.
- 정산 결과가 불명확하면 기존 승인부터 확인하세요.
결제 식별자는 비밀번호나 접근 토큰이 아닙니다. 식별자만 아는 요청에 유료 캐시를 반환하면 안 됩니다. 다른 사용자나 다른 요청 본문에 재사용되는지도 검사하세요.
운영 기록에는 체인·거래 해시·로그 위치를 함께 남기세요. 이 기록을 주문과 연결해 다른 주문의 재사용을 차단합니다. Transfer 일치만으로 주문 귀속까지 입증되지는 않습니다.
결제 구현의 흔한 실수
버전 필드와 성공 신호를 혼동하면 결제 판정이 틀어집니다. 아래 표를 배포 검토에 활용하세요. 버전 차이는 x402 이전 가이드를 기준으로 합니다.
| 흔한 실수 | 바로잡을 기준 |
|---|---|
extra.version: "2"를 x402 버전으로 해석 |
프로토콜 버전은 x402Version으로 확인 |
v2에 maxAmountRequired 사용 |
결제 조건의 금액은 amount 사용 |
X-PAYMENT-REQUIRED 헤더를 임의로 생성 |
v1은 본문, v2는 PAYMENT-REQUIRED 사용 |
isValid: true를 입금 완료로 처리 |
/settle 결과를 별도로 확인 |
거래의 최상위 to를 USDC 수취인으로 처리 |
USDC Transfer.to 확인 |
| 서명자가 거래 전송자와 같다고 가정 | 승인 결제자와 Transfer.from 비교 |
| 테스트넷 수령액을 매출로 합산 | 메인넷과 테스트넷 장부 분리 |
| 시간 초과 때마다 새 결제 생성 | 기존 주문과 승인 상태부터 복구 |
| SDK 패키지 버전과 프로토콜 버전을 동일시 | 패키지 버전과 x402Version을 별도 관리 |
USDC 서명의 도메인 정보에도 버전 문자열이 들어갑니다. 그 값은 x402 프로토콜 버전과 목적이 다릅니다. 서명 도메인 구조는 ERC-3009 사양을 확인하세요.
보안 점검과 외부 서비스의 신뢰 범위
결제 성공 판정은 서버에서 통제하세요. 외부 facilitator의 응답도 명시적인 신뢰 대상입니다. 자체 운영도 가능한 구조는 facilitator 문서에 설명돼 있습니다.
- 클라이언트의 성공 메시지로 콘텐츠를 해제하지 마세요.
- 서버가 선언한 체인·토큰·금액·수취인을 비교하세요.
- 개인키와 시드 문구를 로그에 남기지 마세요.
- 승인 서명 전체도 일반 접근 로그에서 제외하세요.
- 결제 헤더의 크기와 요청 처리 시간을 제한하세요.
- 유료 응답이 공유 캐시에 남지 않도록 점검하세요.
- 테스트 키와 운영 키의 보관 위치를 분리하세요.
- facilitator 장애 시 무결제 우회 경로를 만들지 마세요.
- 승인 만료와 중복 요청을 실제로 시험하세요.
실습의 고정 의존성에는 보안 검사 경고가 남아 있습니다. npm audit는 zod 관련 권고를 보고했습니다. 관련 내용은 GitHub 보안 권고에서 확인하세요.
npm audit
운영 배포 전 수정 버전의 SDK 호환성을 확인하세요. 주요 버전을 강제로 교체하면 동작이 달라질 수 있습니다. 이 실습을 보안 검증이 끝난 배포 구성으로 사용하지 마세요.
자체 서버와 Cloudflare Gateway 비교
Cloudflare Gateway를 쓰면 원본 서버의 검증 방식이 달라집니다. 2026년 9월 30일자 문서는 x402 v2 사용을 명시합니다. 다음 구분은 Cloudflare x402 문서를 기준으로 합니다.
| 구분 | 이 글의 Express 방식 | Cloudflare Monetization Gateway |
|---|---|---|
| 클라이언트 결제 처리 | Express 미들웨어 | Gateway |
| 원본 서버의 검증 지점 | 선택한 facilitator와 연동 |
PAYMENT-CONTEXT JWT 검증 |
| 정산 결과 전달 | 미들웨어의 PAYMENT-RESPONSE
|
Gateway가 정산 영수증 생성 |
| 추가로 확인할 사항 | facilitator 지원 체인과 인증 | JWT 서명 검증과 원본 접근 보호 |
PAYMENT-CONTEXT는 Gateway와 원본 서버 사이의 헤더입니다. 일반 x402 클라이언트의 서명 헤더가 아닙니다. 원본 검증은 Cloudflare Payment validation 문서를 따르세요.
Gateway를 거쳐도 JWT를 단순 디코딩해서는 안 됩니다. 문서가 지정한 키로 서명을 검증하세요. 헤더가 없거나 검증이 실패하면 유료 콘텐츠를 차단합니다.
실제 사용량과 정산 기록 구분
온체인 송금 건수만으로 콘텐츠 소비를 입증할 수는 없습니다. 운영 지표는 정산과 응답 전달을 연결해 집계하세요. 이는 결제 흐름과 재전송 동작을 바탕으로 한 설계 제안입니다.
| 기록 | 답할 수 있는 질문 | 추가로 필요한 기록 |
|---|---|---|
| 402 응답 | 결제 조건을 얼마나 요청했는지 | 실제 결제 여부 |
| 검증 성공 | 승인이 유효했는지 | 정산 결과 |
| USDC 전송 | 자산이 이동했는지 | 주문 귀속 |
| 주문별 정산 완료 | 유료 요청이 결제됐는지 | 응답 전달 상태 |
| 재전달 기록 | 이미 결제한 요청을 복구했는지 | 신규 결제와 구분하는 식별자 |
테스트 요청은 실제 사용량 집계에서 구분하세요. 같은 주문의 재시도도 별도로 표시합니다. 거래량 자체를 API 수요나 수익 보장으로 설명하면 안 됩니다.
Base 메인넷 전환 절차
테스트넷의 정상·실패 경로를 확인한 뒤 메인넷을 검토하세요. 네트워크 문자열만 바꾸면 전환이 끝나지 않습니다. 기준 절차는 공식 판매자 가이드를 따릅니다.
- 선택한 facilitator의 Base 메인넷 지원을 확인하세요.
-
/supported에서 v2·exact·eip155:8453지원을 확인하세요. - 제공자가 요구하는 인증과 이용 조건을 설정하세요.
- 서버와 클라이언트의 네트워크를 함께 변경하세요.
- 클라이언트의 USDC 허용 주소를 메인넷 주소로 변경하세요.
- 402 응답의 금액과 실제 수취 주소를 다시 대조하세요.
- 중복 요청 저장소와 장애 복구 절차를 연결하세요.
- 운영 의존성의 보안 검사를 완료하세요.
- 설정한 소액으로 실제 전송과 콘텐츠 전달을 확인하세요.
다중 체인 지원은 모든 조합의 지원을 뜻하지 않습니다. SDK와 facilitator의 지원 범위를 함께 확인하세요. 확인 기준은 네트워크·토큰 지원 문서입니다.
FAQ
x402는 일반 USDC 입금 확인과 무엇이 다른가요?
HTTP 요청 과정에서 결제 조건을 전달하고 서명한 결제 승인으로 재요청합니다. 입금 내역을 나중에 대조하는 방식과 달리 유료 리소스 요청에 결제 절차를 연결합니다.
x402 v2의 결제 헤더는 무엇인가요?
결제 조건은 PAYMENT-REQUIRED, 결제 페이로드는 PAYMENT-SIGNATURE로 전달합니다. 정산 결과는 PAYMENT-RESPONSE로 전달합니다.
v1에 X-PAYMENT-REQUIRED 헤더가 있나요?
공식 HTTP v1 사양은 최초 결제 조건을 JSON 응답 본문에 담습니다. 결제 페이로드와 정산 결과에는 각각 X-PAYMENT와 X-PAYMENT-RESPONSE를 사용합니다.
/verify가 성공하면 결제가 완료된 것인가요?
/verify 성공은 결제 승인이 유효하다는 뜻입니다. 이 실습에서는 /settle의 성공 결과를 별도로 확인해야 합니다.
Base Sepolia 테스트 USDC를 실제 수익으로 볼 수 있나요?
Circle은 테스트넷 USDC에 금융 가치가 없다고 명시합니다. 테스트넷 정산은 실제 매출과 분리해서 기록하세요.
수취 서버에 개인키를 넣어야 하나요?
이 예제의 수취 서버에는 공개 수취 주소만 설정합니다. 결제 승인에 쓰는 개인키는 결제 클라이언트에서 읽습니다.
0.001 USDC의 amount 값은 얼마인가요?
USDC의 소수 자릿수 6자리를 적용하면 최소 단위로 1000입니다. v2 결제 조건에는 문자열 "1000"을 사용합니다.
거래 영수증이 성공이면 USDC 수령도 확인된 것인가요?
거래 성공 상태만으로는 충분하지 않습니다. 공식 USDC 컨트랙트의 Transfer 이벤트에서 결제자·수취인·금액을 대조하세요.
같은 결제 요청을 다시 보내도 안전한가요?
동일 승인 nonce의 재사용 방지와 주문 중복 방지는 별개입니다. 재시도 전에 기존 결제 상태를 확인하고 주문별 멱등성 처리를 구현하세요.
PAYMENT-RESPONSE가 없으면 다시 결제하면 되나요?
응답 유실이나 시간 초과라면 정산 결과가 불명확할 수 있습니다. 새 승인부터 만들지 말고 기존 요청의 정산 상태를 확인하세요.
x402는 특정 회사의 facilitator만 사용해야 하나요?
공식 구조는 외부 facilitator 이용과 자체 운영을 모두 허용합니다. 선택한 구현이 필요한 프로토콜 버전·결제 방식·체인을 지원하는지 확인하세요.
AI 에이전트의 지출도 자동으로 제한되나요?
이 예제는 요청별 결제 조건만 제한합니다. 반복 호출에 따른 누적 지출은 별도의 예산 정책으로 관리하세요.
Cloudflare PAYMENT-CONTEXT는 표준 결제 서명 헤더인가요?
PAYMENT-CONTEXT는 Gateway가 원본 서버에 전달하는 JWT입니다. 클라이언트의 PAYMENT-SIGNATURE와 용도가 다릅니다.
예제를 바로 메인넷에 배포해도 되나요?
이 코드는 테스트용 최소 구성입니다. 운영 배포 전 의존성 보안 경고, 중복 요청 처리, facilitator 인증과 실제 정산을 검증하세요.
Sources
- x402 Protocol Specification v2
- x402 HTTP Transport v1
- x402 HTTP Transport v2
- x402 Migration Guide: V1 to V2
- x402 Facilitator 역할과 책임
- x402 Quickstart for Sellers
- x402 Quickstart for Buyers
- x402 공식 TypeScript fetch 클라이언트 예제
- x402 Express 미들웨어 구현
- x402Client 결제 조건 선택과 정책 구현
- x402 ExactEvmScheme 서버 가격 변환 구현
- Circle USDC 컨트랙트 주소
- Base 메인넷과 Base Sepolia 네트워크 정보
- Circle Testnet Faucet
- viem getTransactionReceipt
- viem parseEventLogs
- ERC-20 Token Standard
- ERC-3009 Transfer With Authorization
- x402 Payment-Identifier 멱등성 확장
- Cloudflare Monetization Gateway x402 protocol
- Cloudflare Monetization Gateway Payment validation
- x402 Networks & Token Support
- GitHub 보안 권고: Zod의 대용량 컬렉션 검증에 따른 메모리 고갈
Images


