Pay for an x402 API with delegation
In this guide, you use a buyer smart accountMetaMask smart account A smart contract account created using the Smart Accounts Kit that supports programmable behavior, flexible signing options, and ERC-7710 delegations. to access API data from an x402 server.
You create an open delegation, restrict redemption to the facilitator, encode the delegation chain, and send it as the payment payload when calling a protected API route.
Prerequisites
Steps
1. Create a buyer account
Create an account to represent the buyer, the delegatorDelegator account The account that creates and signs a delegation to grant limited authority to another account. who will create a delegation.
The delegator must be a MetaMask smart accountMetaMask smart account A smart contract account created using the Smart Accounts Kit that supports programmable behavior, flexible signing options, and ERC-7710 delegations.; use the toolkit's toMetaMaskSmartAccount method to create the buyer account.
Fund the smart account with USDC for the requested payment.
- example.ts
- config.ts
import { Implementation, toMetaMaskSmartAccount } from '@metamask/smart-accounts-kit'
import { publicClient, buyerAccount } from './config'
export const buyerSmartAccount = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [buyerAccount.address, [], [], []],
deploySalt: '0x',
signer: { account: buyerAccount },
})
import { createPublicClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { base as chain } from 'viem/chains'
export const publicClient = createPublicClient({
chain,
transport: http(),
})
export const buyerAccount = privateKeyToAccount('0x<BUYER_PRIVATE_KEY>')
2. Get payment requirements
Call the protected API route once without a payment header.
The server returns 402 with the payment terms (PAYMENT-REQUIRED) in the response, which you use
to build the payment payload.
- example.ts
- types.ts
import { PaymentRequirements } from './types'
// Update the URL
const challengeResponse = await fetch('https://api.example.com/paid-endpoint')
if (challengeResponse.status !== 402) {
console.error('Expected 402 challenge from protected route')
// Handle error
}
const paymentRequiredHeader = challengeResponse.headers.get('PAYMENT-REQUIRED')
if (!paymentRequiredHeader) {
console.error('PAYMENT-REQUIRED header is missing')
// Handle error
}
const decodedPaymentRequired = Buffer.from(paymentRequiredHeader, 'base64').toString('utf-8')
const paymentRequired = JSON.parse(decodedPaymentRequired) as {
accepts: PaymentRequirements[]
}
const accepted = paymentRequired.accepts[0]
if (!accepted) {
console.error('Server did not provide accepted payment requirements')
// Handle error
}
if (accepted.extra.assetTransferMethod !== 'erc7710') {
console.error('Server does not support ERC-7710 delegation payments')
// Handle error
}
export type PaymentRequirements = {
scheme: string
network: string
amount: string
asset: `0x${string}`
payTo: `0x${string}`
maxTimeoutSeconds: number
extra: {
assetTransferMethod: string
facilitators?: `0x${string}`[]
}
}
3. Create a delegation
Create an open root delegation
from the buyer smart account. With an open root delegation, the buyer delegates authority without
setting a specific delegate. Use createOpenDelegation to create the open root delegation.
This example uses the erc20TransferAmount
scope to allow USDC transfers up to the amount requested in payment terms.
It also uses the redeemer caveat enforcer to restrict
redemption to facilitator addresses provided by the server.
Before creating the delegation, make sure your buyer smart account is deployed. If it is not deployed, delegation redemption will fail.
import { CaveatType, createOpenDelegation, ScopeType } from '@metamask/smart-accounts-kit'
const facilitators = accepted.extra.facilitators
if (!facilitators || facilitators.length === 0) {
console.log('No facilitators found in PAYMENT-REQUIRED')
// Handle the error
}
// Use the amount requested by the seller.
const maxAmount = BigInt(accepted.amount)
const caveats = [
{
type: CaveatType.Redeemer,
redeemers: facilitators,
},
]
const delegation = createOpenDelegation({
from: buyerSmartAccount.address,
environment: buyerSmartAccount.environment,
scope: {
type: ScopeType.Erc20TransferAmount,
tokenAddress: accepted.asset,
maxAmount,
},
caveats,
})
const signature = await buyerSmartAccount.signDelegation({
delegation,
})
const signedDelegation = {
...delegation,
signature,
}
4. Create the payment payload
Create a payment payload using the signed delegation and accepted requirements.
For ERC-7710 (Smart Contract Delegation), x402 requires the payload fields delegationManager, permissionContext, and delegator.
The facilitator uses permissionContext to simulate during verification and then settle the payment.
Use encodeDelegations to encode the delegation chain.
Then base64 encode the full x402 payment payload before sending it in the payment-signature header.
- example.ts
- types.ts
import { encodeDelegations } from '@metamask/smart-accounts-kit/utils'
import { PaymentPayload } from './types'
const permissionContext = encodeDelegations([signedDelegation])
const paymentPayload: PaymentPayload = {
x402Version: 2,
accepted,
payload: {
delegationManager: buyerSmartAccount.environment.DelegationManager,
permissionContext,
delegator: buyerSmartAccount.address,
},
}
const encodedPayment = Buffer.from(JSON.stringify(paymentPayload)).toString('base64')
export type PaymentPayload = {
x402Version: 2
accepted: PaymentRequirements
payload: {
delegationManager: `0x${string}`
permissionContext: `0x${string}`
delegator: `0x${string}`
}
}
5. Make the paid request
Send the encoded x402 payment payload in the payment-signature header.
If verification succeeds, the server returns the protected data.
const apiResponse = await fetch('https://api.example.com/paid-endpoint', {
headers: {
'payment-signature': encodedPayment,
},
})
if (!apiResponse.ok) {
const errorBody = await apiResponse.json()
throw new Error(errorBody.error ?? 'API request failed')
}
const data = await apiResponse.json()
console.log('Protected API response:', data)