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

- User request API -> เจอ return HTTP 402 พร้อมส่ง
PAYMENT-REQUIREDheader - Client ทำการ decoded Payment -> sign message -> submit
- API รับข้อมูล client -> ส่งต่อไป Facilitator (จัดการ blockchain ต่างๆ verification, settle, submit tx)
- 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
go run .และลอง curl
curl -i "http://localhost:4900/crypto/price?symbol=bitcoin&vs=usd"จะได้ response แบบนี้ (HTTP 402 Payment Required พร้อม Payment-Required encoded)
HTTP/1.1 402 Payment RequiredContent-Type: application/jsonPayment-Required: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsidXJsIjoiaHR0cDovL2xvY2FsaG9zdDo0OTAwL2NyeXB0by9wcmljZSIsImRlc2NyaXB0aW9uIjoiR2V0IGNyeXB0byBwcmljZSBmb3IgYSBzeW1ib2wiLCJtaW1lVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24ifSwiYWNjZXB0cyI6W3sic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMyIiwiYXNzZXQiOiIweDAzNkNiRDUzODQyYzU0MjY2MzRlNzkyOTU0MWVDMjMxOGYzZENGN2UiLCJhbW91bnQiOiIxMDAwIiwicGF5VG8iOiIweGJCNGYzOUQwMmQ3NDVkYjJDMzY1NkU2Qjg2MjVEMmNmM0U3Qjg3NkUiLCJtYXhUaW1lb3V0U2Vjb25kcyI6NjAsImV4dHJhIjp7Im5hbWUiOiJVU0RDIiwidmVyc2lvbiI6IjIifX1dfQ==Date: Mon, 23 Feb 2026 09:41:53 GMTContent-Length: 4ลอง decoded base64 ออกมา (ใช้ jq format output)
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 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
-
Chai Phonbopit
Senior Software Engineer ประสบการณ์กว่า 12 ปี ด้าน Frontend: React, Next.js, Tailwind CSS และ Backend: Node.js, Express, NestJS ปัจจุบันสนใจ Astro, Cloudflare Workers และ AI Coding Tool