NEW

บันทึกการลอง x402 ครั้งแรก

PublishedAt

Blockchain

Learn in Public

บันทึกการลอง x402 ครั้งแรก

วันนี้มาเขียนบันทึกเอาไว้หลังจากที่ลอง x402 เอาโค๊ดมารันทั้งฝั่ง server และฝั่ง client ซึ่งในบันทึกนี้ ตัว Server ผมใช้เป็น Go และ Client ก็เป็น TypeScript ธรรมดา เนื่องจากเป้าหมายที่จะทำคือ เอา x402 มาเป็น protocol ชำระเงิน micropayments สำหรับ Agents ฉะนั้น Client ก็เป็น program ธรรมดา ไม่ได้มี UI

หน้าเว็บเคลมว่า “Accept payments with a single line of code”

app.use(
paymentMiddleware(
{
"GET /weather": {
accepts: [...], // As many networks / schemes as you want to support
description: "Weather data", // What your endpoint does
},
},
)
);

x402 คืออะไร?

x402 - Payment Required
x402 is the internet's payment standard. An open standard for internet-native payments that empowers agentic payments at scale. Build a more free and fair internet.x402.org

x402 คือ เป็น Protocol สำหรับการชำระเงินแบบใหม่ที่หยิบยืม HTTP status code 402 มาใช้ (ซึ่งมีมานานมากแล้วตั้งแต่ยุคสร้างอินเทอร์เน็ตแต่ไม่ค่อยมีใครใช้) นำทีมโดยทีม Coinbase ออกแบบมาเพื่อ AI Agents (machine to machine)

Flow การทำงานของ x402

x402 flow

  1. User request API -> เจอ return HTTP 402 พร้อมส่ง PAYMENT-REQUIRED header
  2. Client ทำการ decoded Payment -> sign message -> submit
  3. API รับข้อมูล client -> ส่งต่อไป Facilitator (จัดการ blockchain ต่างๆ verification, settle, submit tx)
  4. API confirm -> response 200 OK

x402 Server

ทดลองใช้งาน x402 โดยการเก็บค่าบริการ $0.001 ต่อ 1 request เวลา user เรียกดูราคา crypto /cryptp/price

ตัว facilitator ใช้ของ default x402 FACILITATOR_URL=https://www.x402.org/facilitator

package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
x402 "github.com/coinbase/x402/go"
x402http "github.com/coinbase/x402/go/http"
ginmw "github.com/coinbase/x402/go/http/gin"
evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server"
ginfw "github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
const (
DefaultPort = "4900"
)
func main() {
godotenv.Load()
evmAddress := os.Getenv("EVM_PAYEE_ADDRESS")
if evmAddress == "" {
fmt.Println("❌ EVM_PAYEE_ADDRESS environment variable is required")
os.Exit(1)
}
facilitatorURL := os.Getenv("FACILITATOR_URL")
if facilitatorURL == "" {
fmt.Println("❌ FACILITATOR_URL environment variable is required")
fmt.Println(" Example: https://x402.org/facilitator")
os.Exit(1)
}
port := os.Getenv("PORT")
if port == "" {
port = DefaultPort
}
// Network configuration - Base Sepolia testnet
evmNetwork := x402.Network("eip155:84532")
coinGeckoClient := &http.Client{Timeout: 5 * time.Second}
fmt.Printf("🚀 Starting Gin x402 server...\n")
fmt.Printf(" EVM Payee address: %s\n", evmAddress)
fmt.Printf(" EVM Network: %s\n", evmNetwork)
fmt.Printf(" Facilitator: %s\n", facilitatorURL)
r := ginfw.Default()
// Create HTTP facilitator client
facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
URL: facilitatorURL,
})
/**
* Configure x402 payment middleware
*
* This middleware protects specific routes with payment requirements.
* When a client accesses a protected route without payment, they receive
* a 402 Payment Required response with payment details.
*/
routes := x402http.RoutesConfig{
"GET /crypto/price": {
Accepts: x402http.PaymentOptions{
{
Scheme: "exact",
Price: "$0.001",
Network: evmNetwork,
PayTo: evmAddress,
},
},
Description: "Get crypto price for a symbol",
MimeType: "application/json",
},
}
// Apply x402 payment middleware
r.Use(ginmw.X402Payment(ginmw.Config{
Routes: routes,
Facilitator: facilitatorClient,
Schemes: []ginmw.SchemeConfig{
{Network: evmNetwork, Server: evm.NewExactEvmScheme()},
},
Timeout: 30 * time.Second,
}))
/**
* Protected endpoint - requires $0.001 USDC payment
*
* Clients must provide a valid x402 payment to access this endpoint.
* The payment is verified and settled before the endpoint handler runs.
*/
r.GET("/crypto/price", func(c *ginfw.Context) {
symbol := strings.ToLower(strings.TrimSpace(c.DefaultQuery("symbol", "bitcoin")))
vsCurrency := strings.ToLower(strings.TrimSpace(c.DefaultQuery("vs_currency", "usd")))
if symbol == "" || vsCurrency == "" {
c.JSON(http.StatusBadRequest, ginfw.H{
"error": "symbol and vs_currency are required",
})
return
}
upstreamURL := fmt.Sprintf(
"https://api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=%s",
url.QueryEscape(symbol),
url.QueryEscape(vsCurrency),
)
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, upstreamURL, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, ginfw.H{"error": "failed to build upstream request"})
return
}
resp, err := coinGeckoClient.Do(req)
if err != nil {
c.JSON(http.StatusBadGateway, ginfw.H{
"error": "upstream request error",
"details": err.Error(),
})
return
}
var upstream map[string]map[string]float64
if err := json.NewDecoder(resp.Body).Decode(&upstream); err != nil {
c.JSON(http.StatusInternalServerError, ginfw.H{"error": "failed to decode upstream response"})
return
}
symbolData, found := upstream[symbol]
if !found {
c.JSON(http.StatusNotFound, ginfw.H{"error": "symbol not found"})
return
}
price, found := symbolData[vsCurrency]
if !found {
c.JSON(http.StatusNotFound, ginfw.H{"error": "currency not found for symbol"})
return
}
c.JSON(http.StatusOK, ginfw.H{
"symbol": symbol,
"vsCurrency": vsCurrency,
"price": price,
"source": "CoinGecko",
"timestamp": time.Now().Format(time.RFC3339),
})
})
/**
* Health check endpoint - no payment required
*
* This endpoint is not protected by x402 middleware.
*/
r.GET("/health", func(c *ginfw.Context) {
c.JSON(http.StatusOK, ginfw.H{
"status": "ok",
"version": "1.0.0",
})
})
fmt.Printf(" Server listening on http://localhost:%s\n\n", port)
if err := r.Run(":" + port); err != nil {
fmt.Printf("Error starting server: %v\n", err)
os.Exit(1)
}
}

ทดลอง start server

Terminal window
go run .

และลอง curl

Terminal window
curl -i "http://localhost:4900/crypto/price?symbol=bitcoin&vs=usd"

จะได้ response แบบนี้ (HTTP 402 Payment Required พร้อม Payment-Required encoded)

Terminal window
HTTP/1.1 402 Payment Required
Content-Type: application/json
Payment-Required: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsidXJsIjoiaHR0cDovL2xvY2FsaG9zdDo0OTAwL2NyeXB0by9wcmljZSIsImRlc2NyaXB0aW9uIjoiR2V0IGNyeXB0byBwcmljZSBmb3IgYSBzeW1ib2wiLCJtaW1lVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24ifSwiYWNjZXB0cyI6W3sic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMyIiwiYXNzZXQiOiIweDAzNkNiRDUzODQyYzU0MjY2MzRlNzkyOTU0MWVDMjMxOGYzZENGN2UiLCJhbW91bnQiOiIxMDAwIiwicGF5VG8iOiIweGJCNGYzOUQwMmQ3NDVkYjJDMzY1NkU2Qjg2MjVEMmNmM0U3Qjg3NkUiLCJtYXhUaW1lb3V0U2Vjb25kcyI6NjAsImV4dHJhIjp7Im5hbWUiOiJVU0RDIiwidmVyc2lvbiI6IjIifX1dfQ==
Date: Mon, 23 Feb 2026 09:41:53 GMT
Content-Length: 4

ลอง decoded base64 ออกมา (ใช้ jq format output)

Terminal window
echo "eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsidXJsIjoiaHR0cDovL2xvY2FsaG9zdDo0OTAwL2NyeXB0by9wcmljZSIsImRlc2NyaXB0aW9uIjoiR2V0IGNyeXB0byBwcmljZSBmb3IgYSBzeW1ib2wiLCJtaW1lVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24ifSwiYWNjZXB0cyI6W3sic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMyIiwiYXNzZXQiOiIweDAzNkNiRDUzODQyYzU0MjY2MzRlNzkyOTU0MWVDMjMxOGYzZENGN2UiLCJhbW91bnQiOiIxMDAwIiwicGF5VG8iOiIweGJCNGYzOUQwMmQ3NDVkYjJDMzY1NkU2Qjg2MjVEMmNmM0U3Qjg3NkUiLCJtYXhUaW1lb3V0U2Vjb25kcyI6NjAsImV4dHJhIjp7Im5hbWUiOiJVU0RDIiwidmVyc2lvbiI6IjIifX1dfQ==" | base64 -D | jq .
# result
{
"x402Version": 2,
"error": "Payment required",
"resource": {
"url": "http://localhost:4900/crypto/price",
"description": "Get crypto price for a symbol",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:84532",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "1000",
"payTo": "0xbB4f39D02d745db2C3656E6B8625D2cf3E7B876E",
"maxTimeoutSeconds": 60,
"extra": {
"name": "USDC",
"version": "2"
}
}
]
}

ฝั่ง client ก็จะรู้ว่า เวลาเรียก api นี้ จะต้องใช้ schema,network และ amount เท่าไหร่

หน้า UI หากเราเข้าผ่าน URL http://localhost:4900/crypto/price?symbol=bitcoin&vs=usd

x402 Payment Required

x402 Client

ฝั่ง Client ก็ง่ายๆ เลย เข้าถึง private key จากนั้น ก็ใช้ fetchWithPayment ของ @x402/fetch

import { x402Client, wrapFetchWithPayment, x402HTTPClient } from '@x402/fetch'
import { registerExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'
import 'dotenv/config'
const privateKey = process.env.PRIVATE_KEY as `0x${string}`
const baseURL = 'http://localhost:4900'
const endpointPath = '/crypto/price?symbol=bitcoin&vs=usd'
async function main(): Promise<void> {
const url = new URL(endpointPath, baseURL).toString()
const evmSigner = privateKeyToAccount(privateKey)
const client = new x402Client()
registerExactEvmScheme(client, { signer: evmSigner })
const fetchWithPayment = wrapFetchWithPayment(fetch, client)
console.log(`Making request to: ${url}\n`)
const response = await fetchWithPayment(url, { method: 'GET' })
const body = await response.json()
console.log('Response body:', body)
if (response.ok) {
const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse((name) =>
response.headers.get(name),
)
console.log('\nPayment response:', JSON.stringify(paymentResponse, null, 2))
} else {
console.log(`\nNo payment settled (response status: ${response.status})`)
}
}
main().catch((error) => {
console.error(error?.response?.data?.error ?? error)
process.exit(1)
})

ตัว Repo ด้านล่าง

❤️ Happy Coding

Authors
avatar

Chai Phonbopit

Senior Software Engineer ประสบการณ์กว่า 12 ปี ด้าน Frontend: React, Next.js, Tailwind CSS และ Backend: Node.js, Express, NestJS ปัจจุบันสนใจ Astro, Cloudflare Workers และ AI Coding Tool