Cloudflare Workers 무료 플랜에서 pay-per-call x402 API를 구축하는 방법 (성공 시에만 과금)
요약
본 글은 Cloudflare Workers의 무료 플랜 환경에서 pay-per-call API를 구축하는 방법을 다룹니다. x402 프로토콜을 활용하여, 서버가 HTTP 402 응답과 함께 결제 요구 정보를 보내면 클라이언트가 스테이블코인 승인을 통해 즉시 결제하고 결과를 받는 과정을 설명합니다. 이를 통해 에이전트들이 실패한 호출에 대해 비용을 지불하는 것을 방지할 수 있습니다.
핵심 포인트
- x402 프로토콜은 HTTP 402를 사용하여 결제를 요구하며, 클라이언트가 서명된 승인으로 처리합니다.
- Cloudflare Workers 무료 플랜에서도 온체인 결제 기반 API 구축이 가능함을 보여줍니다.
- 결제 검증(verification)과 정산(settlement) 과정을 분리하여 실패한 호출에 대한 비용 청구를 방지합니다.
면책 조항: 저는 이 게시물에서 다루는 API의 제작자인 ScrapeMole입니다. 아래 모든 수치는 저희 자체 테스트 보고서에서 가져온 것입니다. 아직 유료 사용자는 없습니다.
AI 에이전트는 도구를 찾는 데 능숙하지만, 그 도구에 가입하는 것에는 서툽니다. 에이전트는 OpenAPI spec을 밀리초 단위로 읽을 수 있지만, 사람의 도움 없이는 계정을 열거나, 이메일을 확인하거나, API 키를 붙여넣을 수는 없습니다. x402는 그 단계를 건너뜁니다: 서버가 가격과 함께 HTTP 402 Payment Required 응답을 보내면, 클라이언트는 헤더에 서명된 스테이블코인 승인으로 결제하고 응답을 받습니다.
ScrapeMole은 이 아이디어의 테스트입니다. 몇 가지 공개 웹 데이터 엔드포인트(YouTube 검색, 채널 비디오, 트랜스크립트, Google Trends, Amazon 상품 페이지)를 제공하며, Base에서 USDC로 호출당 $0.005~$0.01을 지불합니다. 본 게시물에서는 이것이 어떻게 작동하는지, Cloudflare의 무료 플랜에서 어떻게 실행되는지, 그리고 과정에서 겪었던 문제들을 다룹니다.
결제 흐름
결제 없이 모든 유료 엔드포인트를 호출해 보세요:
curl -i "https://scrapemole-api-mainnet.scrapemole.workers.dev/v1/youtube/search?q=espresso"
HTTP/2 402
payment-required: eyJ4NDAyVmVyc2lvbiI6Mi... (아래 JSON의 base64)
...
(생략됨: 실제 응답에는 입력 스키마와 예시 출력을 포함하는 리소스 설명 및 Bazaar discovery extension이 있습니다.)
amount는 USDC 기본 단위(소수점 6자리)로 되어 있으므로, 10000은 $0.01입니다. 클라이언트는 정확히 그 금액에 대한 EIP-3009 transferWithAuthorization을 서명하고 PAYMENT-SIGNATURE 헤더를 사용하여 재시도합니다. @x402/fetch를 사용하면 이는 fetch의 래퍼(wrapper)입니다:
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
import { privateKeyToAccount } from 'viem/accounts';
...
구매자는 가스비를 지불하지 않습니다: 촉진자(facilitator)가 온체인으로 승인을 제출합니다.
이것은 우리가 가장 중요하게 생각하는 속성이었습니다. 스크래핑 API는 호출자가 통제할 수 없는 이유로 실패합니다. 예를 들어, 비디오에 캡션이 없거나, 채널이 존재하지 않거나, 소스에서 사용량 제한(rate-limits)을 걸었기 때문입니다. 이러한 호출에 대해 비용을 부과한다면 에이전트들이 우리를 피하게 만들 것입니다.
x402 플로우는 이를 간단하게 강제할 수 있게 합니다. 서버는 어떤 작업을 수행하기 전에 페이먼트를 중개자(우리는 PayAI 사용)에게 **검증(verification)**용으로 보내고, 핸들러가 200을 반환한 후에만 **정산(settlement)**을 요청합니다. 다른 모든 상태 코드는 "charged": false를 포함하는 JSON 오류이며, 승인된 금액은 결코 정산되지 않습니다:
| 상태 | 코드 | 의미 |
|---|---|---|
| 400 | INVALID_INPUT | 누락되거나 잘못된 매개변수 |
| ... |
우리는 임시 지갑을 사용하여 Base Sepolia에서 이 과정을 끝단 간(end to end)으로 테스트했으며, 실행 전후의 온체인 USDC 잔액을 비교했습니다. 최근 실행에서는 13/13 항목을 통과했습니다: 미지급 호출 4건은 올바른 402를 받았고, 유료 호출 4건은 데이터를 반환했으며, 의도적으로 실패하도록 만든 유료 호출 4건(알 수 없는 비디오, 알 수 없는 채널, 누락된 쿼리, 불가능한 시간 범위)은 오류를 반환했고, 비활성화했던 경로가 존재하지 않는다는 것이 확인되었습니다. 구매자의 잔액은 네 번의 성공적인 호출 합계인 정확히 $0.035만큼 감소했습니다. 실패한 호출들은 비용이 $0였습니다.
만약 유사한 것을 구축한다면, 테스트에 잔액 검사를 포함시키십시오. "요금이 부과되지 않았다"는 것이 실제로 유효하다는 것을 확신할 수 있는 유일한 방법입니다.
Cloudflare Workers 무료 플랜에서 실행하기
무료 플랜은 요청당 약 10ms의 CPU(네트워크 대기 시간 제외)와 하루 10만 건의 요청을 제공합니다. YouTube 내부 API나 Google Trends처럼 JSON API를 호출하고 결과를 재구성하는 엔드포인트의 경우, 10ms로 충분했습니다. 세 가지 부분이 수정이 필요했습니다.
1. 이그레스(Egress) 리전별 봇 검사 방식 차이
데이터센터 IP는 봇 검사를 거치며, Cloudflare의 IP도 예외가 아닙니다. 우리를 놀라게 한 것은 Worker가 어디서 실행되느냐에 따라 그 정도가 크게 달라진다는 점이었습니다. Workers에서 진행한 첫 라이브 프로빙(live probe)에서 YouTube 트랜스크립트는 5번 중 1번 성공했고, Google Trends는 5번 중 0번 성공했습니다.
수정된 부분: 실행 결과가 BLOCKED로 돌아올 경우, locationHint를 사용하여 생성한 Durable Object 내부에서 재실행합니다. 이는 해당 객체와 그 외부 요청들을 다른 리전에 배치하는 방식입니다.
for (const hint of ['weur', 'eeur', 'sam']) {
const stub = env.SCRAPER.get(env.SCRAPER.idFromName(`relay-${hint}`), { locationHint: hint });
const r = await (await stub.fetch('https://relay/run', {
...
테스트 결과, 서유럽(Western Europe), 동유럽(Eastern Europe), 남미(South America)는 통과했지만 다른 지역들은 차단되었습니다. 재시도 덕분에 다음 테스트에서는 **스크립트(transcripts)**와 Google Trends 모두 10/10을 기록했습니다. YouTube 검색 및 채널 목록은 필요하지 않았습니다 (각 5/5). 이 방식은 프록시를 사용하지 않으며, 재시도는 첫 시도가 차단되었을 때만 실행되므로 일반적인 경우에는 비용이 발생하지 않습니다.
이는 보장이 아닙니다: 차단 여부는 소스(source)의 결정이며 언제든지 변경될 수 있습니다. 이것이 바로 차단된 호출은 절대 과금되면 안 되는 또 다른 이유입니다.
2. 일부 파싱 작업은 10ms 안에 처리하기 어렵습니다
cheerio를 사용하여 전체 Amazon 상품 페이지를 파싱하는 과정에서 CPU 제한을 초과했습니다. 플랜 비용을 지불하는 대신, 해당 엔드포인트는 Vercel 함수에서 실행됩니다. Worker는 이를 / 및 llms.txt에서 연결하므로 에이전트들이 여전히 이 기능을 찾을 수 있습니다.
3. 요청 간에 진행 중인 I/O를 공유하지 마세요
x402 서버 미들웨어는 초기화될 때 페실리테이터(facilitator)(GET /supported)와 동기화됩니다. 만약 이 과정이 한 요청 중에 발생하고 두 번째 요청이 동일한 Promise를 기다린다면, workerd가 거부합니다: Worker는 다른 요청이 시작한 I/O를 await 할 수 없습니다. 우리의 해결책은 I/O가 필요하지 않은 모든 것(라우트, 디스커버리 스키마)을 시작 시점에 구축하고, 첫 번째 유료 요청 내부에서 초기화하며, 성공한 후에만 캐싱하는 것입니다. 만약 페실리테이터에 접근할 수 없다면, 호출은 503을 반환합니다 (과금되지 않음). 그리고 다음 요청이 다시 시도합니다.
디스커버리: 앱 스토어는 없습니다
에이전트들은 기계가 읽을 수 있는 메타데이터를 게시했을 때만 당신을 찾습니다. 우리는 제공합니다:
openapi.json파일에 모든 유료 작업마다x-payment-info블록(가격, 네트워크, 자산) 포함/.well-known/x402경로와 각 402 응답 본문 내부에 Bazaar 디스커버리 확장 기능 추가 (입력 스키마 및 예시 출력 포함)- LLM을 위한 일반 텍스트 가이드인
llms.txt파일
한 가지 호환성 참고 사항: x402 v2는 결제 인증(payment challenge)을 오직 PAYMENT-REQUIRED 헤더에만 넣고, v1용으로 구축된 일부 인덱서는 여전히 본문에서 accepts를 읽습니다. 저희는 디코딩된 인증 정보를 본문에도 복사했으며, 그 결과 이전에 거부했던 한 인덱서가 해당 목록을 수락했습니다.
여러 x402 디렉터리(x402scan, x402dash, Agent402, 402 Index)가 계정 없이 각각 하나의 API 호출로 저희의 목록을 수락했습니다.
MCP를 통한 동일한 도구들
또한 /mcp에 원격 MCP 서버(스트리밍 가능한 HTTP, 무상태)가 있습니다. 이 서버는 x402 MCP 전송 방식을 사용합니다:
curl -s https://scrapemole-api-mainnet.scrapemole.workers.dev/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
tools/list는 무료입니다. 비용이 발생하지 않는 tools/call은 structuredContent 내의 동일한 PaymentRequired 객체와 함께 isError: true를 반환합니다. 클라이언트는 `params._meta[
지금까지는 없습니다. Workers, Durable Objects, cron 모니터링 및 웹사이트는 Cloudflare의 무료 티어에서 운영되며, Amazon은 Vercel 무료 함수를 사용합니다. PayAI의 촉진자(facilitator) 또한 정산에 대한 무료 할당량을 가지고 있으며, 구매자는 가스비를 지불하지 않습니다.
미해결 질문 (Open questions)
- 에이전트 빌더가 실제로 호출 건별로 비용을 지불할까요? Base 메인넷에서 라이브 상태이지만, 아직 아무도 비용을 지불한 적은 없습니다. API 키를 사용한 선불 크레딧 방식이 팀들에게는 여전히 더 쉬울 수 있습니다.
- 지역별 재시도(regional retry) 기능은 얼마나 오래 작동할까요? 30분마다 실행되는 cron 카나리(cron canary)로 계속 측정해 볼 것으로 예상합니다.
- 다음에는 무엇이 와야 할까요? 만약 402 뒤에 두고 싶은 소스가 있다면, 듣고 싶습니다.
문서, 가격 및 실시간 테스트 수치: scrapemole.pages.dev. API의 openapi.json과 llms.txt는 여기에 연결되어 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기