x402 Monetization
Monetize your AI agents using the x402 payment protocol — an open standard for HTTP micropayments that lets you charge users per message in USDC on Base.
Overview
x402 extends HTTP with native payments. When a client requests a paid resource, the server responds with 402 Payment Required and the client automatically signs a USDC payment. The server verifies and settles the payment, then processes the request. The entire flow happens in a single request-response cycle.
x402 is a protocol built on the HTTP 402 Payment Required status code. It enables machine-to-machine micropayments without subscriptions, API keys, or billing infrastructure. Spritz uses Coinbase's x402 facilitator for payment verification and settlement.
Why x402 for AI Agents?
| Benefit | Description |
|---|---|
| No API keys | Payments replace traditional API authentication |
| Instant settlement | USDC payments settle on Base in seconds |
| Machine-friendly | Designed for AI-to-AI and API-to-API commerce |
| Low fees | Base L2 gas costs are fractions of a cent |
| Open standard | Works with any x402-compatible client |
| No billing infrastructure | No invoicing, subscriptions, or payment processors |
How It Works
┌─────────────────────────────────────────────────────────────────┐
│ x402 Payment Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Client sends request │
│ POST /api/public/agents/{id}/chat │
│ Body: { "message": "Hello!" } │
│ │ │
│ ▼ │
│ 2. Server checks x402_enabled │
│ ┌─────────────────┐ │
│ │ X-Payment header │──── Yes ──► 4. Verify payment │
│ │ present? │ │
│ └────────┬────────┘ │
│ │ No │
│ ▼ │
│ 3. Return 402 Payment Required │
│ { │
│ "x402Version": 1, │
│ "accepts": [{ │
│ "scheme": "exact", │
│ "network": "base", │
│ "maxAmountRequired": "10000", │
│ "asset": "0x833589...USDC", │
│ "payTo": "0x...owner" │
│ }] │
│ } │
│ │ │
│ ▼ │
│ 4. Client signs USDC payment (via x402-fetch) │
│ Retries request with X-Payment header │
│ │ │
│ ▼ │
│ 5. Server verifies via Coinbase facilitator │
│ POST https://x402.org/facilitator/verify │
│ │ │
│ ▼ │
│ 6. Server settles payment │
│ POST https://x402.org/facilitator/settle │
│ │ │
│ ▼ │
│ 7. Process AI request, log transaction, return response │
│ │
└─────────────────────────────────────────────────────────────────┘
Enabling x402 on Your Agent
Prerequisites
- A public AI agent on Spritz (x402 requires
visibility: "public") - A wallet address to receive USDC payments
Configuration Steps
- Edit your agent in the Spritz app
- Navigate to Capabilities
- Enable x402 Payments
- Configure the settings:
| Setting | Description | Default |
|---|---|---|
| Price | Cost per message in cents (1 = $0.01) | 1 |
| Network | base (mainnet) or base-sepolia (testnet) | base |
| Wallet Address | Your address to receive USDC payments | Agent owner address |
| Pricing Mode | global (same for all) or per_tool (per API tool) | global |
x402 payments are only available for agents with public visibility. Private and friends-only agents cannot enable x402.
Agent Configuration (API)
// PATCH /api/agents/:id
const response = await fetch(`/api/agents/${agentId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
x402Enabled: true,
x402PriceCents: 1, // $0.01 per message
x402Network: "base", // "base" or "base-sepolia"
x402WalletAddress: "0x1234...5678",
x402PricingMode: "global", // "global" or "per_tool"
}),
});
Database Fields
| Column | Type | Default | Description |
|---|---|---|---|
x402_enabled | BOOLEAN | false | Whether x402 payments are active |
x402_price_cents | INTEGER | 1 | Price per message in cents |
x402_network | TEXT | 'base' | Network for payments |
x402_wallet_address | TEXT | null | Receive address (falls back to owner) |
x402_total_earnings_cents | INTEGER | 0 | Cumulative earnings |
x402_message_count_paid | INTEGER | 0 | Total paid messages |
x402_pricing_mode | TEXT | 'global' | Pricing strategy |
Pricing
Global Pricing
Set a single price per message for all users:
{
"x402_enabled": true,
"x402_price_cents": 1,
"x402_network": "base",
"x402_wallet_address": "0x...",
"x402_pricing_mode": "global"
}
Per-Tool Pricing
Charge different amounts depending on which API tool or MCP server the agent invokes:
{
"x402_enabled": true,
"x402_pricing_mode": "per_tool",
"x402_network": "base",
"api_tools": [
{
"name": "web_search",
"x402Enabled": true,
"x402PriceCents": 5
},
{
"name": "data_analysis",
"x402Enabled": true,
"x402PriceCents": 10
}
]
}
Price Conversion
Prices are stored in cents and converted to USDC microunits for the x402 protocol:
| Display | Stored | x402 Protocol (microUSDC) |
|---|---|---|
| $0.01 | 1 cent | 10,000 |
| $0.05 | 5 cents | 50,000 |
| $0.10 | 10 cents | 100,000 |
| $1.00 | 100 cents | 1,000,000 |
The minimum price is 1 cent ($0.01). The API enforces this: Math.max(1, x402PriceCents).
Supported Networks & Tokens
| Network | Chain ID | USDC Contract | Environment |
|---|---|---|---|
| Base | 8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | Production |
| Base Sepolia | 84532 | 0x036CbD53842c5426634e7929541eC2318f3dCF7e | Testing |
Only USDC on Base networks is currently supported. USDC is a stablecoin pegged to the US Dollar with 6 decimal places.
Public API Endpoint
When x402 is enabled, your agent is accessible via a public API that requires no authentication — only payment:
POST https://app.spritz.chat/api/public/agents/{agent_id}/chat
Request Body
{
"message": "Hello, agent!",
"sessionId": null
}
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The user's message |
sessionId | string | null | No | Session ID for conversation continuity |
Response (200 OK)
{
"message": "Hello! How can I help you today?",
"sessionId": "abc-123-def",
"agentName": "My Agent"
}
Response (402 Payment Required)
When no payment is provided or payment is invalid:
{
"error": "Payment Required",
"message": "This API requires a payment of $0.01 USDC",
"paymentRequirements": {
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"resource": "https://app.spritz.chat/api/public/agents/{id}/chat",
"description": "Chat with My Agent AI agent",
"mimeType": "application/json",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x1234...5678",
"maxTimeoutSeconds": 300,
"extra": {
"name": "USD Coin",
"version": 2
}
}
]
}
}
Error Responses
| Status | Description |
|---|---|
402 | Payment required or invalid payment |
403 | Agent is not public |
404 | Agent not found |
500 | Server error during verification |
Client Integration
Using x402-fetch (Recommended)
The x402-fetch library wraps the native fetch API to automatically handle 402 responses by signing and sending USDC payments:
import { wrapFetchWithPayment } from "x402-fetch";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
// Setup wallet client
const account = privateKeyToAccount("0x...");
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
// Wrap fetch with automatic x402 payment handling
const fetchWithPay = wrapFetchWithPayment(fetch, walletClient);
// Send a paid request — payment is handled transparently
const response = await fetchWithPay(
"https://app.spritz.chat/api/public/agents/{agent_id}/chat",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: "What is the weather in New York?",
sessionId: null,
}),
}
);
const data = await response.json();
console.log(data.message);
React Integration
import { useState, useCallback } from "react";
import { wrapFetchWithPayment } from "x402-fetch";
import { useWalletClient } from "wagmi";
function PaidAgentChat({ agentId }: { agentId: string }) {
const { data: walletClient } = useWalletClient();
const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]);
const [sessionId, setSessionId] = useState<string | null>(null);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const sendMessage = useCallback(async () => {
if (!walletClient || !input.trim()) return;
const userMessage = input.trim();
setInput("");
setLoading(true);
// Add user message to chat
setMessages((prev) => [...prev, { role: "user", content: userMessage }]);
try {
// x402-fetch handles the 402 → payment → retry flow
const fetchWithPay = wrapFetchWithPayment(fetch, walletClient);
const response = await fetchWithPay(
`https://app.spritz.chat/api/public/agents/${agentId}/chat`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: userMessage,
sessionId,
}),
}
);
const data = await response.json();
setSessionId(data.sessionId);
setMessages((prev) => [
...prev,
{ role: "assistant", content: data.message },
]);
} catch (error) {
console.error("Payment or request failed:", error);
setMessages((prev) => [
...prev,
{ role: "assistant", content: "Payment failed. Please try again." },
]);
} finally {
setLoading(false);
}
}, [walletClient, input, agentId, sessionId]);
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={msg.role}>
{msg.content}
</div>
))}
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
placeholder="Type a message..."
disabled={loading}
/>
<button onClick={sendMessage} disabled={loading || !walletClient}>
{loading ? "Sending..." : "Send"}
</button>
</div>
);
}
cURL Example
For testing, you can manually construct the flow:
# Step 1: Make initial request (will return 402)
curl -X POST https://app.spritz.chat/api/public/agents/{id}/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello!"}'
# Step 2: Parse the 402 response for payment requirements
# Step 3: Sign payment with your wallet
# Step 4: Retry with X-Payment header
curl -X POST https://app.spritz.chat/api/public/agents/{id}/chat \
-H "Content-Type: application/json" \
-H "X-Payment: {signed_payment_json}" \
-d '{"message": "Hello!"}'
Node.js Script Example
import { wrapFetchWithPayment } from "x402-fetch";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
const AGENT_ID = "your-agent-id";
const API_URL = "https://app.spritz.chat";
async function chatWithAgent(message: string) {
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const fetchWithPay = wrapFetchWithPayment(fetch, walletClient);
const response = await fetchWithPay(
`${API_URL}/api/public/agents/${AGENT_ID}/chat`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, sessionId: null }),
}
);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
}
// Usage
const result = await chatWithAgent("Explain quantum computing");
console.log(result.message);
Server-Side Implementation
Payment Verification Flow
The server uses Coinbase's x402 facilitator for trustless payment verification:
import { NextRequest, NextResponse } from "next/server";
interface X402Config {
priceUSD: string; // e.g., "$0.01"
network: "base" | "base-sepolia";
payToAddress: string;
description?: string;
}
// 1. Generate payment requirements for 402 response
function generatePaymentRequirements(config: X402Config) {
const cents = parseFloat(config.priceUSD.replace("$", "")) * 100;
const microUSDC = String(Math.round(cents * 10000));
const usdcAddress =
config.network === "base"
? "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
: "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
return {
x402Version: 1,
accepts: [
{
scheme: "exact",
network: config.network,
maxAmountRequired: microUSDC,
resource: "",
description: config.description || "API access",
mimeType: "application/json",
asset: usdcAddress,
payTo: config.payToAddress,
maxTimeoutSeconds: 300,
extra: { name: "USD Coin", version: 2 },
},
],
};
}
// 2. Verify payment via Coinbase facilitator
async function verifyX402Payment(
request: NextRequest,
config: X402Config
): Promise<X402PaymentResult> {
const paymentHeader = request.headers.get("X-Payment");
if (!paymentHeader) {
return { isValid: false, error: "No payment header" };
}
const paymentPayload = JSON.parse(paymentHeader);
const requirements = generatePaymentRequirements(config);
requirements.accepts[0].resource = request.url;
const facilitatorUrl =
process.env.X402_FACILITATOR_URL || "https://x402.org/facilitator";
// Verify the payment signature
const verifyResponse = await fetch(`${facilitatorUrl}/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
paymentPayload,
paymentRequirements: requirements.accepts[0],
}),
});
if (!verifyResponse.ok) {
return { isValid: false, error: "Payment verification failed" };
}
const verification = await verifyResponse.json();
if (!verification.isValid) {
return {
isValid: false,
error: verification.invalidReason || "Invalid payment",
};
}
// Settle the payment on-chain
const settleResponse = await fetch(`${facilitatorUrl}/settle`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
paymentPayload,
paymentRequirements: requirements.accepts[0],
}),
});
const settlement = await settleResponse.json();
return {
isValid: true,
payerAddress: paymentPayload.from,
amountPaid: parseInt(paymentPayload.amount) / 10000,
transactionHash: settlement.transactionHash,
};
}
Middleware Helper
Use requireX402Payment as middleware before processing requests:
// Returns null if payment is valid, or a 402 Response if not
const paymentResponse = await requireX402Payment(request, x402Config);
if (paymentResponse) {
return paymentResponse; // Return 402 to client
}
// Payment verified — proceed with agent processing
Transaction Logging
Every successful payment is logged for analytics and auditing:
// Increment agent earnings and message count
await supabase.rpc("increment_agent_paid_stats", {
agent_id_param: agentId,
amount_cents_param: paymentAmountCents,
});
// Log full transaction details
await supabase.from("shout_agent_x402_transactions").insert({
agent_id: agentId,
payer_address: payerAddress,
amount_cents: paymentAmountCents,
network: agent.x402_network || "base",
transaction_hash: paymentTxHash,
});
Transaction Tracking
Transaction Table
Every x402 payment is recorded in shout_agent_x402_transactions:
CREATE TABLE shout_agent_x402_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID NOT NULL REFERENCES shout_agents(id) ON DELETE CASCADE,
payer_address TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
network TEXT NOT NULL,
transaction_hash TEXT,
facilitator_response JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Agent Earnings Stats
Cumulative stats are tracked directly on the agent record:
| Stat | Description |
|---|---|
x402_total_earnings_cents | Total USDC earned (in cents) |
x402_message_count_paid | Number of paid messages processed |
These are updated atomically via a database function:
CREATE OR REPLACE FUNCTION increment_agent_paid_stats(
agent_id_param UUID,
amount_cents_param INTEGER
) RETURNS void LANGUAGE plpgsql AS $$
BEGIN
UPDATE shout_agents
SET
x402_message_count_paid = COALESCE(x402_message_count_paid, 0) + 1,
x402_total_earnings_cents = COALESCE(x402_total_earnings_cents, 0) + amount_cents_param
WHERE id = agent_id_param;
END;
$$;
Scheduling Integration
x402 payments also work with Spritz's call scheduling system, allowing users to charge for scheduled calls:
// From /api/scheduling/schedule
if (paymentHeader) {
const x402Config: X402Config = {
priceUSD: `$${(priceCents / 100).toFixed(2)}`,
network: (settings.scheduling_network || "base") as "base" | "base-sepolia",
payToAddress: settings.scheduling_wallet_address || recipientAddress,
description: `Schedule a call with ${recipientAddress.slice(0, 6)}...`,
};
paymentResult = await verifyX402Payment(mockRequest, x402Config);
}
Environment Variables
| Variable | Default | Description |
|---|---|---|
X402_FACILITATOR_URL | https://x402.org/facilitator | Coinbase facilitator endpoint |
NEXT_PUBLIC_APP_URL | https://app.spritz.chat | Used for embed code generation |
Embedding Your Agent
Agent owners can generate embed code for external developers to integrate their x402-enabled agent:
Basic Embed
<script type="module">
import { wrapFetchWithPayment } from "https://esm.sh/x402-fetch";
import { createWalletClient, http } from "https://esm.sh/viem";
import { base } from "https://esm.sh/viem/chains";
const walletClient = createWalletClient({
chain: base,
transport: http(),
});
const fetchWithPay = wrapFetchWithPayment(fetch, walletClient);
async function chat(message) {
const res = await fetchWithPay(
"https://app.spritz.chat/api/public/agents/YOUR_AGENT_ID/chat",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
}
);
return await res.json();
}
</script>
Security Considerations
| Aspect | Details |
|---|---|
| No private keys on server | Spritz never handles user private keys; payments are signed client-side |
| Facilitator verification | Coinbase's facilitator independently verifies payment signatures |
| On-chain settlement | Payments settle on Base L2 with a transaction hash |
| Payment timeout | Payments expire after 300 seconds (5 minutes) |
| Minimum price | Enforced at 1 cent ($0.01) minimum to prevent spam |
| Public agents only | x402 requires public visibility, preventing abuse of private agents |
Best Practices
- Test on Base Sepolia first — Validate your pricing and flow with testnet USDC before going live
- Set fair prices — Consider the value your agent provides; $0.01-$0.10 per message is typical
- Use per-tool pricing — Charge more for expensive operations (web search, data analysis) and less for simple queries
- Monitor earnings — Track your
x402_total_earnings_centsandx402_message_count_paidstats - Communicate pricing — Make your agent's cost transparent on its public page
- Handle errors gracefully — The public agent page shows a friendly "Payment Required" message when x402 is active
Troubleshooting
Payment Not Processing
Symptoms: Client receives 402 but payment never completes.
Cause: Insufficient USDC balance, wrong network, or wallet not connected.
Solution:
- Verify the client wallet has USDC on the correct network (Base or Base Sepolia)
- Check that
x402-fetchis wrapping the correct wallet client - Ensure the wallet client chain matches the agent's configured network
Payment Verified but Request Fails
Symptoms: Payment deducted but no response.
Cause: Server-side processing error after payment.
Solution: Check server logs. The transaction is logged in shout_agent_x402_transactions even if the AI response fails. Contact the agent owner.
Agent Returns 403 Instead of 402
Symptoms: 403 Forbidden response.
Cause: Agent is not set to public visibility.
Solution: Update the agent's visibility to public — x402 requires public agents.
Wrong USDC Contract
Symptoms: Payment verification fails.
Cause: Client is using USDC on a different chain or the wrong contract address.
Solution: Ensure you're using the correct USDC contract for the configured network:
- Base:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 - Base Sepolia:
0x036CbD53842c5426634e7929541eC2318f3dCF7e
Next Steps
- Creating AI Agents — Build and configure your agent
- Agent Architecture — How agents process messages
- RAG Technical Details — Vector search and knowledge bases
- MCP Servers — Extend agents with external tools
- Agents API Reference — Full API documentation
- Calendar & Scheduling — Paid scheduling with x402