
EC 쇼핑몰을 AI 결제 대응시킨 이야기 (JPYC・x402) ── EC 편
요약
HTTP 상태 코드 402를 활용한 결제 프로토콜 x402를 JPYC 기반 EC 플랫폼에 도입한 기술 사례입니다. 기존의 복잡한 3가지 결제 경로를 통합하여 AI 에이전트와 인간 사용자가 동일한 엔드포인트를 사용할 수 있도록 설계 및 구현한 과정을 다룹니다.
핵심 포인트
- x402 프로토콜을 통한 HTTP 네이티브 결제 메커니즘 도입
- AI 에이전트와 인간 사용자를 위한 결제 경로 통합 설계
- EIP-3009 가스리스 송금을 활용한 사용자 경험 최적화
- 다중 결제 경로 병존에 따른 기술 부채 해결 과정
서론
결제 프로토콜 x402는 Coinbase가 제창하는 HTTP-native 결제 메커니즘으로, HTTP 상태 코드 402 Payment Required를 그대로 사용하여 결제를 진행합니다.
AI 에이전트의 경우 HTTP 형태로 결제할 수 있어 궁합이 좋지만, 인간의 쇼핑에도 통용될 수 있지 않을까 하는 생각이 이번에 x402를 도입하게 된 배경입니다.
x402의 사양 및 개요는 이곳에 정리되어 있습니다.
그런데 막상 기존 EC 시스템을 x402로 이행하려고 하니, "엔드포인트(Endpoint)만 교체하면 되는 거 아냐?"라고 생각했으나, 실제로는 결제 경로가 여러 개 병존하고 있거나 주문 정보를 어떻게 전달할지를 EC 측에서 다시 설계해야 하는 등... 상상 이상으로 힘들었습니다.
이 기사에서는 JPYC 기반의 다점포 EC 플랫폼을 x402로 완전 이행한 기록을 코드 레벨에서 전달합니다. 3개였던 결제 경로를 하나로 통합하여, 인간의 쇼핑도 AI 에이전트의 쇼핑도 동일한 HTTP 엔드포인트를 통하게 하기까지의 설계 판단, 시행착오, 최종 구현을 써 내려가겠습니다. x402로의 이행을 검토 중인 엔지니어나 결제 시스템을 운용하고 있는 분, AI 결제 도입을 고려하고 있는 경영자를 대상으로 합니다. 구체적인 이행 이미지를 잡으실 수 있다면 좋겠습니다!
참고로 Facilitator 측(온체인 settle을 담당하는 Cloudflare Workers) 이야기는 별도 리포지토리인 「Facilitator 편」으로 나누어져 있습니다. 이 기사는 EC 플랫폼 측의 관점에 집중하여 전달합니다.
0. 전제: 어떤 시스템인가
JPYC EC Platform은 상점이 **JPYC(일본 엔화 스테이블코인, 1 JPYC = 1 엔)**로 결제를 받을 수 있는 멀티 테넌트(Multi-tenant) SaaS입니다. EIP-3009의 가스리스(Gasless) 송금을 통해 구매자는 가스비를 내지 않고 "서명"만 하면, 플랫폼이 온체인 송금을 대행합니다.
이 "가스리스 송금을 어떻게 성립시킬 것인가" 부분이 이번 이행에서 다루는 핵심입니다.
1. 왜 이행했는가 ── 3개의 결제 경로가 병존하고 있었다
이행 전에는 결제 경로가 3개 병존하고 있었습니다. 우선 이 "병존 상태"가 어떤 것이었는지 살펴보겠습니다.
| 경로 | 플랜 | 메커니즘 | 구매자가 서명하는 타입 |
|---|---|---|---|
| A | self_collect | 고객이 서명 → 상점이 수동으로 회수(가스비는 상점 부담) | ReceiveWithAuthorization |
| B | relayer (맡기기) | 고객이 서명 → 운영측의 relayer가 자동 settle | TransferWithAuthorization |
| C | x402 (AI 에이전트용) | 에이전트가 서명 → x402 facilitator가 즉시 settle | TransferWithAuthorization |
초기 구현은 경로 A·B뿐이었습니다. admin 측에 상점의 수동 회수용 코드가, storefront 측에 주문 생성 코드와 체크아웃 화면이 있었으며, 주문은 EIP-712 서명 → 상점 회수라는 모델이었습니다.
그 후 AI 에이전트용으로 경로 C(x402)를 추가하여 3개 경로가 병존하게 되었습니다. 참고로 이때의 AI 에이전트 결제는 디지털 상품에 한정되었으며, 단품 구매만 가능했습니다.
병존이 낳은 구체적인 부채
- 테스트 경로가 3배. 회귀 테스트(Regression) 탐지 비용이 그대로 3배가 됩니다.
- 공개 API가 2개 계통. 외부로 공개할 경우 인지 부하가 높습니다.
- self_collect의 "서명 완료·미회수" 주문이 장기간 방치될 리스크. 구매자는 서명했음에도 상점이 회수할 때까지(최대 3일) 결제가 완료되지 않습니다.
- 경로 B와 C의 코드 중복. 둘 다 relayer wallet으로
transferWithAuthorization을 호출할 뿐인데 별도 구현으로 되어 있었습니다. - 공개 REST 경로에 설계 버그에 가까운 허점. 어떤 API에서는 재고를 잠금(Lock)하고 있는데, 어떤 API에서는 재고를 잠금하지 않는 등... 이는 "병존"이 숨기고 있던 사고의 근원이었다고 생각합니다.
x402라는 선택
경로 C에서 사용하던 x402(Coinbase가 제창하는 HTTP-native 결제 프로토콜, v2)를 "정(正)"으로 삼아 이 시스템에 도입하기로 했습니다. 이유는 단순합니다. x402는
HTTP 402 Payment Required를 실제로 사용하며, 헤더(Header)를 통해 결제 요청과 서명(Signature)을
주고받는다
는 점에서, AI 에이전트뿐만 아니라 인간에게도 동일한 형태로 통용되는 결제 방식이라고 생각했기 때문입니다. "인간용"과 "AI용"을 나눌 필요가 프로토콜 레벨에서 사라집니다.
참고로 조금 보충하자면, x402는 Google이 2025년 9월에 발표한 AI 에이전트 결제 프로토콜 AP2 (Agent Payments Protocol)의 공식 확장으로도 포함되어 있으며, AP2의 스테이블코인 (Stablecoin) 결제 레인을 담당하는 위치에 있습니다 (AP2 문서). Coinbase, Mastercard, MetaMask, Ethereum Foundation 등 60개 이상의 조직이 AP2에 참여하고 있는 가운데, x402가 암호자산 결제의 표준적인 선택지로 채택된 형국입니다. 이번 이행은 사내 사정으로 시작한 것이었지만, 외부 세계에서도 이 방향으로 통일되어 가고 있구나 하는 생각에 조금 힘을 얻은 기분이었습니다.
여기서 한 가지, 이행을 통해 몇 번이고 마주하게 되는 **개념의 분리 (Separation of Concerns)**를 먼저 적어두겠습니다.
x402는 "결제 프로토콜"만을 규정합니다. "주문 정보를 운반하는 HTTP 인터페이스"는 EC 측의 사양입니다.
x402가 정하는 것은 "402를 반환한다", "PAYMENT-REQUIRED 헤더에 결제 요청을 base64url로 실는다", "PAYMENT-SIGNATURE 헤더로 서명을 받는다"와 같은 **결제의 악수 (Handshake)**뿐입니다. 반면 "장바구니에 무엇이 들어있는가", "배송비는 얼마인가", "선물 포장(노시)을 할 것인가", "선물의 수령인은 누구인가"와 같은 주문의 내용은 x402의 관할 밖이며, 완전히 EC 측에서 결정합니다.
이 분리를 간과하면 "x402로 이행한다 = 엔드포인트 (Endpoint)를 교체하기만 하면 된다"라고 오해하게 됩니다. 실제로는 레거시 REST가 가지고 있던 주문 정보의 표현력을 x402 엔드포인트로 **이식 (Porting)**하는 작업이 핵심이었습니다 (§3에서 상세히 다룹니다).
2. 이행의 목표
EC 운영 측면에서는 시스템 이행 시 가스비 (Gas fee)가 운영 부담이 되므로 부하가 큽니다. 하지만 점주님께 지금까지 1%였던 수수료를 2%로 올리겠다고는 말하고 싶지 않았습니다. 그래서 일률적으로 가스비를 포함하여 1%로 설정했습니다.
| 지표 | 이행 전 | 이행 후 |
|---|---|---|
| 숍이 선택하는 플랜 | 2종류 (1% / 2%) | 1종류 (1%) |
| ... | 즉시 (서명으로부터 1회 왕복) |
"인간도 AI도 POST /api/v1/checkout 하나로", "플랜은 하나로", "주문은 서명하는 순간 확정". 이전보다 점주님과 운영 측 모두에게 심플하게 만드는 것이 목표였습니다.
3. 4단계 단계적 이행
이행이라고 하면 무심코 한꺼번에 전환하고 싶어집니다. 하지만 그것은 상당히 위험했습니다.
지금까지의 주문(서명 완료·미회수)을 안고 있는 상태에서 경로를 일제히 전환하면, "서명했는데 결제가 완료되지 않는" 고객이 발생합니다. 롤백 (Rollback)도 어렵습니다. 그래서 **4단계 (4 Phases)**로 나누었습니다. 각 단계는 독립된 커밋 (Commit)으로 구성되어 있으며, 각각 단독으로 배포 및 검증할 수 있도록 했습니다. 그럼 4개의 단계를 차례대로 살펴보겠습니다.
Phase A x402 엔드포인트의 기능 확장
Phase B 인간 체크아웃을 x402화
Phase C 플랜 통합 (1% 일률)
...
Phase A ── x402 엔드포인트에 "장바구니의 표현력"을 이식하기
이행 전의 x402 경로는 단품만 구매할 수 있었습니다. URL에 상품 ID가 포함되어 있기 때문에 구조적으로 장바구니를 표현할 수 없었습니다.
- 다수 상품 장바구니
- NFT 할인
- 주문 옵션 (선물 포장·도착 시간·메시지 카드)
- 증정 정보 (선물 플래그 및 수령지)
- 배송비 계산 (무료 배송 임계값 및 구매 시 무료 배송이 되는 대상 상품 판정)
- 고객 이메일 주소 필수화
설계 판단: 단품 엔드포인트를 "신설하지 않는다"
x402화에 따라 "단품용 /products/:id/checkout과 다수 상품용 /checkout
「둘 다 갖는다」는 안도 있었으나, 심플 이즈 베스트(Simple is best)를 위해 통일하기로 했습니다. shop_id를 URL이 아닌 body로 받고, items[]로 장바구니를 표현합니다.
2단계 플로우와 「예약 스냅샷 (Reservation Snapshot)"
x402의 플로우는 HTTP 2회 왕복(Round trip)이 됩니다. EC 구현에서의 구체적인 내용은 다음과 같습니다:
1회차: POST /api/v1/checkout (PAYMENT-SIGNATURE 헤더 없음)
body: { shop_id, items[], customer_email, shipping?, ... }
↓ 서버가 재고를 5분간 「가예약」, 금액을 확정
...
여기서 핵심은 **「2회차의 body는 reservation_id만 포함한다」**는 설계입니다.
1회차에서 받은 주문 내용은 X402CheckoutReservationSnapshot이라는 **서버 측 스냅샷 (in-memory, TTL 5분)**에 고정됩니다. 2회차에서는 이 스냅샷을 참조하여 정산(settle)합니다. items도 discount도 shipping도 다시 전송하지 않습니다.
왜 그렇게 하는가?
서명 시 구매자가 승낙한 금액과 DB에 기록되는 금액이 구조적으로 일치함을 보장하기 위해서입니다.
만약 2회차에서 items를 다시 전송하게 하면, 「1회차에서 5,000엔이라고 해서 서명했는데, 2회차에서 3,000엔 분량의 items를 보낸다」와 같은 변조의 여지가 생겨버립니다. reservation_id만 사용하면 구매자가 서명한 대상은 스냅샷 그 자체이므로, 내용을 바꿔치기할 수 없습니다.
구현상의 안전장치는 checkPayloadAgainstReservation()입니다. 서명된 PaymentPayload와 스냅샷을 대조하여, amount / payTo / network / authorization.value 중 어느 하나라도 불일치하면 payload_mismatch로 거부합니다.
// 이행 완료 시점의 실제 코드
export function checkPayloadAgainstReservation(
payload: PaymentPayload,
...
레이어 분리 ── 장바구니 계산은 「순수 함수 (Pure Function)」로 격리
Phase A에서 신설한 컴포넌트군:
| 컴포넌트 | 역할 |
|---|---|
| HTTP 입구 | 요청 검증 및 분기만 수행 |
| ... | 장바구니 계산 순수 함수 (재고·variant·할인·옵션·배송비) |
| 예약 스냅샷 lib | 스냅샷 유지 (in-memory store) |
| 검증 (Validation) | 요청 body의 Zod 스키마 |
이 프로젝트는 「Loaders/Actions → Usecases → Lib → Repositories」라는 단방향 레이어 아키텍처를 채택하고 있습니다. Lib는 부작용(Side effect)이 없는 순수 함수이며, 테스트를 필수로 합니다.
장바구니 계산, 즉 「이 상품 구성·이 배송지·이 할인·이 옵션일 때 소계 얼마·배송비 얼마·합계 얼마」를 계산하는 처리를 computeCart()라는 순수 함수로 격리한 것은 이 방침을 따랐기 때문입니다. DB 액세스도 네트워크도 가지 않으므로, 입력과 출력만으로 테스트할 수 있습니다.
실패는 모두 타입이 지정된 에러(Typed error)로 표현합니다:
export type CartComputationError =
| { code: "product_missing"; productId: string }
| { code: "insufficient_stock"; productId: string; ... }
...
구 엔드포인트는 「Deprecation(폐지 예정)이 포함된 proxy」로 남겨둠
POST /api/v1/products/:id/checkout은 외부의 SKILL(후술함)이 0.1.0 버전에서 호출한다는 전제였습니다. 갑자기 삭제하면 하위 호환성(Backward compatibility)을 깨뜨리게 됩니다. 따라서 내부에서 /api/v1/checkout을 호출하도록 만든 뒤, RFC 8594의 폐지 예고 헤더를 모든 응답에 추가했습니다:
const DEPRECATION_HEADERS = {
Deprecation: "true",
Sunset: SUNSET_DATE,
...
"즉시 삭제"하는 것이 아니라 "예고하고, 이행 기간을 두고, 삭제"하는 방식입니다.
Phase B ── 인간의 체크아웃도 x402를 통하게 하기
Phase A에서 AI 에이전트(AI Agent)를 위한 엔드포인트(Endpoint)는 정비되었습니다. Phase B에서는 인간의 체크아웃 화면을 동일한 POST /api/v1/checkout으로 통하게 합니다.
인간용 체크아웃 화면은 이미 wagmi를 통한 서명(Signature)・MetaMask 모바일 복구(sessionStorage)・체인(Chain) 선택・잔액 체크・NFT 할인 체크가 구현되어 있었습니다. 이것들은 보존하고, "서명 후의 통신 대상"만 교체하는 방침입니다:
기존: createOrderAction → signTypedDataAsync → submitSignatureAction(orderId, signature, address)
신규: POST /api/v1/checkout (1회차) → signTypedDataAsync → POST /api/v1/checkout (2회차)
신설된 x402 체크아웃 클라이언트(Checkout Client)가 인간 UI 측으로부터 x402의 2단계 플로우(Flow)를 호출하는 역할을 담당합니다(startX402Reservation() / settleX402Checkout()). EIP-712의 typed-data 빌더(Builder)도 별도로 준비했습니다.
plan_type 분기의 소멸
이행 전, 체크아웃 화면에는 plan_type에 따른 분기가 8곳 이상 있었습니다. 가장 본질적인 것이 서명하는 타입(Type)의 전환입니다:
// 이행 전
planType === "self_collect" → ReceiveWithAuthorization 서명
planType === "relayer" → TransferWithAuthorization 서명
x402에서는 facilitator(의 relayer wallet)가 msg.sender가 되어 transferWithAuthorization을 호출합니다. 따라서 구매자가 서명하는 것은 항상 TransferWithAuthorization입니다. ReceiveWithAuthorization 경로는 사라집니다.
이행 완료 시점의 typed-data 빌더를 확인하면, TransferWithAuthorization의 필드(Field) 정의만 가지고 있습니다:
const FIELDS = [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
...
ReceiveWithAuthorization은 실제 코드에서 사라졌습니다.
"서명 유효 기간"이 의미를 잃음
기존 플로우에서는 숍 설정의 서명 유효 기간(기본 3일)이 중요했습니다. 서명으로부터 회수(Collection)까지 유예가 필요하기 때문입니다. x402에서는 서명한 순간에 settle(결제 완료)되므로, 이 값은 사용자 경험(UX)상의 의미를 잃습니다. 대신 x402의 maxTimeoutSeconds(기본 90초)를 사용합니다.
Phase C ── 플랜을 하나로 통합하기
settle가 운영 대행의 한 경로가 된 이상, "숍이 직접 회수하는 self_collect 플랜"은 존재 의의가 없습니다. 플랜을 relayer 1종・수수료 1% 일괄로 통합했습니다.
설계상, plan_type 열거형(Enum)과 fee_rate 컬럼(Column) 자체는 남겨두었습니다. 과거의 주문 및 과거의 수수료 계산 이력이 이것들을 참조하고 있기 때문이며, 삭제하면 이력이 깨지게 됩니다. 수행한 것은 "신규 입력 경로를 전부 relayer / 0.01로 고정하는 것"입니다.
Phase D ── 구 코드를 물리적으로 삭제하기
펜딩(Pending) 상태인 주문이 소화되기를 기다렸다가, 구 코드를 물리적으로 삭제했습니다. 차분이 -923행으로, 이행을 통해 "압축했다"는 느낌이 가장 잘 드러나는 숫자입니다.
삭제한 것:
- 기존 주문 생성 유스케이스(Usecase)
createOrderAction/submitSignatureAction
Server Action의
POST /api/v1/orders
구현 (GET = 주문 이력은 유지)
POST /api/v1/orders/:id/signature
루트(전체) - relayer를 트리거하는 헬퍼
- 내부용 relayer 실행 엔드포인트
남겨둔 것도 있습니다. 구독·플랫폼 지원의 3가지 relayer 큐는, x402 facilitator를 경유하지 않고 EC 내부의 admin이 relayer wallet으로 transferWithAuthorization을 직접 실행하는 설계 그대로입니다. 이것들은 "이번 이관 대상 외"라고 명시적으로 결정하고, relayer 실행 코드를 남겨두었습니다.
4. 이관 후의 아키텍처 ── @shared/x402라는 자체 툴킷
x402는 비교적 새로운 프로토콜이라, JPYC(EIP-3009 토큰)에 맞는 라이브러리가 수중에 없었습니다. 그래서 결국, shared 패키지 안에 자체적인 x402 v2 툴킷을 구현했습니다.
| 컴포넌트 | 역할 |
|---|---|
| 타입 정의 (schemas) | x402 v2의 타입(PaymentRequirements / PaymentPayload / SettlementResponse 등)을 Zod로 정의 |
| ... |
facilitator는 "교체 가능"하게 만들었다
FacilitatorClient는 baseUrl을 생성자(constructor)에서 받습니다. facilitator의 호스트는 코드에 내장하지 않습니다.
facilitator URL은 facilitatorUrlFromEnv()가 환경 변수 FACILITATOR_URL로부터 한 번만 읽습니다. 이 "교체 가능"한 설계가 나중에 효과를 발휘했습니다. 데모 숍 기능(§7)이나, facilitator 장애 시의 원인 분리에서, facilitator를 한 곳에 고정하지 않은 것이 도움이 되었습니다.
facilitator 장애는 "502"로 매핑한다
facilitator는 별도 리포지토리·별도 인프라(Cloudflare Workers)에서 동작하는 외부 의존성입니다. 네트워크 단절·타임아웃·5xx·비 JSON 응답과 같은 케이스가 발생했을 때, EC를 단순하게 작성하면 "facilitator의 응답을 Zod로 파싱하려다 예외 발생 → 500"이 되어버립니다.
FacilitatorClient는 실패를 FacilitatorError라는 타입이 지정된 예외(typed exception)로 묶습니다. 호출 측은 이를 잡아 **502 (upstream failure)**로 매핑합니다. 500(EC 자신의 버그)과는 구별하는 것입니다.
// facilitator 클라이언트(실제 코드)
// Non-2xx: the facilitator itself errored (worker exception, 5xx, 4xx).
// Surface it as a typed error so callers can map it to a 502 instead of
...
"외부 의존성의 실패"와 "자신의 버그"를 HTTP 상태 코드로 분리할 수 있도록 해두는 것. 이것은 훗날, 실제 facilitator 장애(§5) 발생 시 원인 분리의 결정적인 열쇠가 되었습니다.
unexpected_settle_error의 분리
- 고전했던 이야기 ── 이관 후, staging에서 x402 checkout이
500을 반환하는 현상이 발생했습니다. 에러 코드는unexpected_settle_error였습니다. EC·facilitator·SKILL의 3자(각각 별도 리포지토리)가 협력하여 원인을 분리하게 되었습니다.
처음에는 facilitator 측으로부터 "EC checkout의 500은 EC 측의 문제"라는 말을 들었습니다. 하지만 §4의 "facilitator 장애는 502"라는 설계 덕분에, EC가 반환하고 있었던 것은 구조화된 502였으며, EC의 코드는 올바르게 동작하고 있었습니다. facilitator 측이 당시 문제였다는 것을 알 수 있었기에, 문제 분리가 가능하여 조사를 용이하게 진행할 수 있었습니다.
결국 원인은, facilitator의 relayer 월렛 가스(gas) 고갈이었습니다. 0엔이라 하더라도 온체인(on-chain)의 transferWithAuthorization 트랜잭션은 실행됩니다. 그 가스를 지불할 relayer 월렛의 잔액이 단 1 트랜잭션분에도 미치지 못했던 것입니다. 노드가 「funds for gas」로 전송을 거부했고, facilitator가 이를 unexpected_settle_error라는 범용 에러로 뭉뚱그려 처리했기 때문에, 원인 파악(切り分け)이 돌아가게 되었습니다.
6. 시행착오 이야기 · 그 두 번째 ── 이행 과정에서 「알림 메일」을 놓치다
지금까지 알림 메일은 다음과 같이 진행해 왔습니다.
- 숍 대상 알림(서명 시) = 「회수해 주세요」
- 고객 대상 주문 접수 알림(서명 시) = 「주문을 접수했습니다」
- 고객 대상 결제 완료 알림(회수 완료 시) = 「결제 완료」
하지만 테스트 중 메일이 도착하지 않는 일이 발생했습니다. 원인은 「구형 코드 제거」를 일괄적으로 진행할 때, 그 구형 코드에 새로운 플로우(flow)로 옮겨야 할 부분이 섞여 있었다는 점을 간과했기 때문입니다...
해결 방법 ── x402의 플로우 특성에 맞춰 이식하기
알림 블록을 그대로 새로운 usecase에 복사하는 것이 아니라, x402의 플로우 특성에 맞춰 다시 조정했습니다. 구형 플로우는 「서명(status=2 (서명 완료))」과 「회수(status=3 (결제 완료))」가 별개의 단계로 나뉘어 있어, 위와 같이 알림 메일이 3개로 분리되어 있었습니다. 하지만 x402에서는 서명과 settle이 일체형이며, 주문은 즉시 status=3이 됩니다. 「회수」라는 단계 자체가 존재하지 않습니다. 그대로 이식하면 숍에 「회수해 주세요」라는, x402에서는 의미가 없는 메일이 발송됩니다.
그래서 다음과 같이 대응했습니다:
- 숍 대상: 숍 대상 알림 이벤트 자체는 재사용하지만, 메일 본문의 「관리 화면에서 서명을 회수해 주세요」를 「결제가 완료되었습니다. 회수 등의 조작은 필요하지 않습니다. 상품 발송을 부탁드립니다」로 변경했습니다.
- 고객 대상: 구형 플로우의 「주문 접수」 + 「결제 완료」 2통을, x402에서는 결제 완료 알림(트랜잭션 해시 포함) 1통으로 집약했습니다. 서명과 결제가 동시에 이루어지는 이상, 「접수되었습니다, 나중에 결제하겠습니다」라는 중간 상태의 메일은 거짓말이 되기 때문입니다.
이식 위치는 settle usecase의 DB 트랜잭션이 커밋(commit)된 후입니다. 알림은 try/catch로 개별적으로 감싸서, fire-and-forget(실패하더라도 주문 확정은 취소하지 않음) 방식으로 처리했습니다.
9. 이행 후의 최종 형태
이행 완료 시점의 코드는 다음과 같습니다:
주문 · 결제 경로
- 구매는
POST /api/v1/checkout단 하나입니다. 사람 UI든 AI 에이전트든, 전용 클라이언트를 통하느냐 직접 HTTP를 통하느냐의 차이는 있지만, 동일한 엔드포인트와 동일한 2단계 플로우를 거칩니다. - 단품
POST /api/v1/products/:id/checkout은 Deprecation / Sunset / Link 헤더가 포함된 proxy로서 잔존하고 있습니다(하위 호환 기간) → 현재는 삭제됨. POST /api/v1/orders는 삭제되었습니다(GET = 주문 이력만 잔존).POST /orders/:id/signature도 삭제되었습니다.
서명
- 구매자가 서명하는 것은 항상
TransferWithAuthorization입니다. ReceiveWithAuthorization은 실제 코드에서 소멸되었습니다(주석의 설명문만 남습니다).- nonce는 32 byte 랜덤이며,
validBefore는 x402의maxTimeoutSeconds(기본 90초)를 기반으로 합니다.
주문 상태
1: 미서명 (구형 플로우의 흔적, 신규 플로우에서는 생성되지 않음)3: 결제 완료 (x402 settle 완료. 신규 플로우의 주문은 처음부터 이 상태임)9: 만료
플랜
- 모든 active 숍은
relayer플랜이며 수수료는 1%입니다.plan_type/fee_rate컬럼은 과거 이력 참조를 위해 남겨두었습니다.
쓰기 원자성 (Atomicity)
- settle 성공 시,
orders
+order_items
- 재고 감소 (decrement) +
x402 결제 감사 행(audit row)을 **단일 DB 트랜잭션 (Single DB Transaction)**으로 작성합니다.
facilitator와의 관계
- EC는 facilitator의
/verify
/settle
을 FacilitatorClient를 통해 호출할 뿐입니다. facilitator URL은 환경 변수로 교체 가능합니다.
- facilitator의 실패는
FacilitatorError
→ HTTP 502로 매핑됩니다.
10. 요약 ── 이번 이전을 통해 남기고 싶은 지견
이번에는 3가지 결제 경로가 병존하던 EC 플랫폼을 x402라는 하나의 프로토콜로 완전히 이전하기까지의 과정을 작성해 보았습니다.
개인적으로 고안한 포인트는 「결제 프로토콜 (Payment Protocol)」과 「주문 정보의 HTTP 인터페이스 (HTTP Interface)」를 분리하여 생각한 점입니다. x402가 규정하는 것은 전자이며, 후자(장바구니·배송비·옵션·선물)는 EC 사양입니다. 이전의 핵심은 레거시 REST가 가지고 있던 후자의 표현력을 x402 엔드포인트(Endpoint)로 이식하는 작업이었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기