A Cloudflare Worker that enables gasless transactions for AI agents on the Stacks blockchain by sponsoring transactions and verifying payment settlement.
The x402 protocol is an HTTP-native payment standard that uses the HTTP 402 "Payment Required" status code to enable instant, autonomous stablecoin payments. This relay service brings gasless transactions to Stacks by:
- Accepting pre-signed sponsored transactions from agents
- Validating the transaction format (must be sponsored type)
- Sponsoring the transaction (covers gas fees)
- Calling the x402 facilitator for settlement verification
- Storing payment receipts for verification and resource access
- Returning the settlement status, sponsored tx hex, and receipt token to the agent
Sponsor and broadcast a transaction directly (requires API key authentication).
Headers:
Authorization: Bearer x402_sk_test_...
Content-Type: application/json
Request:
{
"transaction": "<hex-encoded-sponsored-stacks-transaction>"
}Response (success):
{
"success": true,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"txid": "0x...",
"explorerUrl": "https://explorer.hiro.so/txid/0x...?chain=testnet",
"fee": "1000"
}Response (error):
{
"success": false,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"error": "Daily spending cap exceeded",
"code": "SPENDING_CAP_EXCEEDED",
"details": "Your API key has exceeded its daily spending limit.",
"retryable": true,
"retryAfter": 3600
}| Error Code | HTTP Status | Description |
|---|---|---|
MISSING_API_KEY |
401 | No API key provided |
INVALID_API_KEY |
401 | API key not found or revoked |
EXPIRED_API_KEY |
401 | API key has expired |
MISSING_TRANSACTION |
400 | Transaction field is missing |
INVALID_TRANSACTION |
400 | Transaction is malformed |
NOT_SPONSORED |
400 | Transaction must be built with sponsored: true |
SPENDING_CAP_EXCEEDED |
429 | Daily fee cap exceeded for this API key tier |
BROADCAST_FAILED |
502 | Transaction rejected by network |
Submit a sponsored transaction for relay and settlement.
Request:
{
"transaction": "<hex-encoded-sponsored-stacks-transaction>",
"settle": {
"expectedRecipient": "SP...",
"minAmount": "1000000",
"tokenType": "STX",
"expectedSender": "SP...",
"resource": "/api/endpoint",
"method": "GET"
}
}| Field | Required | Description |
|---|---|---|
transaction |
Yes | Hex-encoded sponsored Stacks transaction |
settle.expectedRecipient |
Yes | Expected payment recipient address |
settle.minAmount |
Yes | Minimum payment amount (in smallest unit) |
settle.tokenType |
No | Token type: STX, sBTC, USDCx (default: STX) |
settle.expectedSender |
No | Expected sender address for validation |
settle.resource |
No | API resource being accessed (for tracking) |
settle.method |
No | HTTP method being used (for tracking) |
Response (success):
{
"success": true,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"txid": "0x...",
"explorerUrl": "https://explorer.hiro.so/txid/0x...?chain=testnet",
"settlement": {
"success": true,
"status": "confirmed",
"sender": "SP...",
"recipient": "SP...",
"amount": "1000000",
"blockHeight": 12345
},
"sponsoredTx": "0x00000001...",
"receiptId": "550e8400-e29b-41d4-a716-446655440000"
}Note:
sponsoredTxcontains the fully-sponsored transaction hex (usable asX-PAYMENTheader).receiptIdis only returned when receipt storage succeeds (requiresRELAY_KVbinding).
Response (error):
{
"success": false,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"error": "Transaction must be sponsored",
"code": "NOT_SPONSORED",
"details": "Build transaction with sponsored: true",
"retryable": false
}Look up a payment receipt by ID and return its status.
Response (success):
{
"success": true,
"requestId": "...",
"receipt": {
"receiptId": "550e8400-...",
"status": "valid",
"createdAt": "2025-01-01T00:00:00.000Z",
"expiresAt": "2025-01-01T01:00:00.000Z",
"senderAddress": "SP...",
"txid": "0x...",
"explorerUrl": "https://explorer.hiro.so/txid/0x...?chain=testnet",
"settlement": {
"success": true,
"status": "confirmed",
"recipient": "SP...",
"amount": "1000000"
},
"resource": "/api/endpoint",
"method": "GET",
"accessCount": 0
}
}Response (not found): 404 for unknown or expired receipts.
Access a protected resource using a payment receipt. Validates the receipt (exists, not expired, not consumed, resource matches) and either returns relay-hosted data or proxies to a downstream service.
Request:
{
"receiptId": "550e8400-...",
"resource": "/api/endpoint",
"targetUrl": "https://downstream-service.com/api/endpoint"
}| Field | Required | Description |
|---|---|---|
receiptId |
Yes | Receipt ID from a successful relay transaction |
resource |
No | Resource path (validated against receipt) |
targetUrl |
No | Downstream URL for proxying (HTTPS only, no internal hosts) |
Response (success):
{
"success": true,
"requestId": "...",
"granted": true,
"receipt": {
"receiptId": "550e8400-...",
"senderAddress": "SP...",
"resource": "/api/endpoint",
"accessCount": 1
},
"data": { "..." }
}Note: Receipts are one-time-use — consumed after successful access. If proxying, the receipt is only consumed on a 2xx downstream response. The
targetUrlmust be HTTPS and cannot point to internal hosts.
Health check endpoint.
Response:
{
"status": "ok",
"network": "testnet",
"version": "0.3.0"
}Interactive API documentation (Swagger UI).
OpenAPI 3.1 specification for programmatic access.
Transactions must be built with sponsored: true and fee: 0n:
import { makeSTXTokenTransfer, getAddressFromPrivateKey, TransactionVersion } from "@stacks/transactions";
const senderAddress = getAddressFromPrivateKey(privateKey, TransactionVersion.Testnet);
const recipient = "SP..."; // Payment recipient
const transaction = await makeSTXTokenTransfer({
recipient,
amount: 1000000n,
senderKey: privateKey,
network: "testnet",
sponsored: true, // Required
fee: 0n, // Sponsor pays
});
const txHex = Buffer.from(transaction.serialize()).toString("hex");const response = await fetch("https://x402-relay.aibtc.dev/relay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
transaction: txHex,
settle: {
expectedRecipient: recipient,
minAmount: "1000000",
tokenType: "STX",
expectedSender: senderAddress,
},
}),
});
const { txid, settlement, sponsoredTx, receiptId } = await response.json();
console.log(`Transaction: https://explorer.hiro.so/txid/${txid}?chain=testnet`);
console.log(`Settlement status: ${settlement.status}`);
// Use receiptId to verify payment or access protected resources
if (receiptId) {
const verifyResponse = await fetch(`https://x402-relay.aibtc.dev/verify/${receiptId}`);
const { receipt } = await verifyResponse.json();
console.log(`Receipt status: ${receipt.status}`);
}| Environment | URL | Network |
|---|---|---|
| Staging | https://x402-relay.aibtc.dev | Testnet |
| Production | https://x402-relay.aibtc.com | Mainnet |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
None | Service info |
| GET | /health |
None | Health check with version and network |
| GET | /docs |
None | Swagger UI documentation |
| GET | /openapi.json |
None | OpenAPI specification |
| POST | /relay |
None | Submit transaction via x402 facilitator |
| POST | /sponsor |
API Key | Sponsor and broadcast transaction directly |
| GET | /verify/:receiptId |
None | Verify a payment receipt |
| POST | /access |
None | Access protected resource with receipt |
| GET | /stats |
None | Relay statistics (JSON) |
| GET | /dashboard |
None | Public dashboard (HTML) |
- 10 requests per minute per sender address
- Rate limiting is based on the transaction sender, not IP
Rate limits and spending caps are based on API key tier:
| Tier | Requests/min | Requests/day | Daily Fee Cap |
|---|---|---|---|
| free | 10 | 100 | 100 STX |
| standard | 60 | 10,000 | 1,000 STX |
| unlimited | Unlimited | Unlimited | No cap |
The /sponsor endpoint requires API key authentication.
API keys are provisioned via the CLI:
# Set your environment (staging = testnet, production = mainnet)
export WRANGLER_ENV=staging
# Create a new API key
npm run keys -- create --app "My App" --email "dev@example.com"
# Create with specific tier (default: free)
npm run keys -- create --app "My App" --email "dev@example.com" --tier standard# List all API keys
WRANGLER_ENV=staging npm run keys -- list
# Get info about a specific key
WRANGLER_ENV=staging npm run keys -- info x402_sk_test_...
# View usage statistics (last 7 days)
WRANGLER_ENV=staging npm run keys -- usage x402_sk_test_... --days 7
# Renew an expiring key (extends by 30 days)
WRANGLER_ENV=staging npm run keys -- renew x402_sk_test_...
# Revoke a key
WRANGLER_ENV=staging npm run keys -- revoke x402_sk_test_...Include the API key in the Authorization header:
const response = await fetch("https://x402-relay.aibtc.dev/sponsor", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer x402_sk_test_...",
},
body: JSON.stringify({ transaction: txHex }),
});- Cloudflare Workers - Serverless deployment
- Hono - Lightweight web framework
- Chanfana - OpenAPI documentation generator
- @stacks/transactions - Stacks transaction handling
- x402-stacks - x402 protocol implementation for Stacks
# Install dependencies
npm install
# Create .env and configure credentials (see Environment Variables below)
# Required: AGENT_MNEMONIC or AGENT_PRIVATE_KEY for test scripts
# Start local dev server
npm run dev
# Test /relay endpoint (no auth required)
npm run test:relay # Uses RELAY_URL from .env or localhost
npm run test:relay -- http://localhost:8787 # Override relay URL
# Test /sponsor endpoint (requires API key)
npm run test:sponsor # Uses TEST_API_KEY from .env
npm run test:sponsor -- http://localhost:8787 # Override relay URL
# Type check
npm run checkThe test scripts support these environment variables (set in .env):
| Variable | Description |
|---|---|
AGENT_MNEMONIC |
24-word mnemonic phrase (recommended) |
AGENT_PRIVATE_KEY |
Hex-encoded private key (alternative) |
AGENT_ACCOUNT_INDEX |
Account index to derive from mnemonic (default: 0) |
RELAY_URL |
Relay endpoint URL (default: http://localhost:8787) |
TEST_API_KEY |
API key for /sponsor endpoint (required for test:sponsor) |
- x402 Protocol - HTTP-native payment standard
- x402-stacks - x402 for Stacks
- stx402 - x402 Stacks implementation
MIT