PaymentSkillX402 - x402 Protocol Support
Full x402 payment protocol integration for WebAgents, enabling agents to provide and consume paid APIs using multiple payment schemes.
Overview
PaymentSkillX402 extends PaymentSkill with complete x402 protocol support, enabling agents to provide and consume paid APIs using multiple payment schemes including blockchain cryptocurrencies.
Key Features:
- ✅ All PaymentSkill functionality (token validation, cost calculation, hooks)
- ✅ Agent B: Expose paid HTTP endpoints with
@http+@pricing - ✅ Agent A: Automatic payment handling via hooks (no manual tool calls needed)
- ✅ Multiple payment schemes: robutler tokens, blockchain (USDC), etc.
- ✅ Cross-token exchange: convert crypto to credits automatically
- ✅ Standard x402 protocol:
scheme: "token", network: "robutler"
What is x402?
x402 is a payments protocol for HTTP, built on blockchain concepts. It allows HTTP APIs to require payment before serving requests, with standardized payment verification and settlement.
Core Concepts:
- 402 Payment Required: HTTP status code indicating payment needed
- Payment Requirements: Structured spec of what payment types are accepted
- Payment Header: Cryptographic proof of payment included in
X-PAYMENTheader - Facilitator: Third-party service that verifies and settles payments
- Multiple Schemes: Support for various payment types (tokens, blockchain, etc.)
Learn more: x402 Protocol Specification
Installation
npm install webagents
# or
pnpm add webagentsQuick Start
Agent B: Providing Paid APIs
Create an agent that exposes a paid HTTP endpoint:
import { BaseAgent, http, pricing, Skill } from 'webagents';
import { PaymentX402Skill } from 'webagents/skills/payments';
class WeatherSkill extends Skill {
readonly name = 'weather';
@http({ path: '/weather', method: 'GET' })
@pricing({ creditsPerCall: 0.05, reason: 'Weather API call' })
async getWeather(params: { location: string }): Promise<unknown> {
return { location: params.location, temperature: 72, conditions: 'sunny' };
}
}
const agentB = new BaseAgent({
name: 'weather-api',
apiKey: process.env.ROBUTLER_API_KEY,
skills: [
new PaymentX402Skill({
acceptedSchemes: [{ scheme: 'token', network: 'robutler' }],
}),
new WeatherSkill(),
],
});When called without payment, returns HTTP 402 with x402 V2 requirements:
curl http://localhost:8080/weather-api/weather?location=SF
# Response: HTTP 402 Payment Required
{
"x402Version": 2,
"accepts": [
{
"scheme": "token",
"network": "robutler",
"amount": "0.05",
"asset": "USD",
"resource": "/weather",
"description": "Weather API call",
"mimeType": "application/json",
"payTo": "agent_weather-api",
"maxTimeoutSeconds": 60,
"extra": {
"tokenPricing": {
"creditsPerCall": 0.05,
"chargeTypes": ["platform_fee", "platform_llm", "agent_fee"]
}
}
}
]
}With valid X-PAYMENT header:
curl -H "X-PAYMENT: <base64_payment_header>" \
http://localhost:8080/weather-api/weather?location=SF
# Response: HTTP 200 OK
{
"location": "SF",
"temperature": 72,
"conditions": "sunny"
}Agent A: Consuming Paid APIs
Create an agent that can automatically pay for services:
import { BaseAgent } from 'webagents';
import { PaymentX402Skill } from 'webagents/skills/payments';
const agentA = new BaseAgent({
name: 'consumer',
apiKey: process.env.ROBUTLER_API_KEY,
skills: [new PaymentX402Skill()],
});
// When the agent makes HTTP requests to paid endpoints:
// 1. Gets 402 response with payment requirements
// 2. Skill automatically creates payment
// 3. Retries with X-PAYMENT header
// 4. Returns resultConfiguration
Basic Configuration
new PaymentX402Skill({
facilitatorUrl: 'https://robutler.ai/api/payments',
acceptedSchemes: [{ scheme: 'token', network: 'robutler' }],
maxPayment: 10.0,
});Multi-Scheme Support (Agent B)
Accept both robutler tokens and blockchain payments:
new PaymentX402Skill({
acceptedSchemes: [
{ scheme: 'token', network: 'robutler' },
{ scheme: 'exact', network: 'base-mainnet' },
],
});Blockchain Support (Agent A)
Enable direct blockchain payments:
// Coming soon — track at https://github.com/robutlerai/webagents/issues
// Direct blockchain wallet support (auto-exchange + signed payments) is
// currently Python-only. TypeScript handles the `token` scheme today.JWKS verification flow
When the X-PAYMENT header contains a JWT (e.g. from POST /api/payments/lock):
- Decode the JWT header (unverified) to get
kidand readissfrom claims. - Fetch the issuer's public keys from
{iss}/.well-known/jwks.json(cached with TTL/ETag). - Verify the JWT signature with RS256 and validate
exp,aud. - Read
payment.balancefrom claims. If verification succeeds, the verify API call can be skipped. - Settlement still uses
POST /api/payments/settleso the platform can deduct balance and credit the recipient.
This reduces latency when the payer uses JWT payment tokens.
Payment Flow
Lock-First Settlement Model
All payment flows use a lock-first model: a single lock (payment token) is created at the beginning of a conversation turn and reused for all settlements within that turn. Settlements happen in a fixed order:
platform_fee— platform margin (configurable viaPLATFORM_FEE_PERCENT, default 20%)platform_llm— LLM inference cost (when platform keys are used, not BYOK)agent_fee— agent markup (configurable viaagent_pricing_percent, default 100%)
Two-Settle Model
Each tool invocation or LLM call may produce two settlements against the same lock:
- The platform settle (
platform_fee+ optionallyplatform_llm) happens first. - The agent settle (
agent_fee) happens second.
This ensures the platform always recovers its costs before the agent receives its markup.
BYOK (Bring Your Own Key) Flow
When the user provides their own LLM API key:
- The native LLM skill detects BYOK (user-supplied provider key exists).
- LLM inference is routed through the user's key — no
platform_llmcharge. - Settlement routes to
agent_feeonly (plusplatform_feeon the agent markup). - If no BYOK key exists,
platform_llmis settled for the inference cost.
Flow 1: Agent A → Agent B (Robutler Token)
1. Agent A calls Agent B's endpoint
GET /weather?location=SF
2. Agent B returns HTTP 402 with x402 V2 payment requirements
{
"x402Version": 2,
"accepts": [{"scheme": "token", "network": "robutler", "amount": "0.05", ...}]
}
3. Agent A's PaymentSkillX402 hook:
- Checks for compatible payment scheme
- Uses existing token from context or API
- Encodes payment header
4. Agent A retries request with X-PAYMENT header
GET /weather?location=SF
X-PAYMENT: <base64_payment_header>
5. Agent B's PaymentSkillX402 hook:
- Verifies payment via facilitator /verify
- Settles platform_fee first, then agent_fee
- Allows request to proceed
6. Agent B returns result
{"location": "SF", "temperature": 72}Flow 2: Agent A → Agent B (Crypto via Exchange)
1. Agent A calls Agent B, gets 402 with token scheme in accepts
2. Agent A has no token but has crypto wallet
3. Agent A's skill:
- Calls facilitator /exchange GET (see rates)
- Creates blockchain payment
- Calls /exchange POST with crypto payment → gets robutler token
4. Agent A retries with new token in X-PAYMENT header
5. Agent B processes payment normallyFlow 3: Agent A → Agent B (Direct Blockchain)
1. Agent B returns 402 with blockchain scheme (e.g., "exact:base-mainnet")
2. Agent A's PaymentSkillX402:
- Creates blockchain payment using wallet
- Includes x402 payment header
3. Agent B's PaymentSkillX402:
- Validates/settles via CDP/x402.org proxy
- Creates virtual token in Portal API
4. Subsequent requests use virtual token until depletedPayment Schemes
Token (Robutler)
Platform credits with instant settlement:
- Scheme:
"token" - Network:
"robutler" - Benefits: Instant, no gas fees, best for agent-to-agent
- Rate: 1:1 USD
{
"scheme": "token",
"network": "robutler",
"amount": "0.05",
"asset": "USD"
}Exact (Blockchain)
Direct USDC payments on various blockchains:
- Scheme:
"exact" - Networks:
"base-mainnet","solana","polygon","avalanche" - Benefits: Real blockchain settlement, decentralized
- Note: Gas fees covered by facilitator
{
"scheme": "exact",
"network": "base-mainnet",
"amount": "1.00",
"asset": "USDC"
}Advanced Features
The @pricing Decorator
The @pricing decorator supports a lock parameter for pre-authorization:
import { http, pricing, Skill } from 'webagents';
class WeatherSkill extends Skill {
readonly name = 'weather';
@http({ path: '/weather', method: 'GET' })
@pricing({ creditsPerCall: 0.05, reason: 'Weather API call', lock: true })
async getWeather(params: { location: string }): Promise<unknown> {
return { location: params.location, temperature: 72 };
}
@http({ path: '/analyze', method: 'POST' })
@pricing({ creditsPerCall: 0.5, reason: 'Analysis', lock: true })
async analyze(params: { data: unknown }): Promise<unknown> {
return { ok: true };
}
}When lock=True, the transport ensures a payment token with sufficient balance exists before the handler runs. If the token is insufficient, a mid-stream top-up flow is triggered (see UAMP Token Top-Up below).
Dynamic Pricing
Use PricingInfo (Python) or a _pricing field on the result (TypeScript) for dynamic pricing based on request params:
import { http, pricing, Skill } from 'webagents';
class AnalyzeSkill extends Skill {
readonly name = 'analyze';
@http({ path: '/analyze', method: 'POST' })
@pricing()
async analyzeData(params: {
data: unknown;
complexity?: 'basic' | 'advanced' | 'enterprise';
}): Promise<unknown> {
const tier = params.complexity ?? 'basic';
const credits = { basic: 0.1, advanced: 0.5, enterprise: 2.0 }[tier];
return {
result: 'analysis result',
_pricing: {
credits,
reason: `Data analysis (${tier})`,
metadata: { complexity: tier },
},
};
}
}Payment Priority
Agent A tries payment methods in this order:
- Existing robutler token from context
- Robutler token from agent's token list (includes virtual tokens)
- Exchange crypto for credits (if
auto_exchange=Trueand wallet configured) - Direct blockchain payment (if wallet configured)
Virtual Tokens
When Agent B receives direct blockchain payments, a "virtual token" is automatically created and tracked via the Robutler API. This allows subsequent requests to use the same payment source without repeated blockchain transactions.
API Reference
PaymentX402Skill / PaymentSkillX402
import type { PaymentX402Config } from 'webagents/skills/payments';
export class PaymentX402Skill extends Skill {
constructor(config?: PaymentX402Config);
}
interface PaymentX402Config {
facilitatorUrl?: string;
acceptedSchemes?: Array<{ scheme: string; network: string }>;
maxPayment?: number;
// ... see PaymentSkillConfig for inherited options
}Hooks
checkHttpEndpointPayment / check_http_endpoint_payment
import { hook } from 'webagents';
@hook({ lifecycle: 'before_http_call', priority: 10 })
async checkHttpEndpointPayment(data, context) {
// Agent B:
// - Checks if endpoint has @pricing decorator
// - If no X-PAYMENT header: throws PaymentRequiredError
// - If X-PAYMENT present: verifies and settles via facilitator
}Helper Methods (Python)
async def _get_available_token(self, context) -> Optional[str]: ...
async def _create_payment(
self, accepts: List[Dict], context
) -> tuple[str, str, float]: ...
async def _exchange_for_credits(self, amount: float, context) -> str: ...Exceptions
import { PaymentRequiredError } from 'webagents/skills/payments';
// PaymentRequiredError is the TS equivalent of PaymentRequired402.
// It carries the x402 `accepts` payload on `error.requirements`.
// Other errors are reported as standard `Error` instances with
// descriptive messages — fine-grained subclasses are coming soon.Examples
Example 1: Simple Paid API
import { BaseAgent, http, pricing, Skill } from 'webagents';
import { PaymentX402Skill } from 'webagents/skills/payments';
class TranslatorSkill extends Skill {
readonly name = 'translator-skill';
@http({ path: '/translate', method: 'POST' })
@pricing({ creditsPerCall: 0.1, reason: 'Translation service' })
async translate(params: { text: string; target_lang: string }): Promise<unknown> {
return { translated: `[${params.target_lang}] ${params.text}` };
}
}
const agent = new BaseAgent({
name: 'translator',
skills: [new PaymentX402Skill(), new TranslatorSkill()],
});Example 2: Multi-Tier Pricing
import { http, pricing, Skill } from 'webagents';
class ComputeSkill extends Skill {
readonly name = 'compute';
@http({ path: '/compute', method: 'POST' })
@pricing()
async compute(params: { task: unknown; tier?: 'basic' | 'standard' | 'premium' }): Promise<unknown> {
const tier = params.tier ?? 'basic';
const credits = { basic: 0.05, standard: 0.2, premium: 1.0 }[tier];
return {
result: 'computed',
_pricing: { credits, reason: `Computation (${tier})`, metadata: { tier } },
};
}
}Example 3: Consumer Agent with Auto-Exchange
// Coming soon — track at https://github.com/robutlerai/webagents/issues
// Auto-exchange and blockchain wallet payments are Python-only.
import { PaymentX402Skill } from 'webagents/skills/payments';
const consumer = new BaseAgent({
name: 'api-consumer',
skills: [new PaymentX402Skill({ maxPayment: 5.0 })],
});Roborum Payments API (/api/payments/*)
The Roborum platform exposes payment endpoints that implement the x402 flow. Payment tokens are RS256-signed JWTs; they can be verified locally via JWKS or via the verify endpoint.
JWT payment tokens
- Issuer:
https://robutler.ai(orJWT_ISSUER). - Claims:
sub(user id),aud,exp,jti, andpayment: { balance, scheme }. - Verification: Same RS256 public key as auth; fetch from
{iss}/.well-known/jwks.json.
POST /api/payments/lock
Create a payment authorization (lock funds, issue JWT):
// Request
{ "amount": 0.05, "audience": ["agent-id"], "expiresIn": 3600 }
// Response
{ "token": "<JWT>", "expiresAt": "2025-11-01T00:00:00Z", "lockedAmount": 0.05 }POST /api/payments/verify
Validate a payment token (optional when using local JWKS verification):
// Request (body or X-PAYMENT header)
{ "token": "<JWT>" }
// Response
{ "valid": true, "balance": 0.05, "expiresAt": "...", "issuer": "https://robutler.ai" }POST /api/payments/settle
Charge against the token (always required for settlement):
// Request
{ "token": "<JWT>", "amount": 0.05, "recipientId": "agent-id", "description": "API call", "resource": "/path" }
// Response
{ "success": true, "charged": 0.05, "remaining": 0 }GET /api/payments/tokens
List current user's payment tokens (active, expired, depleted).
GET /.well-known/jwks.json
Public keys for JWT verification (RS256). Used by the skill for local verification to avoid a network call when the X-PAYMENT header contains a JWT.
GET /.well-known/ucp
UCP capability manifest (version, issuer, capabilities, endpoints: lock, verify, settle, tokens).
Facilitator API (legacy /api/x402)
The skill also supports the legacy x402 facilitator interface:
POST /x402/verify
Verify payment validity:
// Request
{
"paymentHeader": "<base64>",
"paymentRequirements": {
"scheme": "token",
"network": "robutler",
"maxAmountRequired": "0.05"
}
}
// Response
{
"isValid": true
}POST /x402/settle
Settle verified payment:
// Response
{
"success": true,
"transactionHash": "robutler-tx-1234567890"
}GET /x402/supported
List supported payment schemes:
// Response
{
"schemes": [
{"scheme": "token", "network": "robutler", "description": "..."},
{"scheme": "exact", "network": "base-mainnet", "description": "..."}
]
}GET /x402/exchange
Get exchange rates (Robutler extension):
// Response
{
"supportedOutputTokens": [
{"scheme": "token", "network": "robutler"}
],
"exchangeRates": {
"exact:base-mainnet:USDC": {
"outputScheme": "token",
"rate": "1.0",
"minAmount": "0.01",
"fee": "0.02"
}
}
}POST /x402/exchange
Exchange crypto for credits (Robutler extension):
// Request
{
"paymentHeader": "<base64_blockchain_payment>",
"paymentRequirements": {},
"requestedOutput": {
"scheme": "token",
"network": "robutler",
"amount": "9.80"
}
}
// Response
{
"success": true,
"token": "tok_xxx:secret_yyy",
"amount": "9.80",
"expiresAt": "2025-11-01T00:00:00Z"
}Best Practices
Security
- Never expose private keys: Store wallet private keys in environment variables
- Set max_payment limits: Prevent accidental overpayment
- Validate pricing: Always verify pricing before accepting payments
- Use HTTPS: Never send payment headers over unencrypted connections
Performance
- Token reuse: Existing tokens are cached and reused when possible
- Async operations: All payment operations are async for non-blocking execution
- Connection pooling: HTTP client uses connection pooling for efficiency
Error Handling
import { PaymentRequiredError } from 'webagents/skills/payments';
try {
const result = await agent.callEndpoint('/paid-api');
} catch (err) {
if (err instanceof PaymentRequiredError) {
console.log('Payment required:', (err as PaymentRequiredError).requirements);
} else {
console.error('Payment error:', (err as Error).message);
}
}Comparison with PaymentSkill
| Feature | PaymentSkill | PaymentSkillX402 |
|---|---|---|
| Tool charging | ✅ Yes | ✅ Yes (inherited) |
| Cost calculation | ✅ Yes | ✅ Yes (inherited) |
| HTTP endpoint payments | ❌ No | ✅ Yes |
| x402 protocol | ❌ No | ✅ Yes |
| Multiple payment schemes | ❌ No | ✅ Yes |
| Blockchain payments | ❌ No | ✅ Yes (optional) |
| Crypto exchange | ❌ No | ✅ Yes |
| Automatic payment | ❌ No | ✅ Yes |
UAMP Payment Flow
When agents are accessed over UAMP (WebSocket), payment negotiation happens inline using UAMP payment events instead of HTTP 402 responses. The flow is:
- Client sends
input.text(orresponse.create) - Agent skill raises
PaymentTokenRequiredError(no token in context) - UAMP transport catches the error and sends
payment.requiredevent to client - Client obtains a payment token (e.g. calls
/api/payments/lockon Roborum) - Client sends
payment.submitevent with the token - UAMP transport sets
context.payment_tokenand retriesprocess_uamp - Agent processes successfully, streams
response.delta/response.done - UAMP transport sends
payment.acceptedwith remaining balance
Mid-Stream UAMP Token Top-Up
When a lock's balance is insufficient mid-turn (e.g., an expensive tool call or long LLM generation), the transport triggers a top-up flow:
- Agent (or native LLM skill) detects the token balance is too low for the next charge.
- UAMP transport sends
payment.requiredwithextra.action: "topup"and the additionalamountneeded. - Client calls
POST /api/payments/tokens/{id}/topupto add funds to the existing token. - Client sends
payment.submitwith the updated token (samejti, higher balance). - Transport resumes processing with the topped-up token — no retry, no lost state.
The top-up flow uses wait_for_event("payment.submit") to block the agent's execution until the client responds, preserving streaming state.
UAMP Payment Events
| Event | Direction | Description |
|---|---|---|
payment.required | Server → Client | Agent needs payment; includes requirements.amount, requirements.currency, requirements.schemes |
payment.submit | Client → Server | Client provides token via payment.scheme, payment.amount, payment.token |
payment.accepted | Server → Client | Payment verified; includes payment_id, balance_remaining |
payment.balance | Server → Client | Balance update notification (low balance warning) |
payment.error | Server → Client | Payment failed; includes code, message, can_retry |
Pre-loading tokens via session.update
Clients can pre-load a payment token before sending any input:
{
"type": "session.update",
"session": {
"payment_token": "eyJhbGciOiJSUzI1NiIsI..."
}
}This sets context.payment_token so the first request doesn't trigger payment.required.
Daemon bridge (Roborum → Agent)
When Roborum routes messages to agents via the UAMP daemon bridge:
- Router calls
sendInputToAgentSession()withsenderIdandagentId - If daemon returns
payment.required, WS server callsfindOrCreatePaymentToken(senderId, { audience: [agentId] }) - WS server sends
payment.submitback to daemon with the JWT token - Daemon retries the agent with the token in context
- On success, daemon sends
response.done; on payment failure,payment.error
Wiring it up
import { BaseAgent } from 'webagents';
import { PaymentX402Skill } from 'webagents/skills/payments';
import { PortalTransportSkill } from 'webagents/skills/transport';
// Portal transport handles payment.required/submit/accepted over WS.
const agent = new BaseAgent({
name: 'paid-agent',
skills: [
new PortalTransportSkill(),
new PaymentX402Skill(),
],
});See Also
- PaymentSkill - Basic payment integration
- Transport Payment Handling - Per-transport payment behavior
- x402 Protocol - Official specification
- Robutler Platform - Platform documentation
- WebAgents Documentation - Main documentation