x402 Payments

이 페이지

@emdash-cms/x402 패키지는 서버 렌더링되는 Astro 사이트에 x402 결제 프로토콜 지원을 추가합니다. 독립적으로 동작하는 Astro 통합이므로 결제를 강제하는 데 EmDash가 필요하지 않습니다. 사이트가 EmDash도 사용한다면 콘텐츠 필드로 항목마다 가격을 지정할 수 있습니다.

x402는 HTTP 기반의 결제 프로토콜입니다. 클라이언트가 결제 없이 유료 리소스를 요청하면 서버는 402 Payment Required와 기계가 읽을 수 있는 결제 안내를 응답합니다. x402를 이해하는 에이전트와 브라우저는 자동으로 결제를 완료하고 요청을 다시 시도할 수 있습니다.

적용 모드 선택

라우트에 대한 모든 요청이 유효한 결제를 제시해야 한다면 일반 적용 모드를 사용하세요. 이 모드는 Cloudflare 전용 요청 메타데이터에 의존하지 않습니다.

사이트가 Bot Management를 활성화한 Cloudflare Workers에서 실행되고, 점수가 낮은 요청에만 결제를 요구하려면 봇 전용 모드를 사용하세요. 패키지는 request.cf.botManagement.score를 읽으며, 이 값이 없으면 요청을 사람으로 간주하고 적용을 건너뜁니다. 봇 데이터가 없을 때 안전하게 실패(fail closed)해야 한다면 봇 전용 모드를 사용하지 마세요.

hasPayment()는 표시 전용인 세 번째 동작을 제공합니다. 요청에 결제 헤더가 있는지 알려 주지만 결제를 검증하거나 정산하지는 않습니다.

패키지 설치

사용 중인 패키지 매니저로 패키지를 설치합니다.

pnpm

pnpm add @emdash-cms/x402

npm

npm install @emdash-cms/x402

yarn

yarn add @emdash-cms/x402

통합 구성

함께 동작하는 지갑, 네트워크, facilitator를 선택하세요. 이 통합은 기본적으로 Ethereum Virtual Machine(EVM) 네트워크를 지원하지만, facilitator도 구성한 네트워크와 자산을 지원해야 합니다. 다음 예시는 Base 메인넷을 사용하고 모든 요청에 결제를 강제합니다.

import { defineConfig } from "astro/config";
import { x402 } from "@emdash-cms/x402";

export default defineConfig({
	integrations: [
		x402({
			payTo: "0xYourWalletAddress",
			network: "eip155:8453", // Base mainnet
			defaultPrice: "$0.01",
		}),
	],
});

TypeScript가 Astro.locals.x402를 인식하도록 타입 참조를 추가합니다.

/// <reference types="@emdash-cms/x402/locals" />

라우트에서 결제 강제

통합은 Astro.locals.x402에 enforcer를 배치합니다. 페이지 프런트매터에서 enforce()를 호출하면 콘텐츠를 결제 뒤에 둘 수 있습니다.

---
const { x402 } = Astro.locals;

const result = await x402.enforce(Astro.request, {
  price: "$0.05",
  description: "Premium article",
});

// If the request has no valid payment, enforce() returns a 402 Response.
// Return it directly to send payment instructions to the client.
if (result instanceof Response) return result;

// Payment verified (or skipped in botOnly mode). Apply response headers
// so the client gets settlement proof.
x402.applyHeaders(result, Astro.response);
---

<article>
  <h1>Premium content</h1>
</article>

enforce() 메서드는 다음 중 하나를 반환합니다.

  • Response(402) — 클라이언트가 결제해야 합니다. 그대로 반환하세요.
  • EnforceResult — 요청을 계속 진행해야 합니다. 콘텐츠에 대해 결제가 이루어졌거나 적용을 건너뛰었습니다(botOnly 모드의 사람).

봇 전용 모드 활성화

통합 구성에서 botOnly를 활성화합니다.

x402({
	payTo: "0xYourWalletAddress",
	network: "eip155:8453",
	defaultPrice: "$0.01",
	botOnly: true,
	botScoreThreshold: 30,
});

통합은 Cloudflare의 request.cf.botManagement.score를 읽어 요청을 분류합니다.

  • 점수가 임계값 미만(기본값 30) -> 봇으로 간주, 결제 강제
  • 점수가 임계값 이상 -> 사람으로 간주, 적용 건너뜀
  • Bot Management 데이터 없음(로컬 개발, Cloudflare가 아닌 배포) -> 사람으로 간주

EnforceResult에는 skipped 플래그가 있어 “결제할 필요가 없었던 경우”와 “결제한 경우”를 구분할 수 있습니다.

---
const result = await x402.enforce(Astro.request, { price: "$0.01" });
if (result instanceof Response) return result;

x402.applyHeaders(result, Astro.response);

// result.paid    — true if payment was verified
// result.skipped — true if enforcement was skipped (human in botOnly mode)
// result.payer   — wallet address of payer (if paid)
---

EmDash에서 가격 읽기

EmDash를 사용할 때는 페이지별 가격 책정을 위해 컬렉션에 일반 number 필드를 추가하고, 요청 시점에 이를 읽습니다.

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;
const { entry } = await getEmDashEntry("posts", slug);

if (!entry) return Astro.rewrite("/404");

const { x402 } = Astro.locals;

// Use the price from the CMS, falling back only when it is absent.
const result = await x402.enforce(Astro.request, {
  price: entry.data.price ?? "$0.01",
  description: entry.data.title,
});
if (result instanceof Response) return result;

x402.applyHeaders(result, Astro.response);
---

<article>
  <h1>{entry.data.title}</h1>
</article>

강제하지 않고 결제 헤더 확인

hasPayment()를 사용하면 요청에 결제 헤더가 포함되어 있는지 검증이나 강제 없이 확인할 수 있습니다. 안내 문구나 로그인 메시지를 바꾸는 데는 쓸 수 있지만, 보호된 콘텐츠를 열어 주는 데 사용해서는 안 됩니다.

---
const { x402 } = Astro.locals;

const hasPaymentHeader = x402.hasPayment(Astro.request);
---

{hasPaymentHeader ? (
  <p>Payment supplied. Verification is still required.</p>
) : (
  <p>This page requires payment.</p>
)}

구성 참조

옵션타입기본값설명
payTostring필수수신 지갑 주소
networkstring필수CAIP-2 네트워크 식별자(예: eip155:8453)
defaultPricePrice—기본 가격. 페이지별로 재정의 가능
facilitatorUrlstringhttps://x402.org/facilitator결제 facilitator URL
schemestring"exact"결제 스킴
maxTimeoutSecondsnumber60결제 서명의 최대 타임아웃
evmbooleantrueEthereum Virtual Machine 네트워크 지원 활성화
svmbooleanfalseSolana Virtual Machine 지원 활성화(@x402/svm 필요)
botOnlybooleanfalse봇에게만 결제 강제
botScoreThresholdnumber30봇 점수 임계값(1-99, 낮을수록 봇일 가능성이 높음)

가격 형식

가격은 여러 형식으로 지정할 수 있습니다.

  • 달러 문자열 — "$0.10"($ 접두사는 제거되고 값은 그대로 전달됩니다)
  • 숫자 문자열 — "0.10"
  • 숫자 — 0.10
  • 객체 — { amount: "100000", asset: "0x...", extra: {} }. 자산과 금액을 명시적으로 지정할 때 사용합니다

네트워크 식별자

CAIP-2는 eip155:8453(Base 메인넷)처럼 블록체인 네트워크에 모호하지 않은 식별자를 부여합니다. facilitator가 공개한 식별자를 그대로 사용하세요. 패키지는 EVM 지원이 활성화되면 eip155:* 계열을, Solana Virtual Machine(SVM) 지원이 활성화되면 solana:* 계열을 받아들입니다. 다만 facilitator가 해당 계열의 모든 네트워크를 받아들인다는 보장은 없습니다.

단일 요청 재정의

특정 페이지에 대해 구성의 기본값을 재정의합니다.

await x402.enforce(Astro.request, {
	price: "$0.25", // Override price
	payTo: "0xDifferentWallet", // Override wallet
	network: "eip155:1", // Override network
	description: "Article: How x402 Works", // Resource description
	mimeType: "text/html", // MIME type hint
});

Solana 지원 활성화

Solana는 선택 사항입니다. @x402/svm을 설치하고 구성에서 활성화합니다.

pnpm add @x402/svm

svm: true를 설정하고 facilitator가 지원하는 CAIP-2 형식의 Solana 식별자를 사용하세요. 사이트가 Solana 결제만 받는다면 EVM을 비활성화합니다. 예를 들어 @x402/svm 2.8이 사용하는 Solana 메인넷 식별자는 solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp입니다.

x402({
	payTo: "YourSolanaAddress",
	network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
	svm: true,
	evm: false, // Disable EVM if only using Solana
});

요청 흐름

  1. 페이지가 실행되기 전에 통합이 결제 enforcer를 Astro.locals.x402에 배치합니다.
  2. enforce()가 payment-signature 헤더를 확인합니다.
  3. 결제 헤더가 없으면 enforce()는 402 Payment Required 응답을 반환하며, 응답 본문과 PAYMENT-REQUIRED 헤더에 허용되는 결제 방식이 기술됩니다.
  4. 일치하는 결제가 있으면 구성된 facilitator가 이를 검증하고 정산합니다.
  5. enforce()는 결제자와 정산 결과를 반환합니다. 페이지 응답에 facilitator의 PAYMENT-RESPONSE 증빙이 포함되도록 applyHeaders()를 호출하세요.