---
title: "X402 Protocol"
description: "Understand the X402 payment protocol for HTTP-based AI agent authentication and authorization"
icon: "file-contract"
---

# X402 Protocol

The X402 protocol is a payment-aware HTTP extension that enables AI agents to require and verify payments for API access. This guide provides a complete overview of X402 implementation in the Nevermined Payments Library.

> 🔐 **Bearer-token hygiene.** The `payment-signature` header is a bearer credential — anyone holding it can spend the associated plan's credits until it expires. Send it only over HTTPS, never log the full value, and configure your log/trace exporters (pino, winston, OpenTelemetry) to redact `payment-signature`, `authorization`, and `cookie` headers by default.

## Overview of X402

X402 is an HTTP-based protocol for payment-protected resources:
- **402 Payment Required**: HTTP status code for payment requests
- **payment-signature**: Header containing payment credentials (X402 v2)
- **PAYMENT-REQUIRED**: Header with payment requirements in 402 responses
- **PAYMENT-RESPONSE**: Header with payment settlement details in success responses
- **Cryptographic Signatures**: ERC-4337 account abstraction for secure payments

## 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 `resolveScheme()`. 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:

1. **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.
2. **Delegation creation** — the cardholder approves a delegation via a WebAuthn/passkey (FIDO) device-binding ceremony embedded by Visa VTS. This produces a single-use `assuranceData` blob 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 — see [Reusing Existing Delegations](#reusing-existing-delegations).

## X402 Version 2 Specification

The Nevermined Payments Library implements X402 v2, which uses:
- `payment-signature` header for access tokens (replaces Authorization)
- `PAYMENT-REQUIRED` header for payment requirements (replaces custom formats)
- `PAYMENT-RESPONSE` header for settlement receipts
- Structured payment credentials with cryptographic signatures

## Generate X402 Access Tokens

The supported flow is **create-first**: create a delegation once with
`payments.delegation.createDelegation(...)`, then request access tokens by
passing only its `delegationId`. **Both schemes (`nvm:erc4337` and
`nvm:card-delegation`) require a `delegationConfig`** — the older
"just pass planId + agentId" shape no longer mints a token and surfaces
`BCK.X402.0030` ("Required token-generation input is missing or incomplete";
the `details` field names the missing input) at the backend.

```typescript
import { Payments, EnvironmentName, X402TokenOptions } from '@nevermined-io/payments'

const subscriberPayments = Payments.getInstance({
  nvmApiKey: process.env.SUBSCRIBER_API_KEY!,
  environment: 'sandbox' as EnvironmentName,
})

// 1. Create a delegation once. `currency` is required: 'usdc' for the
//    crypto (erc4337) scheme, 'usd'/'eur'/… for card providers.
const { delegationId } = await subscriberPayments.delegation.createDelegation({
  provider: 'erc4337',
  spendingLimitCents: 1000, // $10 cap
  durationSecs: 86_400, // 1 day
  currency: 'usdc',
})

// 2. Request a token by delegationId. Reuse this delegation for every
//    subsequent token request until it expires or is exhausted.
const tokenOptions: X402TokenOptions = {
  scheme: 'nvm:erc4337',
  delegationConfig: { delegationId },
}

const { accessToken } = await subscriberPayments.x402.getX402AccessToken(
  planId, // Required: Plan ID
  agentId, // Optional: Agent ID (restricts token to specific agent)
  tokenOptions, // Required: scheme + delegationConfig
)

console.log('X402 access token generated')
```

> ⚠️ **Inline create-on-the-fly is deprecated.** Passing creation fields
> (`spendingLimitCents`, `durationSecs`, `providerPaymentMethodId`, `cardId`,
> `currency`, `merchantAccountId`, `maxTransactions`) directly in
> `delegationConfig` instead of a `delegationId` still works, but
> `getX402AccessToken` now emits a runtime deprecation warning. Create the
> delegation first (above) and pass only `{ delegationId }`.

### Single-Use, Seller-Bound Tokens (v3)

A v2 access token is a **reusable bearer credential**: its EIP-712 signature
covers `[from, sessionKeysProvider, sessionKeys, planId]` only, so the same
token settles more than once and can be presented to any seller on that plan.

Token **v3** closes both holes. It appends `agentId`, `resourceUrl`, `httpVerb`
and a one-time `nonce` to the signed struct, which makes the token:

- **Bound to one seller resource** — it only settles against the `resource.url`
  and `httpVerb` it was minted for.
- **Single-use** — it is consumed by the **first** `POST /x402/settle`. A second
  settlement of the same token fails with `BCK.X402.0059`. `verifyPermissions`
  never consumes a token, so the standard verify-then-settle flow is unchanged
  and verification stays repeatable.

v3 is **opt-in**: pass `tokenVersion: 3` along with the resource binding. The
resource must be the string the seller advertises — read it off the seller's
402 rather than reconstructing it (see the matching rule below):

```typescript
import { detectAccessTokenVersion } from '@nevermined-io/payments'

// From the seller's 402 `payment-required` header (base64 X402PaymentRequired).
const sellerResourceUrl = paymentRequired.resource.url // e.g. '/api/v1/tasks'

const { accessToken, tokenVersion } = await subscriberPayments.x402.getX402AccessToken(
  planId,
  agentId,
  {
    delegationConfig: { delegationId },
    resource: { url: sellerResourceUrl },
    httpVerb: 'POST',
    tokenVersion: 3,
  },
)

if (tokenVersion === 3) {
  // Single-use: spend it on exactly one paid request, then mint another.
}
```

> ⚠️ **Never assume you got the version you asked for.** A backend that predates
> v3 support drops `tokenVersion` from the request without an error and mints a
> v2 token. Always read the version off the token you received — that is what
> `getX402AccessToken` returns as `tokenVersion`, and what
> `detectAccessTokenVersion(accessToken)` reports for a token from anywhere else.

**The resource URL must match the seller's, verbatim.** The backend compares the
token's `resource.url` with the `resource.url` in the seller's `paymentRequired`
— origin + path, query string ignored — and falls back to an exact string
comparison for anything it cannot parse as a URL. A mismatch is rejected with
`BCK.X402.0013`. This comparison applies to **any** token that carries a
resource, not just v3, so bind only to the exact string the seller advertises.
Sellers built with this SDK's Express middleware advertise `req.originalUrl`
(a **relative** path such as `/ask`), so a buyer paying such a seller must mint
with that same relative string — not the absolute URL it is fetching.

Because the check is version-independent, attaching a resource to a **v2** mint
can only cost you a failed verification: it binds nothing, while still arming the
backend's endpoint allowlist. The SDK therefore **refuses** `resource`/`httpVerb`
without `tokenVersion: 3` — as does the Python SDK — the CLI rejects the flag
combination, and the OpenClaw tools drop it. (Once the backend default becomes
v3, an omitted version will be accepted with a binding; an explicit `2` will
stay refused.)

**Caching rule.** A v2 token may be cached and replayed for as long as it is
valid. A v3 token must be **minted per paid request** — reusing one settles
against a spent nonce. The SDK's own clients follow this rule: `PaymentsClient`
(A2A) caches a v2 token for the client's lifetime, and mints a fresh token for
every paid call as soon as the token comes back as v3. Opt in with the
`tokenVersion` argument of `PaymentsClient.create` (or `ClientRegistryOptions`,
where it is part of the cache key), which also binds the token to the agent's
service endpoint — and fails the mint rather than returning an unbound token if
that endpoint cannot be resolved.

**Handling a spent token.** `BCK.X402.0059` is neither a decline nor a forgery
(`BCK.X402.0005`) — the token was valid and has already been settled once. The
only correct response is to mint a new token; retrying with the same one fails
identically forever.

```typescript
import { isAccessTokenAlreadyUsed } from '@nevermined-io/payments'

try {
  await payments.facilitator.settlePermissions({ paymentRequired, x402AccessToken })
} catch (error) {
  if (isAccessTokenAlreadyUsed(error)) {
    // Mint a new token — do NOT replay this one.
  }
  throw error
}
```

**MPP carries no token version at all.** The two protocols no longer share a
version ladder: MPP's single-use unit is the **challenge**, not the token, and
one MPP access token is presented across many challenges by design — an x402 v3
per-token nonce would reject every buyer's second challenge. `payments.mpp.*`
therefore takes `MppTokenOptions` (`X402TokenOptions` without `tokenVersion`),
and passing a version anyway is refused before the request; the backend answers
`BCK.MPP.0007` for **any** value, `2` included.

### Generate Tokens via Nevermined App

Users can also generate X402 access tokens through the Nevermined App:

1. Visit [nevermined.app/permissions/agent-permissions](https://nevermined.app/permissions/agent-permissions)
2. Select the plan you've purchased
3. Configure token parameters (agent, expiration, limits)
4. Generate and copy the X402 access token
5. Use the token in API requests

This provides a user-friendly interface for non-technical users to generate tokens without code.

### Generate Card-Delegation Tokens

For fiat plans using `nvm:card-delegation`, create the delegation first with
`createDelegation` (passing the card's `providerPaymentMethodId`), then request
tokens by its `delegationId`. `currency` is required on creation:

```typescript
import { X402TokenOptions } from '@nevermined-io/payments'

// 1. Create a USD card delegation once.
const { delegationId: usdDelegationId } =
  await subscriberPayments.delegation.createDelegation({
    provider: 'stripe',
    providerPaymentMethodId: 'pm_1AbCdEfGhIjKlM',
    spendingLimitCents: 10000, // $100.00
    durationSecs: 2592000, // 30 days
    currency: 'usd',
    maxTransactions: 100,
  })

// 2. Request a token by delegationId (reuse for every subsequent request).
const tokenOptions: X402TokenOptions = {
  scheme: 'nvm:card-delegation',
  delegationConfig: { delegationId: usdDelegationId },
}

const { accessToken } = await subscriberPayments.x402.getX402AccessToken(
  planId,
  agentId,
  tokenOptions,
)
```

A EUR delegation is the same call with `currency: 'eur'`. The delegation's
currency is fixed at creation, so you create one delegation per currency and
reuse each by its `delegationId`.

### Reusing Existing Delegations

Passing `delegationConfig: { delegationId }` is the supported pattern for every
provider, and the **only** supported pattern for Visa delegations (which cannot
be created from the SDK). Reuse one delegation across multiple agents that share
a single spending budget:

```typescript
const tokenOptions: X402TokenOptions = {
  scheme: 'nvm:card-delegation',
  delegationConfig: {
    delegationId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  }
}

const { accessToken } = await subscriberPayments.x402.getX402AccessToken(
  planId,
  agentId,
  tokenOptions,
)
```

When `delegationId` is provided, the backend verifies that the delegation is active and that the requesting API key has access, then returns its existing token without creating a new delegation. The provider (`stripe`, `braintree`, or `visa`) is inferred from the delegation record on the server.

To scope a token request to a specific NVM API key, add `apiKeyId` alongside
`delegationId` — both fields stay active (non-deprecated):

```typescript
delegationConfig: { delegationId: '…', apiKeyId: 'key-uuid' }
```

### Deprecated: inline create-on-the-fly

> ⚠️ **Deprecated.** Supplying creation fields directly in `delegationConfig`
> instead of a `delegationId` (e.g. `providerPaymentMethodId` +
> `spendingLimitCents`, a bare `cardId`, or an identifier-less "auto-select"
> shape) still creates a delegation on the fly, but `getX402AccessToken` now
> emits a runtime deprecation warning. Use the create-first pattern above
> (`createDelegation` → `{ delegationId }`); inline creation will be removed in
> a future major release.

### CardDelegationConfig Reference

| Field | Type | Status | Description |
|-------|------|--------|-------------|
| `delegationId` | `string` | Active | Existing delegation UUID to reuse — the supported path |
| `apiKeyId` | `string` | Active | NVM API Key ID to scope the token request to |
| `cardId` | `string` | Deprecated | Payment method UUID to target (inline create) |
| `providerPaymentMethodId` | `string` | Deprecated | Stripe payment method ID (e.g., `pm_...`) (inline create) |
| `spendingLimitCents` | `number` | Deprecated | Max spending in cents (inline create) |
| `durationSecs` | `number` | Deprecated | Duration in seconds (inline create) |
| `currency` | `string` | Deprecated | Currency code (inline create) |
| `planId` | `string` | Deprecated | Plan ID to scope a new delegation to (inline create) |
| `merchantAccountId` | `string` | Deprecated | Stripe Connect account ID (inline create) |
| `maxTransactions` | `number` | Deprecated | Max transactions allowed (inline create) |

The deprecated fields apply only to inline create-on-the-fly. Create the
delegation with [`createDelegation`](#createdelegation) and pass only
`delegationId` (optionally `apiKeyId`) here.

### Auto Scheme Resolution

Use `resolveScheme()` to auto-detect the correct scheme from plan metadata:

```typescript
import { resolveScheme } from '@nevermined-io/payments'

// Auto-detect scheme from plan metadata (cached for 5 minutes)
const scheme = await resolveScheme(payments, planId)
// Returns "nvm:erc4337" for crypto plans, "nvm:card-delegation" for fiat plans

// Explicit override
const scheme = await resolveScheme(payments, planId, 'nvm:card-delegation')
```

### DelegationAPI

Manage payment methods and delegations for card delegation:

```typescript
import { DelegationAPI, PaymentMethodSummary } from '@nevermined-io/payments'

const delegationApi = DelegationAPI.getInstance(payments.options)

// List enrolled payment methods (every provider)
const methods: PaymentMethodSummary[] = await delegationApi.listPaymentMethods()

// Restrict the result to a single provider with the optional `provider` filter
const stripeMethods: PaymentMethodSummary[] = await delegationApi.listPaymentMethods({
  provider: 'stripe',
})

for (const method of methods) {
  console.log(`${method.brand} ****${method.last4} (expires ${method.expMonth}/${method.expYear})`)
  if (method.allowedApiKeyIds) {
    console.log(`  Restricted to API keys: ${method.allowedApiKeyIds.join(', ')}`)
  }
}
```

`listPaymentMethods` accepts an optional `ListOptions`:

| Option | Type | Description |
|--------|------|-------------|
| `provider` | `DelegationProvider` (`'stripe' \| 'braintree' \| 'visa' \| 'erc4337'`) | Server-side filter that restricts the result to payment methods backed by this provider. Omit it (the default) to return methods from every provider. |
| `accessible` | `boolean` | When `true`, return only methods accessible to the requesting API key. |

#### Update Payment Method

Restrict a card to specific NVM API Keys so only designated agents can use it:

```typescript
await delegationApi.updatePaymentMethod('pm-uuid-here', {
  alias: 'Production Card',
  allowedApiKeyIds: ['sk-agent-1', 'sk-agent-2'],  // Only these API keys can use this card
})

// Remove restrictions (any API key can use the card)
await delegationApi.updatePaymentMethod('pm-uuid-here', {
  allowedApiKeyIds: null,
})
```

#### List Delegations

Retrieve all delegations for the authenticated user:

```typescript
const { delegations, totalResults } = await delegationApi.listDelegations()

console.log(`Total delegations: ${totalResults}`)
for (const d of delegations) {
  console.log(`${d.delegationId} - ${d.status} (${d.remainingBudgetCents} cents remaining)`)
  if (d.apiKeyId) {
    console.log(`  Restricted to API key: ${d.apiKeyId}`)
  }
}
```

#### createDelegation

Create a delegation once, then reuse it across token requests by its
`delegationId` (the create-first pattern). This is the supported way to
provision spending authority for `nvm:erc4337` and `nvm:card-delegation` —
prefer it over inline create-on-the-fly in `getX402AccessToken`.

```typescript
// Crypto (erc4337): currency must be 'usdc'.
const { delegationId } = await delegationApi.createDelegation({
  provider: 'erc4337',
  spendingLimitCents: 100000, // $1000 cap
  durationSecs: 604800, // 1 week
  currency: 'usdc',
})

// Card (stripe / braintree): pass the enrolled card's providerPaymentMethodId.
const { delegationId: cardDelegationId } = await delegationApi.createDelegation({
  provider: 'stripe',
  providerPaymentMethodId: 'pm_1AbCdEfGhIjKlM',
  spendingLimitCents: 10000,
  durationSecs: 2592000,
  currency: 'usd',
})
```

`CreateDelegationPayload` fields:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | `'stripe' \| 'braintree' \| 'visa' \| 'erc4337'` | Yes | Delegation provider |
| `spendingLimitCents` | `number` | Yes | Maximum spending limit in cents |
| `durationSecs` | `number` | Yes | Delegation lifetime in seconds |
| `currency` | `DelegationCurrency` (`'usd' \| 'eur' \| 'usdc' \| 'eurc'`) | Yes | Currency code — `'usdc'`/`'eurc'` for erc4337, `'usd'`/`'eur'` for card providers. No silent default; the backend requires it. |
| `providerPaymentMethodId` | `string` | Card only | Payment method ID (Stripe `pm_...`, Braintree vault token, Visa Agentic token id) |
| `planId` | `string` | No | Optional and additive — delegations are plan-agnostic by default; supply `planId` to bind the delegation to a single plan. (Visa is always plan-specific and requires it.) |
| `merchantAccountId` | `string` | No | Stripe Connect account ID or Braintree merchantId |
| `maxTransactions` | `number` | No | Maximum number of transactions allowed |
| `apiKeyId` | `string` | No | NVM API Key ID to scope the delegation to |

> Visa delegations require a browser-side device-binding ceremony and cannot be
> created from the SDK — see [Visa support](#visa-support). Create them in the
> Nevermined webapp, then consume them here by `delegationId`.

#### DelegationListResponse Fields

`listDelegations()` resolves to a `DelegationListResponse` object:

| Field | Type | Description |
|-------|------|-------------|
| `delegations` | `DelegationSummary[]` | Array of delegation records |
| `totalResults` | `number` | Total number of delegations |
| `page` | `number` | Current page number |
| `offset` | `number` | Offset into the full result set |

#### DelegationSummary Fields

Each item in the `delegations` array is a `DelegationSummary`:

| Field | Type | Description |
|-------|------|-------------|
| `delegationId` | `string` | Unique delegation identifier |
| `provider` | `string` | Payment provider (`stripe`, `braintree`, or `visa`) |
| `providerPaymentMethodId` | `string` | Provider-side payment method ID |
| `status` | `string` | Delegation status (`Active`, `Revoked`, etc.) |
| `spendingLimitCents` | `string` | Maximum spendable amount in cents |
| `amountSpentCents` | `string` | Amount already spent in cents |
| `remainingBudgetCents` | `string` | Remaining budget in cents |
| `currency` | `string` | Currency code (e.g., `usd`) |
| `transactionCount` | `number` | Number of transactions made |
| `expiresAt` | `string` | Expiry timestamp (ISO 8601) |
| `createdAt` | `string` | Creation timestamp (ISO 8601) |
| `apiKeyId` | `string \| null` | API key this delegation is restricted to, or `null` if unrestricted |

#### PaymentMethodSummary Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Payment method ID (e.g., `pm_...`) |
| `brand` | `string` | Card brand (e.g., `visa`, `mastercard`) |
| `last4` | `string` | Last 4 digits of the card number |
| `expMonth` | `number` | Card expiration month |
| `expYear` | `number` | Card expiration year |
| `allowedApiKeyIds` | `string[] \| null` | API keys allowed to use this card (`null` = unrestricted) |

## X402 Access Token Structure

The access token is a JWT containing an X402 v2 payment credential:

```json
{
  "x402Version": 2,
  "accepted": {
    "scheme": "nvm:erc4337",           // Payment scheme
    "network": "eip155:84532",         // Blockchain network (Base Sepolia)
    "planId": "plan-123",              // Payment plan ID
    "extra": {
      "version": "1",                  // Extra version info
      "agentId": "did:nv:agent-456"    // Optional agent restriction
    }
  },
  "payload": {
    "signature": "0x...",              // Cryptographic signature
    "authorization": {
      "from": "0xSubscriberAddress",   // Subscriber's wallet address
      "sessionKeysProvider": "zerodev", // Session key provider
      "sessionKeys": []                // Session keys for gasless transactions
    }
  },
  "extensions": {}                     // Optional extensions
}
```

A **v3** token adds the bound fields and the one-time nonce inside
`payload.authorization` (and mirrors `httpVerb` into `accepted.extra`):

```json
{
  "payload": {
    "signature": "0x...",
    "authorization": {
      "from": "0xSubscriberAddress",
      "sessionKeysProvider": "zerodev",
      "sessionKeys": [],
      "agentId": "did:nv:agent-456",
      "resourceUrl": "https://seller.example/api/v1/tasks",
      "httpVerb": "POST",
      "nonce": "0x..."
    }
  }
}
```

The presence of a non-empty `nonce` is what identifies a v3 token — that is
exactly what `detectAccessTokenVersion` checks. A field that was absent at mint
is signed as the empty string and must also be absent from the envelope; editing
the envelope after minting so it disagrees with the signed value is rejected as
forgery (`BCK.X402.0005`).

### Card-Delegation Token Structure

For fiat plans using `nvm:card-delegation`, the token contains a JWT-based delegation authorization:

```json
{
  "x402Version": 2,
  "accepted": {
    "scheme": "nvm:card-delegation",
    "network": "stripe",
    "planId": "plan-123",
    "extra": {
      "version": "1",
      "agentId": "did:nv:agent-456"
    }
  },
  "payload": {
    "token": "eyJhbGciOiJSUzI1NiIs...",
    "authorization": {
      "from": "0xSubscriberAddress",
      "sessionKeys": [{ "id": "redeem", "data": "0xabc123..." }]
    }
  },
  "extensions": {}
}
```

### Token Components

- **x402Version**: Protocol version (2 for current spec)
- **accepted**: Payment method specification
  - **scheme**: `nvm:erc4337` for crypto or `nvm:card-delegation` for fiat
  - **network**: `eip155:84532` (Base Sepolia) for crypto, one of `stripe`, `braintree`, or `visa` for fiat
  - **planId**: The payment plan being used
  - **extra**: Additional metadata (version, agentId, etc.)
- **payload**: Payment authorization
  - **signature** (erc4337): Cryptographic proof of payment authorization
  - **token** (card-delegation): Signed JWT encoding the delegation claims
  - **authorization**: Subscriber identity and session keys
- **extensions**: Optional protocol extensions

## Verify X402 Permissions

Agents verify tokens before executing requests:

```typescript
import { Payments, EnvironmentName } from '@nevermined-io/payments'
import { buildPaymentRequired } from '@nevermined-io/payments'

const agentPayments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,  // Builder/agent API key
  environment: 'sandbox' as EnvironmentName,
})

// Build payment required specification
const paymentRequired = buildPaymentRequired(planId, {
  endpoint: '/api/v1/tasks',
  agentId: agentId,
  httpVerb: 'POST',
  network: 'eip155:84532',
  description: 'Task execution API',
})

// Verify subscriber permissions
const verification = await agentPayments.facilitator.verifyPermissions({
  paymentRequired,
  x402AccessToken: accessToken,
  maxAmount: 2n,  // Max credits to verify
})

if (verification.isValid) {
  console.log('✓ Permissions verified')
  console.log(`Payer: ${verification.payer}`)
} else {
  console.log(`✗ Verification failed: ${verification.invalidReason}`)
}
```

### Verification Response

```typescript
interface VerifyPermissionsResult {
  isValid: boolean           // True if token is valid
  invalidReason?: string     // Reason for invalidity (if isValid is false)
  payer?: string             // Address of the payer's wallet
  agentRequestId?: string    // Agent request ID for observability tracking (Nevermined extension)
  urlMatching?: string       // URL pattern that matched the endpoint (Nevermined extension)
  agentRequest?: StartAgentRequest  // Agent request context for observability (Nevermined extension)
}

interface StartAgentRequest {
  agentRequestId: string     // Unique request identifier
  agentName: string          // Name of the agent
  agentId: string            // Agent identifier
  balance: PlanBalance       // Current plan balance (planId, balance, pricePerCredit, etc.)
  urlMatching: string        // URL pattern that was matched
  verbMatching: string       // HTTP verb that was matched
  batch: boolean             // Whether this is a batch request
}
```

## Settle X402 Permissions

After successful execution, burn credits:

```typescript
// Settle permissions (burn credits)
const settlement = await agentPayments.facilitator.settlePermissions({
  paymentRequired,
  x402AccessToken: accessToken,
  maxAmount: 2n,  // Credits to burn
  batch: false,   // Batch settlement (optional)
  marginPercent: 5,  // Add 5% margin (optional)
  agentRequestId: verification.agentRequest?.agentRequestId,  // From verification
})

console.log(`✓ Settlement successful`)
console.log(`Transaction: ${settlement.transaction}`)

// Read `billingModel` before the credit fields — see "Was the buyer charged?" below
if (settlement.billingModel === 'pay-as-you-go') {
  console.log(`Charged, reference: ${settlement.orderTx || settlement.transaction}`)
} else {
  console.log(`Credits burned: ${settlement.creditsRedeemed}`)
  console.log(`Remaining balance: ${settlement.remainingBalance}`)
}
```

### Settlement Response

```typescript
type X402BillingModel = 'credits' | 'pay-as-you-go'

interface SettlePermissionsResult {
  success: boolean               // True if settlement succeeded
  errorReason?: string           // Only present when success is false
  payer?: string                 // Payer's wallet address
  transaction: string            // Blockchain transaction hash
  network: string                // Rail: CAIP-2 chain id, or the fiat PSP ('stripe', …)
  billingModel?: X402BillingModel // How this request was priced
  creditsRedeemed?: string       // Credits burned — always '0' on pay-as-you-go
  remainingBalance?: string      // Remaining credits — always '0' on pay-as-you-go
  orderTx?: string               // Order / per-request charge reference
}
```

### Was the buyer charged?

`success` alone tells you the settle worked. What to check **in addition** depends on
`billingModel`:

| `billingModel` | Success criterion | Credit fields |
| -------------- | ----------------- | ------------- |
| `credits` | `success === true && Number(creditsRedeemed) > 0` | `creditsRedeemed` is the amount burned, `remainingBalance` what is left |
| `pay-as-you-go` | `success === true` **and** a non-empty `orderTx` (fiat rails) / `transaction` (crypto rails) | always the string `'0'` — no balance exists on this plan shape |

**No `billingModel` in the response?** You are talking to a Nevermined API that predates the
discriminator. Apply the **credits** rule — never read a missing discriminator as pay-as-you-go.

```typescript
const settled =
  settlement.success &&
  (settlement.billingModel === 'pay-as-you-go'
    ? Boolean(settlement.orderTx || settlement.transaction)
    : Number(settlement.creditsRedeemed ?? '0') > 0)
```

Two things make this easy to get wrong:

- **Do not gate on `creditsRedeemed` without reading `billingModel` first.** A pay-as-you-go
  plan holds no credit balance, so `creditsRedeemed > 0` can never hold there — a real charge
  reads as a decline. On a card rail that invites a retry of a payment that already succeeded,
  and repeated attempts feed issuer fraud scoring.
- **These fields are strings.** `'0'` is truthy while `Number('0') > 0` is false, so two
  plausible-looking checks disagree. Compare numerically, and only on `credits` plans.

## HTTP Headers (X402 v2)

### payment-signature Header

Subscribers include this header in requests:

```http
POST /api/v1/tasks HTTP/1.1
Host: agent.example.com
Content-Type: application/json
payment-signature: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{"prompt": "Hello"}
```

### PAYMENT-REQUIRED Header (402 Response)

Agents return this header when payment is required:

```http
HTTP/1.1 402 Payment Required
Content-Type: application/json
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwicmVzb3VyY2UiOnsidXJsIjoiL2FwaS92MS90YXNrcyJ9...

{"error": "Payment required"}
```

The header contains base64-encoded payment requirement JSON. The `scheme` and `network` vary by plan type:

**Crypto plan:**

```json
{
  "x402Version": 2,
  "resource": {
    "url": "/api/v1/tasks",
    "description": "Task execution API",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "nvm:erc4337",
      "network": "eip155:84532",
      "planId": "plan-123",
      "extra": {
        "version": "1",
        "agentId": "did:nv:agent-456"
      }
    }
  ],
  "extensions": {}
}
```

**Fiat plan:**

```json
{
  "x402Version": 2,
  "resource": {
    "url": "/api/v1/tasks",
    "description": "Task execution API",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "nvm:card-delegation",
      "network": "stripe",
      "planId": "plan-123",
      "extra": {
        "version": "1",
        "agentId": "did:nv:agent-456"
      }
    }
  ],
  "extensions": {}
}
```

### PAYMENT-RESPONSE Header (Success)

Agents include this header in successful responses:

```http
HTTP/1.1 200 OK
Content-Type: application/json
PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLCJ0cmFuc2FjdGlvbiI6IjB4Li4uIn0=

{"result": "Task completed"}
```

The header contains base64-encoded settlement details. On a `credits` plan:

```json
{
  "success": true,
  "network": "eip155:84532",
  "transaction": "0x...",
  "billingModel": "credits",
  "creditsRedeemed": "2",
  "remainingBalance": "98"
}
```

The same successful settle on a **pay-as-you-go** plan reports the charge in `orderTx`
(fiat rails) or `transaction` (crypto rails), and reads `"0"` for both credit fields:

```json
{
  "success": true,
  "network": "stripe",
  "transaction": "pi_3U6tgrBYvSRKcV421ehH4bnX",
  "billingModel": "pay-as-you-go",
  "creditsRedeemed": "0",
  "remainingBalance": "0",
  "orderTx": "pi_3U6tgrBYvSRKcV421ehH4bnX"
}
```

See [Was the buyer charged?](#was-the-buyer-charged) for the check to apply to each.

## Complete X402 Flow

### Subscriber Side

```typescript
import { Payments, EnvironmentName } from '@nevermined-io/payments'

const subscriberPayments = Payments.getInstance({
  nvmApiKey: process.env.SUBSCRIBER_API_KEY!,
  environment: 'sandbox' as EnvironmentName,
})

// 1. Create a delegation once (currency required; 'usdc' for erc4337),
//    then generate an access token by its delegationId.
const { delegationId } = await subscriberPayments.delegation.createDelegation({
  provider: 'erc4337',
  spendingLimitCents: 1000,
  durationSecs: 86_400,
  currency: 'usdc',
})
const { accessToken } = await subscriberPayments.x402.getX402AccessToken(planId, agentId, {
  scheme: 'nvm:erc4337',
  delegationConfig: { delegationId },
})

// 2. Make request with payment-signature header
const response = await fetch('https://agent.example.com/api/v1/tasks', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'payment-signature': accessToken,
  },
  body: JSON.stringify({ prompt: 'Hello, agent!' }),
})

// 3. Handle response
if (response.status === 402) {
  // Payment required - need to purchase plan or top up
  const paymentRequired = response.headers.get('PAYMENT-REQUIRED')
  console.error('Payment required:', paymentRequired)
} else {
  const result = await response.json()
  const paymentResponse = response.headers.get('PAYMENT-RESPONSE')
  console.log('Success:', result)
  console.log('Payment:', paymentResponse)
}
```

### Agent Side

```typescript
import express from 'express'
import { Payments, EnvironmentName } from '@nevermined-io/payments'
import { buildPaymentRequired } from '@nevermined-io/payments'

const app = express()
app.use(express.json())

const agentPayments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,
  environment: 'sandbox' as EnvironmentName,
})

app.post('/api/v1/tasks', async (req, res) => {
  // 1. Extract token
  const accessToken = req.headers['payment-signature'] as string

  if (!accessToken) {
    const paymentRequired = buildPaymentRequired(planId, {
      endpoint: req.url,
      agentId: agentId,
      httpVerb: req.method,
    })

    return res
      .status(402)
      .set('PAYMENT-REQUIRED', Buffer.from(JSON.stringify(paymentRequired)).toString('base64'))
      .json({ error: 'Payment required' })
  }

  // 2. Build payment required
  const paymentRequired = buildPaymentRequired(planId, {
    endpoint: req.url,
    agentId: agentId,
    httpVerb: req.method,
  })

  try {
    // 3. Verify permissions
    const verification = await agentPayments.facilitator.verifyPermissions({
      paymentRequired,
      x402AccessToken: accessToken,
      maxAmount: 2n,
    })

    if (!verification.isValid) {
      return res
        .status(402)
        .set('PAYMENT-REQUIRED', Buffer.from(JSON.stringify(paymentRequired)).toString('base64'))
        .json({ error: 'Insufficient credits' })
    }

    // 4. Execute task
    const result = await processTask(req.body)

    // 5. Settle permissions
    const settlement = await agentPayments.facilitator.settlePermissions({
      paymentRequired,
      x402AccessToken: accessToken,
      maxAmount: 2n,
    })

    // 6. Return response with PAYMENT-RESPONSE header. Pass `billingModel` through —
    //    without it the client cannot tell a pay-as-you-go charge (creditsRedeemed
    //    always "0") from a credits settle that burned nothing.
    const paymentResponse = {
      success: settlement.success,
      network: settlement.network,
      transaction: settlement.transaction,
      billingModel: settlement.billingModel,
      creditsRedeemed: settlement.creditsRedeemed,
      orderTx: settlement.orderTx,
    }

    return res
      .status(200)
      .set('PAYMENT-RESPONSE', Buffer.from(JSON.stringify(paymentResponse)).toString('base64'))
      .json({ result })

  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

app.listen(3000)
```

## buildPaymentRequired Helper

Simplifies creating X402PaymentRequired objects:

```typescript
import { buildPaymentRequired } from '@nevermined-io/payments'

// Scheme auto-detected from plan metadata when omitted
const paymentRequired = buildPaymentRequired(
  planId,
  {
    endpoint: '/api/v1/tasks',      // Resource URL
    agentId: 'did:nv:agent-456',    // Agent ID
    httpVerb: 'POST',               // HTTP method
    network: 'eip155:84532',        // Blockchain (default for nvm:erc4337)
    description: 'Task execution',  // Description
    scheme: 'nvm:erc4337'           // Optional: omit to auto-detect from plan metadata
  }
)
```

When `scheme` is set to `'nvm:card-delegation'`, the `network` is automatically set to `'stripe'`.

## Best Practices

1. **Always Verify Before Execute**: Never skip token verification
2. **Settle After Success**: Only burn credits after successful execution
3. **Use X402 v2 Headers**: Prefer `payment-signature` over Authorization
4. **Return 402 Properly**: Include `PAYMENT-REQUIRED` header with details
5. **Log Transactions**: Record settlement transaction hashes
6. **Handle Errors**: Provide clear error messages in 402 responses
7. **Token Reuse**: Subscribers can reuse tokens for multiple requests
8. **Restrict Cards to API Keys**: When running multiple agents, restrict each card to specific NVM API Keys using `allowedApiKeyIds` to prevent unauthorized spending
9. **Reuse Delegations**: Pass `delegationId` to reuse existing delegations instead of creating new ones on each request — this avoids delegation sprawl and keeps spending consolidated

## Related Documentation

- [Querying an Agent](./querying-an-agent) - How subscribers use X402 tokens
- [Validation of Requests](./validation-of-requests) - How agents verify and settle
- [MCP Integration](./mcp-integration) - Automatic X402 handling in MCP
- [A2A Integration](./a2a-integration) - Automatic X402 handling in A2A

---

**Source References**:
- `src/x402/token.ts` (getX402AccessToken)
- `src/x402/delegation-api.ts` (DelegationAPI: listPaymentMethods, listDelegations, updatePaymentMethod)
- `src/x402/facilitator-api.ts` (verifyPermissions, settlePermissions, buildPaymentRequired)
- `tests/e2e/test_x402_e2e.test.ts` (complete X402 flow)
