Skip to content

feat: x402 payment protocol — complete POC with escrow, channels, dApp, and deployment - #1

Open
andreabadesso wants to merge 37 commits into
masterfrom
feat/x402-poc
Open

feat: x402 payment protocol — complete POC with escrow, channels, dApp, and deployment#1
andreabadesso wants to merge 37 commits into
masterfrom
feat/x402-poc

Conversation

@andreabadesso

@andreabadesso andreabadesso commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

x402 Payment Protocol POC for Hathor Network

Complete proof-of-concept implementation of the x402 protocol on Hathor Network, enabling pay-per-request HTTP APIs settled natively on Hathor's DAG-based L1 blockchain using nano contract escrow. Aligned with x402 V2 spec.

Live Demo

Connect your Hathor wallet via WalletConnect, fetch the weather API, approve the escrow deposit, and receive the data.


How it works

Every x402 payment creates a new nano contract instance from the X402Escrow blueprint. The client deposits funds into the contract (locked on-chain), the facilitator verifies the lock, the server delivers the resource, and the facilitator releases the funds to the seller.

Client → GET /weather → 402 Payment Required (x402Version: 2)
Client → Create escrow nano contract (deposit funds on-chain)
Client → GET /weather + PAYMENT-SIGNATURE: <base64> → Facilitator verifies → 200 + PAYMENT-RESPONSE header + data
Facilitator → release() → Funds sent to seller

For repeat clients, payment channels allow pre-funding a single contract and making multiple requests without per-request contract creation.

See the README for the full explanation of the escrow model.


x402 V2 Spec Alignment

Change Description Issues
PAYMENT-SIGNATURE header Base64-encoded payment proof (replaces X-Payment, backward compat kept) #2
PAYMENT-RESPONSE header Base64-encoded settlement result on 200 responses #3
x402Version: 2 field Added to 402 response, payment payload, verify/settle responses #4
CAIP-2 network IDs hathor:<network> used consistently via config #5
price field Renamed from amount in 402 accepts[] array #7
Settlement idempotency Escrow settlements cached by ncId to prevent duplicate release() #8
/health endpoint Reports wallet connectivity, returns 503 when degraded #12
Wallet auto-recovery Facilitator checks wallets every 30s, restarts if disconnected #12
MCP server example examples/mcp-server/ — payment-gated AI tool calls #13

What's included

Nano Contract Blueprints

Blueprint File Purpose
X402Escrow blueprint/x402_escrow.py One escrow per payment. LOCKED → RELEASED or REFUNDED.
X402Channel blueprint/x402_channel.py Pre-funded channel. Client deposits once, facilitator calls spend() per request.

Backend Services (Node.js)

Service File Port Description
Facilitator facilitator.js 8402 Verifies escrow/channel state, triggers settlement. /health endpoint with wallet auto-recovery. Idempotent escrow settlements.
Resource Server resource-server.js 3001 Paid weather API with x402 V2 middleware. Returns PAYMENT-RESPONSE header on success.
Client client.js Programmatic x402 V2 client. Sends PAYMENT-SIGNATURE header.

Frontend dApp (Next.js)

Component Description
dapp/components/X402Fetch.tsx Main x402 flow: fetch URL → 402 → choose escrow or channel → pay → receive data
dapp/components/EscrowList.tsx Track and list escrow contracts
dapp/components/EscrowDetail.tsx View escrow details, trigger refund
dapp/components/CreateEscrowForm.tsx Manual escrow creation form

Built with create-hathor-dapp. Connects via WalletConnect (Reown). Supports privatenet and testnet. Balance fetches fall back to direct fullnode query when WalletConnect relay is flaky.

MCP Server Example

File Description
examples/mcp-server/server.js Payment-gated MCP server with get_weather (paid) and get_price (free) tools
examples/mcp-server/README.md Setup guide including Claude Code integration

Deployment (Docker Compose)

File Description
docker-compose.yml 4 services: wallet-headless, facilitator, resource-server, dApp
Dockerfile Backend services image
dapp/Dockerfile Next.js production build
init-wallets.sh Auto-starts facilitator and seller wallets on deploy

Deployed on Dokploy at *.x402.hathor.dev connected to the playground testnet.


Protocol Messages (V2)

402 Response:

{
  "x402Version": 2,
  "accepts": [
    { "scheme": "hathor-escrow", "network": "hathor:testnet", "asset": "00", "price": "100", "payTo": "WQ6F...", "description": "Pay 1.00 HTR (single escrow)", "extra": { "facilitatorUrl": "...", "blueprintId": "..." } },
    { "scheme": "hathor-channel", "network": "hathor:testnet", "asset": "00", "price": "100", "description": "Pay 1.00 HTR via channel" }
  ],
  "version": "1"
}

PAYMENT-SIGNATURE request header (Base64-encoded):

{ "x402Version": 2, "scheme": "hathor-escrow", "network": "hathor:testnet", "payload": { "ncId": "000abc...", "depositTxId": "000abc...", "buyerAddress": "Wg7r..." } }

PAYMENT-RESPONSE response header (Base64-encoded):

{ "x402Version": 2, "success": true, "scheme": "hathor-escrow", "network": "hathor:testnet", "ncId": "000abc...", "settleTxId": "000def..." }

Related RFCs

All design documents are in HathorNetwork/rfcs#109:

RFC Title Description
0001 x402 Protocol Support Base protocol design — escrow blueprint, facilitator, message formats
0002 Client SDK (@hathor/x402-client) fetch() wrapper that handles 402 → pay → retry automatically
0003 Server Middleware (@hathor/x402-server) Express/Fastify/Hono middleware — 3 lines to add x402 to any API
0004 Payment Channels Pre-funded channels for repeat clients — eliminates per-request contract creation

Issues addressed

Closes #2, closes #3, closes #4, closes #5, closes #7, closes #8, closes #12, closes #13

Issues NOT addressed (external/decision tasks):

Test plan

  • Deploy to Dokploy and verify dApp completes payment flow (escrow + channel)
  • Verify PAYMENT-SIGNATURE header sent by client (Base64)
  • Verify PAYMENT-RESPONSE header present on 200 responses
  • Verify 402 response includes x402Version: 2 and price field
  • Verify /health endpoint returns wallet status
  • Verify backward compat: old X-Payment header still accepted
  • Verify channel spend() is called on every request (no over-caching)
  • Verify escrow idempotency: duplicate settle returns cached result
  • Verify wallet auto-recovery after headless restart

🤖 Generated with Claude Code

andreabadesso and others added 6 commits April 2, 2026 12:24
Implements the x402 protocol (HTTP 402 + nano contract escrow) as a
proof of concept. Includes:

- X402Escrow nano contract blueprint (Python 3.11)
- Facilitator server with /x402/verify and /x402/settle endpoints
- Resource server with x402 middleware (weather API demo)
- Client script demonstrating the full payment flow
- Dockerfile for containerized deployment

Tested end-to-end on Hathor Forge localnet: buyer deposits HTR into
escrow, resource server verifies via facilitator, serves data, then
facilitator releases funds to seller.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename `tokenUid` to `asset` in all protocol messages per x402 spec
- Resource server returns multiple payment options in `accepts` array
  (HTR + optional custom token like hUSDC)
- Facilitator verifies against array of payment requirements, matching
  the on-chain escrow state to any accepted token
- Client supports `--token htr` and `--token custom` flags to choose
  which token to pay with
- Config updated with separate HTR and custom token amount settings
- Tested end-to-end with both HTR and a custom hUSDC token

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser-based x402 payment client built with create-hathor-dapp.
Demonstrates the full x402 protocol flow through a UI:

1. User enters a URL of an x402-enabled API
2. dApp fetches it, gets 402 Payment Required
3. Shows payment requirements (amount, token, seller, facilitator)
4. User approves — wallet signs the escrow deposit tx
5. Waits for on-chain confirmation
6. Retries with X-Payment header containing the escrow proof
7. Displays the received resource data

Key components:
- X402Fetch: Main flow component handling 402 -> pay -> retry
- EscrowList/Detail: Track and manage existing escrows
- CreateEscrowForm: Manual escrow creation
- Privatenet support for hathor-forge local development
- WalletConnect (Reown) integration for real wallet signing

Also adds CORS headers to resource-server.js for browser access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explains how Hathor's x402 implementation works: one nano contract
instance per payment, instantiated from the X402Escrow blueprint.
Covers the escrow lifecycle, why not pre-signed transactions,
the full payment flow with timing, multi-token support, and the
dApp. Links to all three RFCs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New X402Channel blueprint that allows a client to deposit once and
make multiple payments without per-request on-chain transactions.

- X402Channel blueprint (blueprint/x402_channel.py): OPEN/CLOSED
  lifecycle with spend(), top_up(), and close() methods
- Facilitator updated to handle both hathor-escrow and hathor-channel
  schemes for verify and settle
- Resource server advertises both escrow and channel payment options
- dApp X402Fetch component supports both modes: escrow (one-shot) and
  channel (pre-funded, instant). Active channels persist in localStorage
  for seamless multi-request flows.

Tested end-to-end: channel with 10 HTR deposit, 2 successful spend()
calls, remaining balance 800 cents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The resource server now waits for settlement confirmation BEFORE
serving the resource. This ensures the on-chain state is updated
before the next request, preventing the race condition where
multiple channel spends could be approved against stale state.

The channel advantage is now correctly: saves the client from
creating a new escrow per request (1 on-chain tx instead of 2),
not "instant" access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andreabadesso andreabadesso self-assigned this Apr 2, 2026
@andreabadesso andreabadesso moved this from Todo to In Progress (WIP) in Hathor Network Apr 2, 2026
andreabadesso and others added 17 commits April 2, 2026 15:15
Docker Compose with 4 services:
- wallet-headless: connects to playground testnet node
- facilitator: escrow/channel verify + settle
- resource-server: example paid weather API
- dapp: Next.js x402 payment client

Blueprint IDs and addresses configured via env vars.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.39.1 doesn't exist on Docker Hub. Use latest (v0.38.0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rking

- Remove port mappings for internal services (wallet-headless, facilitator)
- Add FACILITATOR_URL env var so resource-server can reach facilitator
  via Docker service name instead of localhost
- Only expose dApp (:3000) and resource-server (:3001) to host

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each service has a healthcheck so Traefik knows when to route
traffic. Services start in order via depends_on + service_healthy:
wallet-headless → facilitator → resource-server → dapp

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without a tx-mining service, wallet-headless can't mine PoW for
transactions. Uses the playground testnet tx-mining service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- dApp default URL points to https://api.x402.hathor.dev/weather
- Resource server 402 response uses public facilitator and resource
  server URLs (not localhost)
- Add FACILITATOR_PUBLIC_URL, RESOURCE_SERVER_PUBLIC_URL, HATHOR_NETWORK
  env vars for Docker deployment
- Internal facilitator communication still uses Docker service name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add init-wallets.sh that starts facilitator and seller wallets on
the deployed wallet-headless before launching the facilitator service.
Seeds are injected via Dokploy env vars (not in git).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.38.0 (latest) has incompatible nano contract validation with
hathor-core v0.69.0. The RC version should match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The debug plugin crashes on startup and may be causing NC execution
failures. Local headless without it works fine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Dokploy server had 0.36.0 cached as 'latest' while the actual
latest on Docker Hub is 0.38.0. Add pull_policy: always.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Settlement takes 30-45s waiting for on-chain confirmation.
Traefik times out before that. Serve resource immediately after
verification, settle in background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The facilitator no longer waits for the release/spend tx to be
confirmed in a block. The tx is accepted by the node (PoW mined),
which is sufficient — it will confirm in the next block. This
avoids the 30-45s timeout that was causing 502s through Traefik.

The resource server still waits for the facilitator to respond
(sync settlement), but the facilitator responds immediately after
the tx is accepted, not after it's confirmed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andreabadesso andreabadesso changed the title feat: x402 payment protocol POC feat: x402 payment protocol — complete POC with escrow, channels, dApp, and deployment Apr 6, 2026
andreabadesso and others added 2 commits April 7, 2026 09:44
Implements issues #2-#5, #7-#8, #12-#13:

- Use PAYMENT-SIGNATURE header (Base64) with X-Payment fallback (#2)
- Add PAYMENT-RESPONSE header on 200 responses (#3)
- Add x402Version: 2 to all protocol messages (#4)
- Use config.network consistently for CAIP-2 identifiers (#5)
- Rename 'amount' to 'price' in 402 accepts array (#7)
- Add settlement cache for Payment-Identifier idempotency (#8)
- Add /health endpoint with wallet connectivity check (#12)
- Add wallet auto-recovery (periodic check + restart) (#12)
- Add MCP server POC example in examples/mcp-server/ (#13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log full error object (message, code, data, raw) on RPC failures
and log successful responses to help diagnose WalletConnect relay issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
andreabadesso and others added 6 commits April 7, 2026 11:14
The thrown error is a non-Error object with all standard properties
undefined. Log Object.keys() and JSON.stringify() to reveal its
actual contents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WalletConnect relay intermittently throws empty {} errors on
htr_getBalance. When this happens, query the fullnode's
thin_wallet/address_balance endpoint directly as a fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The RPC call was actually succeeding, but our debug log line
JSON.stringify(result) crashed on BigInt values in the response,
which became the thrown error. Use console.log directly instead.

Also fix fullnode fallback to use received-spent (not unlocked).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cache key channel:id:amount is identical for every request on
the same channel, so after the first spend() all subsequent requests
returned the cached result without actually spending. Escrow cache
(keyed by unique ncId) is correct and remains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the x402 "upto" payment scheme alongside the existing
"exact" scheme (per CDP docs). Clients authorize a maximum amount;
the server settles only the actual usage, and the remainder is
refunded to the buyer in the same settle call.

- blueprint/x402_escrow.py: add release_upto(charged_amount) method
  that withdraws charged_amount to the seller and decrements the
  escrow balance, leaving the remainder claimable via the existing
  refund() flow (no new state/phase fields needed).

- facilitator.js: add settleEscrowUpto() which calls release_upto
  followed by refund() to return the difference to the buyer.
  /x402/settle now routes on scheme=hathor-escrow-upto and keys the
  idempotency cache separately from exact settlements.

- resource-server.js: refactor middleware into an x402Middleware(routeConfig)
  factory; settlement now happens AFTER the handler runs so handlers
  can call setSettlementOverrides({ amount }) to report actual usage.
  Adds GET /generate, a simulated LLM endpoint that charges per token
  up to a configurable max (default 5.00 HTR).

NOTE: the blueprint change adds a new method — requires republishing
the X402Escrow blueprint and updating BLUEPRINT_ID for /generate to
work. /weather (exact scheme) keeps working with the current blueprint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Recognize hathor-escrow-upto in the 402 options selector and render it
  with its own label ("Upto · Usage-billed") and blue accent.
- Pay button now says "Authorize up to X HTR" for upto options.
- After a successful upto request, show a "Usage-Based Settlement" panel
  with max authorized, actual charged, refund amount, and refund txId.
- Payment history badge distinguishes upto / channel / escrow, and upto
  rows show "charged X HTR · refunded Y HTR".
- Add preset URL buttons (/weather exact, /generate upto) so users can
  try both schemes without typing the URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pedroferreira1 pedroferreira1 moved this from In Progress (WIP) to Todo in Hathor Network May 13, 2026
…ned-requestId scheme

  - Replace X402Escrow / X402Channel blueprints with regular send-tx flow
  - Read-only verifier + better-sqlite3 dedup/blocklist
  - htr_sendTransaction + htr_signWithAddress on the dApp (UTXOs filtered to address-0)
  - Two schemes: hathor-direct (exact) + hathor-direct-upto (refund remainder)
  - Optional standalone facilitator; resource-server self-verifies by default
  The relative bind mount './data:/app/data' depended on Dokploy not wiping
  the cloned repo dir, which isn't guaranteed across redeploys/branch swaps.
  A named volume is Docker-managed and survives everything except an
  explicit 'docker volume rm'.
  Mint was using config.serverSecret || 'dev-fallback-secret' while verify
  used raw config.serverSecret. When SERVER_SECRET env was missing,
  mint hashed with the fallback and verify hashed with empty string,
  producing silent bad_request_id_mac on every retry. Make config.serverSecret
  resolve to the placeholder by default so both paths use the same key.
  - Document why payerAddress is by definition address-0 (WalletConnect's
    session approval shares getAddressAtIndex(0)) and add a null guard.
  - Always set changeAddress to address-0 so it stays funded across
    consecutive payments instead of draining into first_empty addresses.
  - Drop refreshBalance() call after a successful payment — it triggers an
    htr_getBalance popup on some wallets, which is noisy right after the
    user already signed twice. BalanceCard's manual button still works.
  The payload shape changes from {txId, payerAddress, signature, requestId}
  to {txId, signatures: [{address, signature}, ...], requestId}. The verifier
  now requires every unique input address of the broadcast tx to have a
  verified signature, proving the payer controls every funding source — not
  just the change/output address.

  This unblocks the x402-pay agent skill, whose headless wallet can pick
  UTXOs from multiple addresses; the dApp continues to send a single
  signature (address-0) wrapped in a one-element array.

  Breaking change: no back-compat with the legacy single-signature shape.
  Nothing in production today depends on it, and api.x402.hathor.dev +
  x402.hathor.dev redeploy atomically from this branch.

  - verifier.js: extract signatures[], require one per unique input address,
    identify the canonical payer as signatures[0].address (used for the
    upto-refund target and blocklist)
  - dapp/components/X402Fetch.tsx: wrap the WalletConnect signature into a
    one-element signatures array
  - client.js: same one-element wrap for the CLI path

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pedroferreira1 pedroferreira1 moved this from Todo to In Progress (WIP) in Hathor Network Jun 1, 2026
  A Claude Code skill that pays HTTP 402 responses on Hathor via a
  hathor-wallet-headless backend — no browser, no popups. On a 402 it
  establishes the wallet (asks the user for the headless URL/wallet-id,
  or spins up a container), pays on-chain, signs the server's requestId
  once per unique input address, retries with the multi-signature
  PAYMENT-SIGNATURE payload, and surfaces the resource. Covers both
  hathor-direct and hathor-direct-upto.

  Safety: asks before any on-chain action, refuses cross-network payments,
  never persists the seed to memory, never auto-confirms mainnet.

  Depends on POST /wallet/sign-message in hathor-wallet-headless (upstream
  PR pending; README references it as #TBD until merged).

  Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pedroferreira1 pedroferreira1 moved this from In Progress (WIP) to Todo in Hathor Network Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment