x402 Protocol¶
This guide covers the x402 payment protocol for verifying permissions and settling payments.
Overview¶
x402 is a payment protocol that enables:
- Permission Generation: Subscribers create access tokens for agents
- Permission Verification: Agents verify tokens without burning credits
- Permission Settlement: Agents burn credits after completing work
The protocol is named after HTTP status code 402 (Payment Required).
Supported Schemes¶
Nevermined supports two x402 payment schemes:
| Scheme | Network | Use Case | Settlement |
|---|---|---|---|
nvm:erc4337 |
eip155:84532 |
Crypto payments | ERC-4337 UserOps + session keys |
nvm:card-delegation |
stripe | braintree | visa |
Fiat/credit card | Provider charge + credit burn |
The scheme is determined by the plan's pricing configuration. Plans with isCrypto: false use nvm:card-delegation; all others use nvm:erc4337. The SDK auto-detects the scheme via resolve_scheme(). The network value within nvm:card-delegation is determined by which provider issued the delegation being consumed (stripe, braintree, or visa).
Visa support¶
Visa delegations use the same nvm:card-delegation scheme and SDK surface as Stripe and Braintree, but two steps must happen in a browser before the SDK can consume them:
- Card enrolment — the cardholder enrols a Visa card through VGS Collect (PCI-compliant iframe) in the Nevermined webapp. The card is bound to a Visa Agentic Token via the VGS Credential Management Platform.
- Delegation creation — the cardholder approves a delegation via a WebAuthn/passkey (FIDO) device-binding ceremony embedded by Visa VTS. This produces a single-use
assuranceDatablob bound to the spending limit + duration + merchant context.
Both steps require a real DOM and a user gesture, so the SDK cannot perform them programmatically. Once a Visa delegation exists, the SDK consumes it identically to Stripe/Braintree — pass delegation_id to DelegationConfig and call get_x402_access_token as usual:
from payments_py.x402 import X402TokenOptions, DelegationConfig
result = subscriber_payments.x402.get_x402_access_token(
plan_id,
token_options=X402TokenOptions(
scheme="nvm:card-delegation",
network="visa",
delegation_config=DelegationConfig(
delegation_id="11111111-1111-1111-1111-111111111111",
),
),
)
delegation_id reuse is the only supported pattern for Visa — create_delegation(provider="visa", ...) is rejected by the backend without the browser-only consumer_prompt + assurance_data blobs the SDK has no way to produce. When a Visa creation call fails this way, PaymentsError.code carries the backend BCK.VISA.0014 so consumers can branch programmatically.
Generate Payment Permissions¶
From Nevermined App¶
The easiest way to generate permissions is through the Nevermined App Permissions page:
- Navigate to the permissions page
- Select your plan and agent
- Configure limits (optional)
- Generate the access token
From SDK¶
from payments_py import Payments, PaymentOptions
payments = Payments.get_instance(
PaymentOptions(nvm_api_key="nvm:subscriber-key", environment="sandbox")
)
# Basic token generation
result = payments.x402.get_x402_access_token(
plan_id="your-plan-id",
agent_id="agent-id"
)
access_token = result['accessToken']
# With delegation config (crypto — erc4337)
from payments_py.x402 import X402TokenOptions, DelegationConfig, CreateDelegationPayload
# Pattern A — auto-create delegation
result = payments.x402.get_x402_access_token(
plan_id="your-plan-id",
agent_id="agent-id",
token_options=X402TokenOptions(
delegation_config=DelegationConfig(
spending_limit_cents=10000, # $100
duration_secs=604800 # 1 week
)
)
)
# Pattern B — explicit create + reuse across plans
delegation = payments.delegation.create_delegation(
CreateDelegationPayload(
provider="erc4337",
spending_limit_cents=10000,
duration_secs=604800
)
)
result = payments.x402.get_x402_access_token(
plan_id="your-plan-id",
agent_id="agent-id",
token_options=X402TokenOptions(
delegation_config=DelegationConfig(delegation_id=delegation.delegation_id)
)
)
Card-Delegation Token Generation¶
For fiat plans using nvm:card-delegation, pass X402TokenOptions with a DelegationConfig:
from payments_py.x402 import X402TokenOptions, DelegationConfig
# USD card delegation
result = payments.x402.get_x402_access_token(
plan_id="your-plan-id",
agent_id="agent-id",
token_options=X402TokenOptions(
scheme="nvm:card-delegation",
delegation_config=DelegationConfig(
provider_payment_method_id="pm_1AbCdEfGhIjKlM",
spending_limit_cents=10000, # $100.00
duration_secs=2592000, # 30 days
currency="usd",
max_transactions=100
)
)
)
access_token = result['accessToken']
# EUR card delegation
eur_result = payments.x402.get_x402_access_token(
plan_id="your-plan-id",
agent_id="agent-id",
token_options=X402TokenOptions(
scheme="nvm:card-delegation",
delegation_config=DelegationConfig(
provider_payment_method_id="pm_1AbCdEfGhIjKlM",
spending_limit_cents=10000, # €100.00 (in euro cents)
duration_secs=2592000, # 30 days
currency="eur",
max_transactions=100
)
)
)
Auto Scheme Resolution¶
Use resolve_scheme() to auto-detect the correct scheme from plan metadata:
from payments_py.x402.resolve_scheme import resolve_scheme
# Auto-detect scheme from plan metadata (cached for 5 minutes)
scheme = resolve_scheme(payments, plan_id="your-plan-id")
# Returns "nvm:erc4337" for crypto plans, "nvm:card-delegation" for fiat plans
# Explicit override
scheme = resolve_scheme(payments, plan_id="your-plan-id", explicit_scheme="nvm:card-delegation")
DelegationAPI¶
Create delegations and list enrolled payment methods:
from payments_py.x402 import CreateDelegationPayload
# Create a crypto delegation
delegation = payments.delegation.create_delegation(
CreateDelegationPayload(
provider="erc4337",
spending_limit_cents=10000,
duration_secs=604800
)
)
print(f"Delegation ID: {delegation.delegation_id}")
# List enrolled payment methods (every provider)
methods = payments.delegation.list_payment_methods()
for method in methods:
print(f"{method.brand} ****{method.last4} (expires {method.exp_month}/{method.exp_year})")
# e.g., "visa ****4242 (expires 12/2027)"
# Restrict the result to a single provider with the optional `provider` kwarg
stripe_methods = payments.delegation.list_payment_methods(provider="stripe")
list_payment_methods() accepts an optional provider keyword argument
('stripe' | 'braintree' | 'visa' | 'erc4337'). When set, it is forwarded as a
?provider= query string and only methods backed by that provider are returned.
Omit it (the default) to return methods from every provider.
PaymentMethodSummary fields:
| Field | Type | Description |
|---|---|---|
id |
str |
Payment method ID (e.g., pm_...) |
brand |
str |
Card brand (e.g., visa, mastercard) |
last4 |
str |
Last 4 digits of the card number |
exp_month |
int |
Card expiration month |
exp_year |
int |
Card expiration year |
Token Structure¶
The x402 token is a base64-encoded JSON document:
{
"payload": {
"authorization": {
"from": "0xSubscriberAddress",
"planId": "plan-123",
"agentId": "agent-456"
},
"sessionKey": {
"address": "0xSessionKeyAddress",
"permissions": ["order", "burn"],
"limits": {
"redemptionLimit": 100,
"orderLimit": "1000000000000000000"
}
}
},
"signature": "0x..."
}
Verify Payment Permissions¶
Verification checks if a subscriber has valid permissions without burning credits:
from payments_py import Payments, PaymentOptions
from payments_py.x402.helpers import build_payment_required
payments = Payments.get_instance(
PaymentOptions(nvm_api_key="nvm:agent-key", environment="sandbox")
)
# Build the 402 Payment Required specification
payment_required = build_payment_required(
plan_id="your-plan-id",
endpoint="https://your-api.com/endpoint",
agent_id="your-agent-id",
http_verb="POST"
)
# Verify the token
verification = payments.facilitator.verify_permissions(
payment_required=payment_required,
x402_access_token=access_token,
max_amount="1" # Optional: max credits to verify
)
if verification.is_valid:
print(f"Valid! Payer: {verification.payer}")
else:
print(f"Invalid: {verification.invalid_reason}")
Verification Response¶
| Field | Type | Description |
|---|---|---|
is_valid |
bool |
Whether verification passed |
invalid_reason |
str |
Reason for invalidity (if is_valid is false) |
payer |
str |
Payer's wallet address |
agent_request_id |
str |
Agent request ID for observability tracking |
Settle Payment Permissions¶
Settlement burns credits after successfully processing a request:
# After processing the request successfully
settlement = payments.facilitator.settle_permissions(
payment_required=payment_required,
x402_access_token=access_token,
max_amount="1", # Credits to burn
agent_request_id="request-123" # Optional: for tracking
)
if settlement.success:
print(f"Settled! Credits burned: {settlement.credits_redeemed}")
print(f"Transaction: {settlement.transaction}")
print(f"Remaining: {settlement.remaining_balance}")
else:
print(f"Settlement failed: {settlement.error_reason}")
Settlement Response¶
| Field | Type | Description |
|---|---|---|
success |
bool |
Whether settlement succeeded |
error_reason |
str |
Reason for failure (if success is false) |
payer |
str |
Payer's wallet address |
transaction |
str |
Blockchain transaction hash |
credits_redeemed |
str |
Credits that were burned |
remaining_balance |
str |
Credits remaining |
Payment Required Object¶
The X402PaymentRequired object specifies what payment is required. The scheme and network fields vary by payment type:
from payments_py.x402.types import X402PaymentRequired, X402Resource, X402Scheme, X402SchemeExtra
# Crypto plan (nvm:erc4337)
payment_required = X402PaymentRequired(
x402_version=2,
resource=X402Resource(
url="https://your-api.com/endpoint",
description="Protected endpoint" # Optional
),
accepts=[
X402Scheme(
scheme="nvm:erc4337",
network="eip155:84532", # Base Sepolia
plan_id="your-plan-id",
extra=X402SchemeExtra(
http_verb="POST", # HTTP method goes in extra, not resource
agent_id="agent-123" # Optional
)
)
],
extensions={}
)
# Fiat plan (nvm:card-delegation)
payment_required_fiat = X402PaymentRequired(
x402_version=2,
resource=X402Resource(
url="https://your-api.com/endpoint",
description="Protected endpoint"
),
accepts=[
X402Scheme(
scheme="nvm:card-delegation",
network="stripe",
plan_id="your-plan-id",
extra=X402SchemeExtra(
http_verb="POST",
agent_id="agent-123"
)
)
],
extensions={}
)
Using the Helpers¶
from payments_py.x402.helpers import build_payment_required, build_payment_required_for_plans
# Single plan (scheme auto-detected from plan metadata when omitted)
payment_required = build_payment_required(
plan_id="your-plan-id",
endpoint="https://api.example.com/tasks",
agent_id="agent-123",
http_verb="POST"
)
# Explicit scheme override
payment_required = build_payment_required(
plan_id="your-plan-id",
endpoint="https://api.example.com/tasks",
agent_id="agent-123",
http_verb="POST",
scheme="nvm:card-delegation" # Force fiat/Stripe scheme
)
# Multiple plans — creates one entry per plan in accepts[]
payment_required = build_payment_required_for_plans(
plan_ids=["plan-basic", "plan-premium"],
endpoint="https://api.example.com/tasks",
agent_id="agent-123",
http_verb="POST"
)
For a single plan, build_payment_required_for_plans delegates to build_payment_required internally. When scheme is omitted, the network defaults to eip155:84532 (Base Sepolia). When scheme="nvm:card-delegation", the network is automatically set to stripe.
Complete Workflow Example¶
from payments_py import Payments, PaymentOptions
from payments_py.x402.helpers import build_payment_required
from flask import Flask, request, jsonify
app = Flask(__name__)
# Agent's payments instance
agent_payments = Payments.get_instance(
PaymentOptions(nvm_api_key="nvm:agent-key", environment="sandbox")
)
PLAN_ID = "your-plan-id"
AGENT_ID = "your-agent-id"
@app.route('/api/process', methods=['POST'])
def process_request():
# 1. Extract x402 token from payment-signature header
token = request.headers.get('payment-signature', '')
if not token:
return jsonify({'error': 'Missing payment-signature header'}), 402
# 2. Build payment requirement
payment_required = build_payment_required(
plan_id=PLAN_ID,
endpoint=request.url,
agent_id=AGENT_ID,
http_verb=request.method
)
# 3. Verify (doesn't burn credits)
verification = agent_payments.facilitator.verify_permissions(
payment_required=payment_required,
x402_access_token=token,
max_amount="1"
)
if not verification.is_valid:
return jsonify({
'error': 'Payment required',
'details': verification.invalid_reason,
'paymentRequired': payment_required.model_dump()
}), 402
# 4. Process the request
try:
result = do_expensive_work(request.json)
except Exception as e:
# Don't settle on failure
return jsonify({'error': str(e)}), 500
# 5. Settle (burn credits) on success
settlement = agent_payments.facilitator.settle_permissions(
payment_required=payment_required,
x402_access_token=token,
max_amount="1"
)
return jsonify({
'result': result,
'creditsUsed': settlement.credits_redeemed,
'remainingBalance': settlement.remaining_balance
})
def do_expensive_work(data):
# Your processing logic
return {'processed': True}
if __name__ == '__main__':
app.run(port=8080)
HTTP Flow¶
sequenceDiagram
participant Subscriber
participant Agent
participant Nevermined
Subscriber->>Agent: GET /api/process (no token)
Agent-->>Subscriber: 402 Payment Required<br/>{paymentRequired: {...}}
Subscriber->>Nevermined: get_x402_access_token()
Nevermined-->>Subscriber: {accessToken: "..."}
Subscriber->>Agent: GET /api/process<br/>payment-signature: token
Agent->>Nevermined: verify_permissions()
Nevermined-->>Agent: {isValid: true}
Note over Agent: Process request
Agent->>Nevermined: settle_permissions()
Nevermined-->>Agent: {success: true}
Agent-->>Subscriber: 200 OK {result: ...}
Best Practices¶
-
Always verify before processing: Don't do expensive work without verification
-
Only settle on success: Don't burn credits if processing fails
-
Use agent_request_id: Include request IDs for tracking and debugging
-
Handle 402 responses: Return proper payment required responses with scheme info
-
Cache verifications carefully: Tokens can be used multiple times until limits are reached
Error Codes¶
| Error | Description | Resolution |
|---|---|---|
invalid_token |
Token is malformed | Generate a new token |
expired_token |
Token has expired | Generate a new token |
insufficient_balance |
Not enough credits | Order more credits |
invalid_plan |
Plan ID mismatch | Use correct plan ID |
invalid_agent |
Agent ID mismatch | Use correct agent ID |
Next Steps¶
- Request Validation - More validation patterns
- MCP Integration - x402 with MCP servers
- MCP OAuth and x402 Discovery - Experimental MCP payment discovery metadata