Skip to main content

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.

What is x402?

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?

BenefitDescription
No API keysPayments replace traditional API authentication
Instant settlementUSDC payments settle on Base in seconds
Machine-friendlyDesigned for AI-to-AI and API-to-API commerce
Low feesBase L2 gas costs are fractions of a cent
Open standardWorks with any x402-compatible client
No billing infrastructureNo 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

  1. Edit your agent in the Spritz app
  2. Navigate to Capabilities
  3. Enable x402 Payments
  4. Configure the settings:
SettingDescriptionDefault
PriceCost per message in cents (1 = $0.01)1
Networkbase (mainnet) or base-sepolia (testnet)base
Wallet AddressYour address to receive USDC paymentsAgent owner address
Pricing Modeglobal (same for all) or per_tool (per API tool)global
warning

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

ColumnTypeDefaultDescription
x402_enabledBOOLEANfalseWhether x402 payments are active
x402_price_centsINTEGER1Price per message in cents
x402_networkTEXT'base'Network for payments
x402_wallet_addressTEXTnullReceive address (falls back to owner)
x402_total_earnings_centsINTEGER0Cumulative earnings
x402_message_count_paidINTEGER0Total paid messages
x402_pricing_modeTEXT'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:

DisplayStoredx402 Protocol (microUSDC)
$0.011 cent10,000
$0.055 cents50,000
$0.1010 cents100,000
$1.00100 cents1,000,000
tip

The minimum price is 1 cent ($0.01). The API enforces this: Math.max(1, x402PriceCents).


Supported Networks & Tokens

NetworkChain IDUSDC ContractEnvironment
Base84530x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913Production
Base Sepolia845320x036CbD53842c5426634e7929541eC2318f3dCF7eTesting
info

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
}
FieldTypeRequiredDescription
messagestringYesThe user's message
sessionIdstring | nullNoSession 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

StatusDescription
402Payment required or invalid payment
403Agent is not public
404Agent not found
500Server error during verification

Client Integration

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:

StatDescription
x402_total_earnings_centsTotal USDC earned (in cents)
x402_message_count_paidNumber 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

VariableDefaultDescription
X402_FACILITATOR_URLhttps://x402.org/facilitatorCoinbase facilitator endpoint
NEXT_PUBLIC_APP_URLhttps://app.spritz.chatUsed 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

AspectDetails
No private keys on serverSpritz never handles user private keys; payments are signed client-side
Facilitator verificationCoinbase's facilitator independently verifies payment signatures
On-chain settlementPayments settle on Base L2 with a transaction hash
Payment timeoutPayments expire after 300 seconds (5 minutes)
Minimum priceEnforced at 1 cent ($0.01) minimum to prevent spam
Public agents onlyx402 requires public visibility, preventing abuse of private agents

Best Practices

  1. Test on Base Sepolia first — Validate your pricing and flow with testnet USDC before going live
  2. Set fair prices — Consider the value your agent provides; $0.01-$0.10 per message is typical
  3. Use per-tool pricing — Charge more for expensive operations (web search, data analysis) and less for simple queries
  4. Monitor earnings — Track your x402_total_earnings_cents and x402_message_count_paid stats
  5. Communicate pricing — Make your agent's cost transparent on its public page
  6. 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:

  1. Verify the client wallet has USDC on the correct network (Base or Base Sepolia)
  2. Check that x402-fetch is wrapping the correct wallet client
  3. 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