이미지 출처: coinbase/x402
"AI에게 USDC를 던져주고 답을 사는 시대"라는 말이 농담이 아니게 되었다. Coinbase가 2025년 발표한 결제 표준
x402는 HTTP 402 상태 코드(Payment Required)를 부활시켜, API 호출 한 번 단위로 스테이블코인 결제를 강제할 수 있게 만들었다. 이 글에서는 SpoonOS 에이전트가 x402로 보호되는 API를 호출해 자동으로 USDC를 결제하고 응답을 받아오는 작은 프로토타입을 직접 만들어 본다.
Stripe·구독·API 키 발급 절차는 사람을 위한 모델이다. 사람이 사이트에 접속해 신용카드를 등록하고, 매달 일정 금액을 내고, 발급받은 키를 코드에 붙여넣는 흐름이다.
하지만 AI 에이전트가 자율적으로 외부 API를 부르기 시작하면 이 모델은 어색해진다.
여기서 등장한 발상이 "호출당 마이크로페이먼트" 다. HTTP 요청을 보내면 서버가 "돈이 필요하다(402 Payment Required)"고 답하고, 클라이언트가 USDC 트랜잭션을 첨부해 다시 요청을 보내면 서버가 결제를 검증한 뒤 응답을 돌려주는 방식이다.
x402는 Coinbase가 표준화한 규약이고, SpoonOS는 이 규약을 도구 레이어에서 1급으로 지원한다. 둘을 결합하면 다음과 같은 그림이 가능해진다.
[SpoonOS 에이전트] ──HTTP 요청──> [x402 보호 API]
│
<──402 Payment Required──┘
──USDC payload + 재요청──> [x402 보호 API]
│
<──200 OK + 응답 본문──────┘
이 글의 목표는 위 흐름을 실제로 한 번 굴려보는 것이다.
pip install spoon-ai-sdk)이번 데모에서는 메인넷 USDC가 아닌 Base Sepolia 테스트넷 USDC를 사용한다. 진짜 돈을 쓰지 않고 끝까지 흐름을 따라갈 수 있다.
먼저 "돈을 받고 답하는 API"를 만들어야 한다. 가장 단순한 시나리오는 다음과 같다. 사용자가 도시 이름을 보내면 "오늘의 추천 점심"을 답해주는 API. 호출 한 번에 0.01 USDC를 받는다.
server.py를 만든다.
# server.py
from fastapi import FastAPI, Request, HTTPException
from x402.fastapi import X402Middleware
app = FastAPI()
app.add_middleware(
X402Middleware,
pay_to="0xYOUR_RECEIVING_WALLET_ADDRESS",
network="base-sepolia",
asset="USDC",
price="0.01",
facilitator_url="https://x402.org/facilitator",
)
@app.get("/lunch")
def recommend_lunch(city: str = "Seoul"):
menus = {
"Seoul": "을지로 평양냉면",
"Tokyo": "츠케멘",
"Taipei": "우육면",
}
return {"city": city, "menu": menus.get(city, "포케볼")}
x402 파이썬 패키지는 설치 시점 기준 다음과 같이 사용 가능하다.
pip install fastapi uvicorn x402
x402 미들웨어가 하는 일은 단순하다. 모든 요청을 가로채서 결제 증빙(payment payload)이 첨부되어 있는지 확인하고, 없으면 402 Payment Required 응답에 결제 메타데이터를 담아 돌려준다. 결제 증빙이 첨부되어 있으면 facilitator 라는 외부 서비스에 검증을 맡긴다. facilitator는 온체인 트랜잭션을 확인해 "이 결제 진짜다"라고 응답하고, 그러면 미들웨어가 원래 핸들러로 요청을 통과시킨다.
서버를 띄운다.
uvicorn server:app --reload --port 8000
이제 평범한 클라이언트로 호출해 보자.
curl -i http://localhost:8000/lunch?city=Seoul
응답을 보면 다음과 같다.
HTTP/1.1 402 Payment Required
content-type: application/json
x-payment-required: { ... }
{"error": "payment required", ...}
서버가 무사히 결제를 요구하고 있다.
이 흐름을 손으로 풀려면 다음을 해야 한다.
X-Payment 헤더에 담아 같은 요청을 다시 보낸다다행히 SpoonOS와 x402 SDK가 이 과정을 추상화해 준다. 손수 짤 필요는 없다.
이제 클라이언트 쪽이다. SpoonOS 에이전트가 위 API를 부르면서 자동으로 결제까지 처리하게 만든다.
agent.py를 만든다.
# agent.py
import asyncio
import os
from dotenv import load_dotenv
from spoon_ai.agents import SpoonReactAI
from spoon_ai.chat import ChatBot
from spoon_ai.tools import ToolManager
from spoon_ai.tools.base import BaseTool
# x402 클라이언트 헬퍼
from x402.client import X402Client
load_dotenv()
class PaidLunchTool(BaseTool):
name: str = "lunch_recommender"
description: str = (
"특정 도시의 점심 메뉴를 추천받는다. "
"이 API는 호출당 0.01 USDC가 필요하므로, "
"사용자가 명시적으로 추천을 요청한 경우에만 호출한다. "
"입력: 도시 이름 (예: Seoul, Tokyo)"
)
def __init__(self):
super().__init__()
self._client = X402Client(
private_key=os.environ["AGENT_WALLET_PRIVATE_KEY"],
network="base-sepolia",
)
async def execute(self, city: str) -> str:
url = f"http://localhost:8000/lunch?city={city}"
response = await self._client.get(url)
if response.status_code != 200:
return f"호출 실패: {response.status_code} {response.text}"
data = response.json()
return f"{data['city']}의 추천 메뉴는 {data['menu']}입니다."
async def main() -> None:
tools = ToolManager([PaidLunchTool()])
agent = SpoonReactAI(
llm=ChatBot(
model_name="gpt-4o-mini",
llm_provider="openai",
),
available_tools=tools,
)
response = await agent.run(
"서울에서 오늘 점심 뭐 먹을지 추천받아줘. "
"필요하면 결제는 알아서 해도 좋아."
)
print(response)
if __name__ == "__main__":
asyncio.run(main())
핵심 포인트를 짚어 두자.
AGENT_WALLET_PRIVATE_KEY 는 에이전트 전용 지갑의 키다. 사람의 메인 지갑을 절대 사용하지 않는다. 테스트넷 새 계정을 발급받아 USDC를 소액 충전한 뒤 그 키만 넣는다.description에 "호출당 0.01 USDC가 필요하다"고 명시했다. 이렇게 적어두면 LLM이 무분별하게 호출하지 않는다. ReAct 에이전트가 도구 호출 전에 한 번 더 사고하게 된다.X402Client가 402 응답을 받으면 자동으로 결제 트랜잭션을 만들고 재요청을 보낸다. 그 결과 response.status_code는 결제를 거친 200으로 돌아온다..env 파일은 다음과 같다.
OPENAI_API_KEY=sk-xxxxxxxx
AGENT_WALLET_PRIVATE_KEY=0xabc... # 테스트넷 전용 키!
서버는 이미 띄워져 있다. 이제 에이전트를 실행한다.
python agent.py
콘솔에는 두 가지 흥미로운 흔적이 남는다.
서버 로그:
INFO: 127.0.0.1 - "GET /lunch?city=Seoul" 402 Payment Required
INFO: 127.0.0.1 - "GET /lunch?city=Seoul" 200 OK
에이전트 로그:
[Reasoning] 사용자가 서울 점심 추천을 요청했다. lunch_recommender 도구를 호출한다.
[Action] lunch_recommender(city="Seoul")
[Observation] Seoul의 추천 메뉴는 을지로 평양냉면입니다.
[Final Answer] 서울이라면 을지로 평양냉면을 추천드립니다. 시원하면서도 깔끔한 국물이 점심으로 잘 맞습니다.
두 줄짜리 서버 로그가 핵심이다. 첫 호출은 402로 거절됐고, 두 번째 호출에서 결제 증빙이 붙어 200으로 통과했다. 그 사이에 일어난 일은 모두 X402Client가 처리했다.
이 데모는 작동만 보면 평범한 API 호출 같다. 하지만 다음 차이를 짚어볼 만하다.
이 4가지 차이가 합쳐지면 에이전트 간(agent-to-agent) 결제가 가능해진다. 사람을 거치지 않고도 에이전트가 다른 에이전트의 서비스를 사 쓰는 시장이 열리는 것이다.
facilitator_url을 안 넣으면 검증 자체가 안 된다. 자체 facilitator를 띄울 수 있지만, 데모에서는 공개 facilitator를 쓰는 게 편하다.description에 "결제가 필요하다"고만 적으면 가끔 LLM이 호출을 피한다. "사용자가 명시적으로 요청한 경우 호출하라"라는 조건을 추가해 두는 것이 안정적이다..env 파일을 절대 깃에 올리지 않는다. 에이전트 키는 가능하면 KMS나 하드웨어 지갑에서 분리해 관리한다.이 글의 데모는 의도적으로 짧게 잡았다. 같은 패턴을 확장하면 다음 시나리오를 시도해 볼 수 있다.
BeVec 또는 외부 벡터 DB와 결합한다.마지막 시나리오가 가장 재미있다. 그동안 "AI 에이전트 경제"라는 말은 추상적이었지만, x402 + SpoonOS 조합 위에서는 적어도 결제 레이어가 구체적으로 작동한다.
이 모든 한계에도 불구하고, 사람이 개입하지 않는 결제 흐름이 코드 수십 줄로 동작한다는 사실은 그 자체로 흥미로운 시도다.
x402와 SpoonOS의 결합은 "AI 에이전트 경제"라는 추상적인 슬로건에 처음으로 코드 레벨의 답을 제시한다. 구독·API 키·신용카드를 모두 우회하고, 호출 단위로 USDC가 흐르는 구조를 직접 만져볼 수 있다는 점이 이 데모의 진짜 가치다.
테스트넷에서 0.01 USDC를 결제하는 데서 시작해, 자신만의 유료 에이전트 서비스로 확장해 보면 좋겠다. 다음 글에서는 이 흐름을 멀티 에이전트 워크플로우(SpoonGraph)로 확장해, 여러 에이전트가 서로 결제하며 협력하는 구조를 다뤄볼 예정이다.