x402 Protocol — Episode 2: From Zero to First On-Chain Payment in 8 Hours

The pivot was decided. Korean crypto data on x402. Now I actually had to build the thing.

Here's what I started with: an Oracle ARM server in Tokyo (free tier — 4 CPUs, 24GB RAM, $0/month), a domain I already owned, and about a day to get something working. No budget. No team. Just Claude and a lot of terminal windows.

What This Post Covers

The entire build process for KR Crypto Intelligence — from checking whether Korean exchange APIs even work from a Tokyo server, to Cloudflare SSL setup, to FastAPI with x402 payment middleware, to seeing $0.001 USDC land in my wallet from the first on-chain payment. Eight hours, start to finish. Everything ran on free infrastructure.

Testing the Data Sources First

Before writing any code, I needed to know one thing: would Upbit and Bithumb respond to requests from a server in Tokyo? Korean exchanges sometimes block non-Korean IPs. If that happened, the entire project was dead before it started.

# The three API calls that decided whether this project lives or dies curl -s "https://api.upbit.com/v1/ticker?markets=KRW-BTC" curl -s "https://api.bithumb.com/public/ticker/BTC_KRW" curl -s "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT"

All three responded. Upbit returned BTC at ₩101,449,000. Bithumb at ₩101,480,000. Binance at $66,832.09. No IP blocks. No authentication required.

That was the green light.

Cloudflare + Oracle Firewall

x402 buyer agents need HTTPS. My Oracle server doesn't have SSL. The fix: put Cloudflare in front — free plan, automatic SSL.

DNS setup: pointed api.printmoneylab.com to the Oracle server's IP with Cloudflare's proxy enabled (the orange cloud icon). The blog's main domain stayed on DNS-only mode pointing to Google's Blogspot servers. One domain, two purposes, no conflicts.

One setting that'll ruin your day if you get it wrong: SSL mode must be "Flexible", not "Full." Full mode means Cloudflare expects SSL on the origin server too. Mine doesn't have it. Full mode = instant 525 error on every request. Flexible means Cloudflare handles SSL on the front, talks plain HTTP to the origin. Took me 20 minutes to figure out why everything was timing out before I remembered this.

Then the Oracle firewall. Oracle's default security blocks everything except SSH. You need to open ports 80 and 443 in two places — the VCN Security List in Oracle Cloud Console, and iptables on the server itself.

Here's the trap I almost fell into again: Oracle's iptables has a REJECT rule near the bottom. If you add ACCEPT rules after it with -A (append), they go below the REJECT and get ignored. You need -I INPUT 5 (insert) to put them above it.

# Wrong — gets ignored because REJECT is above it sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT # Right — inserted before the REJECT rule sudo iptables -I INPUT 5 -p tcp --dport 80 -j ACCEPT sudo iptables -I INPUT 6 -p tcp --dport 443 -j ACCEPT sudo netfilter-persistent save
What this does in plain English: The firewall checks rules top to bottom. If "reject everything" comes before "accept port 80," your port never opens. You have to slide the accept rule in above the reject. I hit this exact same problem setting up ACP agents — caught it faster this time.

One more thing: Cloudflare's Flexible SSL only forwards to port 80 on the origin. I initially set my API to run on port 8402 (because, you know, x402). Didn't work. Changed to port 80, which requires root privileges, so the systemd service runs as root. Not elegant, but it works.

Building the API

FastAPI for the framework. Five paid endpoints, three free ones. The paid ones:

/api/v1/kimchi-premium — Compares Upbit KRW price vs Binance USDT price using the live USD/KRW exchange rate. Returns the premium percentage and direction.
/api/v1/kr-prices — Upbit and Bithumb prices in KRW with 24-hour volume and change.
/api/v1/fx-rate — Live USD/KRW exchange rate.
/api/v1/stablecoin-premium — USDT/USDC price on Upbit vs official exchange rate. Shows whether capital is flowing into or out of the Korean crypto market.
All at $0.001 USDC per call.

The code has a bunch of safety measures I won't bore you with in detail, but a few are worth mentioning because they caused bugs when I didn't have them:

Bithumb returns HTTP 200 even when something's wrong — the actual error code is buried inside the JSON body (status "5300" and similar). If you just check the HTTP status, you'll think everything's fine while serving garbage data. I check body["status"] != "0000" now.

The exchange rate API (exchangerate-api.com) occasionally fails. My fallback: estimate USD/KRW by comparing BTC's KRW price on Upbit with its USDT price on Binance. But that calculation itself needs an exchange rate, which creates a loop. I added an estimated_from_crypto flag that breaks the cycle. Sounds niche, but it saved the API from hanging during an actual outage.

And because Cloudflare proxies all requests, every client looks like the same IP. Rate limiting by IP was useless until I switched to reading the CF-Connecting-IP header, which contains the real client IP.

Adding x402 Payments

This was the part I'd been waiting for. And honestly? It was easier than I expected.

pip install x402[fastapi,evm]

The middleware wraps around your existing FastAPI routes. You define which endpoints are paid, set the price, specify your wallet address, and point to a facilitator (I use xpay — they handle the gas fees):

from x402.http.middleware.fastapi import PaymentMiddlewareASGI x402_routes = { "GET /api/v1/kimchi-premium": RouteConfig( accepts=[PaymentOption( scheme="exact", price="$0.001", network="eip155:8453", # Base pay_to=WALLET_ADDRESS )] ), # ... same for other paid endpoints } app.add_middleware(PaymentMiddlewareASGI, routes=x402_routes, server=x402_server)
What this does in plain English: Any request to a paid endpoint without a payment gets a "402 Payment Required" response. The buyer agent sees the price, pays $0.001 USDC automatically, and the data comes back. The facilitator (xpay) handles the blockchain transaction and covers gas fees. You just receive USDC in your wallet.

I opened a browser and hit https://api.printmoneylab.com/api/v1/kimchi-premium. The screen showed "402 Payment Required" with a wallet selection popup. That was the moment it clicked — the API was live, paywalled, and waiting for its first customer.

I also set up a Telegram bot for monitoring. Every minute, it sends a summary: how many requests came in, which endpoints were hit, how many succeeded or failed. Filtered to only track /api/v1/ paths because without that filter, bot scanners hitting /.env and /wp-admin were triggering alerts every few seconds.

Telegram bot notification showing KR Crypto API server start and kimchi-premium API request received

The First $0.001

Time to be my own first customer. I ran a buyer script on my MacBook using a wallet that had some USDC on Base:

r = await httpx_client.get( "https://api.printmoneylab.com/api/v1/kimchi-premium?symbol=BTC" )

Response:

{ "symbol": "BTC", "upbit_krw": 102395000.0, "binance_usdt": 67677.98, "fx_rate": 1509.52, "premium_percent": 0.23, "premium_direction": "positive" }

BTC was trading at a 0.23% premium on Upbit versus Binance. Real data, real calculation, real API response.

I checked Basescan. There it was: $0.001 USDC, transferred from the buyer wallet to mine, on Base mainnet. The first on-chain payment for Korean crypto data on x402.

Basescan transaction detail showing first x402 payment of 0.001 USDC on Base network

Server cost: $0. API cost: $0. Cloudflare: $0. Gas fees: $0 (xpay covers it). Revenue: $0.001.

Not life-changing money. But after spending months on PriceVerifier where every job earned $0.008 minus $5/month hosting, getting paid anything on infrastructure that costs nothing felt different. The math doesn't start from a hole anymore.

What's Next

The API works and payments flow. But right now nobody knows it exists. Next episode covers the distribution side — building an MCP server so Claude and ChatGPT can call this API as a tool, getting listed on awesome-x402, registering on MCP directories, and adding the stablecoin premium endpoint that came from a random market research session.

← Previous: Episode 1: Why I Pivoted to Korean Crypto Data       Next: Episode 3: MCP Server and Ecosystem Registration →


More updates on the way. If you're working on something similar or found a smarter way to do it, drop it in the comments — the more we share, the faster we all move.

Disclaimer: This blog documents my personal learning journey. Nothing here is financial advice.





Comments