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​