feat: x402 payment protocol — complete POC with escrow, channels, dApp, and deployment - #1
Open
andreabadesso wants to merge 37 commits into
Open
feat: x402 payment protocol — complete POC with escrow, channels, dApp, and deployment#1andreabadesso wants to merge 37 commits into
andreabadesso wants to merge 37 commits into
Conversation
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>
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>
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>
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>
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
node1.playground.testnet.hathor.network)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.
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
PAYMENT-SIGNATUREheaderX-Payment, backward compat kept)PAYMENT-RESPONSEheaderx402Version: 2fieldhathor:<network>used consistently via configpricefieldamountin 402accepts[]array/healthendpointexamples/mcp-server/— payment-gated AI tool callsWhat's included
Nano Contract Blueprints
blueprint/x402_escrow.pyblueprint/x402_channel.pyBackend Services (Node.js)
facilitator.js/healthendpoint with wallet auto-recovery. Idempotent escrow settlements.resource-server.jsPAYMENT-RESPONSEheader on success.client.jsPAYMENT-SIGNATUREheader.Frontend dApp (Next.js)
dapp/components/X402Fetch.tsxdapp/components/EscrowList.tsxdapp/components/EscrowDetail.tsxdapp/components/CreateEscrowForm.tsxBuilt 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
examples/mcp-server/server.jsget_weather(paid) andget_price(free) toolsexamples/mcp-server/README.mdDeployment (Docker Compose)
docker-compose.ymlDockerfiledapp/Dockerfileinit-wallets.shDeployed on Dokploy at
*.x402.hathor.devconnected 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:
@hathor/x402-client)@hathor/x402-server)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
PAYMENT-SIGNATUREheader sent by client (Base64)PAYMENT-RESPONSEheader present on 200 responsesx402Version: 2andpricefield/healthendpoint returns wallet statusX-Paymentheader still accepted🤖 Generated with Claude Code