Skip to content

Commit da43bc8

Browse files
add x402 buyers guide
* add delegations buyer guide * update step title * add advanced permission buyer guide * add recurring payments guide * fix typo * clean up code snippets * Apply suggestions from code review Co-authored-by: Alexandra Carrillo <12214231+alexandratran@users.noreply.github.com> * minor fixes * add link to rule reference * resolve review comments --------- Co-authored-by: Alexandra Carrillo <12214231+alexandratran@users.noreply.github.com>
1 parent f1af270 commit da43bc8

7 files changed

Lines changed: 838 additions & 4 deletions

File tree

gator-sidebar.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,23 @@ const sidebar = {
113113
'guides/advanced-permissions/create-redelegation',
114114
],
115115
},
116+
{
117+
type: 'category',
118+
label: 'x402',
119+
collapsed: true,
120+
items: [
121+
{
122+
type: 'category',
123+
label: 'Buyer',
124+
collapsed: true,
125+
items: [
126+
'guides/x402/buyer/delegations',
127+
'guides/x402/buyer/advanced-permissions',
128+
'guides/x402/buyer/recurring-payments',
129+
],
130+
},
131+
],
132+
},
116133
],
117134
},
118135
{

smart-accounts-kit/guides/advanced-permissions/execute-on-metamask-users-behalf.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ import { calldata } from './config.ts'
258258

259259
// These properties must be extracted from the permission response.
260260
const permissionContext = grantedPermissions[0].context
261-
const delegationManager = grantedPermissions[0].signerMeta.delegationManager
261+
const delegationManager = grantedPermissions[0].delegationManager
262262

263263
// USDC address on Ethereum Sepolia.
264264
const tokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'
@@ -290,7 +290,7 @@ import { calldata } from './config.ts'
290290

291291
// These properties must be extracted from the permission response.
292292
const permissionContext = grantedPermissions[0].context
293-
const delegationManager = grantedPermissions[0].signerMeta.delegationManager
293+
const delegationManager = grantedPermissions[0].delegationManager
294294

295295
// USDC address on Ethereum Sepolia.
296296
const tokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
---
2+
description: Pay for an x402-protected API data using Advanced Permissions with a fixed allowance.
3+
sidebar_label: Advanced Permissions
4+
keywords:
5+
[x402, ERC-7715, ERC-7710, advanced permissions, redeemers, allowance, session account, buyer]
6+
---
7+
8+
import Tabs from '@theme/Tabs';
9+
import TabItem from '@theme/TabItem';
10+
import GlossaryTerm from '@theme/GlossaryTerm';
11+
12+
# Pay for an x402 API with Advanced Permissions
13+
14+
In this guide, you request <GlossaryTerm term="Advanced Permissions" /> with a fixed ERC-20 allowance
15+
to pay for a specific x402-protected resource.
16+
17+
## Prerequisites
18+
19+
- [Install and set up the Smart Accounts Kit.](../../../get-started/install.md)
20+
21+
## Steps
22+
23+
### 1. Set up a Wallet Client
24+
25+
Set up a Wallet Client using Viem's [`createWalletClient`](https://viem.sh/docs/clients/wallet) function. Use this client to interact with MetaMask.
26+
27+
Extend the Wallet Client with `erc7715ProviderActions` to enable <GlossaryTerm term="Advanced Permissions" /> requests.
28+
29+
```typescript
30+
import { createWalletClient, custom } from 'viem'
31+
import { erc7715ProviderActions } from '@metamask/smart-accounts-kit/actions'
32+
33+
const walletClient = createWalletClient({
34+
transport: custom(window.ethereum),
35+
}).extend(erc7715ProviderActions())
36+
```
37+
38+
### 2. Set up a session account
39+
40+
Set up a session account. The requested permissions are granted to the session account, which is responsible for making x402 API calls.
41+
42+
The session account can be either a <GlossaryTerm term="MetaMask smart account">smart account</GlossaryTerm> or an <GlossaryTerm term="Externally owned account (EOA)">EOA</GlossaryTerm>.
43+
This example uses an EOA as the session account.
44+
45+
```typescript
46+
import { privateKeyToAccount } from 'viem/accounts'
47+
import { sepolia as chain } from 'viem/chains'
48+
import { createWalletClient, http } from 'viem'
49+
50+
const sessionAccount = privateKeyToAccount('0x...')
51+
```
52+
53+
### 3. Get payment requirements
54+
55+
Call the protected API route once without a payment header.
56+
57+
The server returns `402` with the payment terms (`PAYMENT-REQUIRED`) in the response, which you use
58+
to build the payment payload.
59+
60+
<Tabs>
61+
<TabItem value="example.ts">
62+
63+
```ts
64+
import { PaymentRequirements } from './types'
65+
66+
// Update the URL
67+
const challengeResponse = await fetch('https://api.example.com/paid-endpoint')
68+
if (challengeResponse.status !== 402) {
69+
console.error('Expected 402 challenge from protected route')
70+
// Handle error
71+
}
72+
73+
const paymentRequiredHeader = challengeResponse.headers.get('PAYMENT-REQUIRED')
74+
if (!paymentRequiredHeader) {
75+
console.error('PAYMENT-REQUIRED header is missing')
76+
// Handle error
77+
}
78+
79+
const decodedPaymentRequired = Buffer.from(paymentRequiredHeader, 'base64').toString('utf-8')
80+
const paymentRequired = JSON.parse(decodedPaymentRequired) as {
81+
accepts: PaymentRequirements[]
82+
}
83+
84+
const accepted = paymentRequired.accepts[0]
85+
if (!accepted) {
86+
console.error('Server did not provide accepted payment requirements')
87+
// Handle error
88+
}
89+
90+
if (accepted.extra.assetTransferMethod !== 'erc7710') {
91+
console.error('Server does not support ERC-7710 delegation payments')
92+
// Handle error
93+
}
94+
```
95+
96+
</TabItem>
97+
<TabItem value="types.ts">
98+
99+
```ts
100+
import { Address } from 'viem'
101+
102+
export type PaymentRequirements = {
103+
scheme: string
104+
network: string
105+
amount: string
106+
asset: Address
107+
payTo: Address
108+
maxTimeoutSeconds: number
109+
extra: {
110+
assetTransferMethod: string
111+
facilitators?: Address[]
112+
}
113+
}
114+
```
115+
116+
</TabItem>
117+
</Tabs>
118+
119+
### 4. Request Advanced Permissions
120+
121+
Request Advanced Permissions from the user with the Wallet Client's `requestExecutionPermissions` action.
122+
123+
In this example, you request an ERC-20 allowance permission with a fixed allowance equal to the
124+
resource cost. Use the [`redeemer`](../../../reference/advanced-permissions/rules.md#redeemer) rule
125+
to restrict redemption to facilitator addresses from the payment requirements.
126+
127+
See the [`requestExecutionPermissions`](../../../reference/advanced-permissions/wallet-client.md#requestexecutionpermissions) API reference for more information.
128+
129+
```ts
130+
import { base as chain } from 'viem/chains'
131+
132+
const facilitators = accepted.extra.facilitators
133+
if (!facilitators || facilitators.length === 0) {
134+
console.error('No facilitators found in PAYMENT-REQUIRED')
135+
// Handle error
136+
}
137+
138+
const currentTime = Math.floor(Date.now() / 1000)
139+
const expiry = currentTime + 3600
140+
141+
const grantedPermissions = await walletClient.requestExecutionPermissions([
142+
{
143+
chainId: chain.id,
144+
expiry,
145+
to: sessionAccount.address,
146+
permission: {
147+
type: 'erc20-token-allowance',
148+
data: {
149+
tokenAddress: accepted.asset,
150+
// Fixed allowance for this resource.
151+
allowanceAmount: BigInt(accepted.amount),
152+
justification: 'Permission to pay for a specific x402-protected API resource',
153+
},
154+
isAdjustmentAllowed: false,
155+
},
156+
rules: [
157+
{
158+
type: 'redeemer',
159+
data: {
160+
addresses: facilitators!,
161+
},
162+
},
163+
],
164+
},
165+
])
166+
```
167+
168+
### 5. Create a redelegation
169+
170+
The granted advanced permission is delegated to the session account. To let facilitator addresses
171+
redeem this permission context for x402 settlement, create an <GlossaryTerm term="Open redelegation">open redelegation</GlossaryTerm> from the session account.
172+
173+
Use the Wallet Client's [`redelegatePermissionContextOpen`](../../../reference/erc7710/wallet-client.md#redelegatepermissioncontextopen)
174+
action to create a redelegated permission context. The granted permission already includes
175+
a [redeemer enforcer](../../../reference/delegation/caveats.md#redeemer), so you do not add extra caveats here.
176+
177+
<Tabs>
178+
<TabItem value="example.ts">
179+
180+
```ts
181+
import { environment, sessionAccountWalletClient } from './config.ts'
182+
183+
const permission = grantedPermissions[0]
184+
if (!permission) {
185+
console.error('No permission response returned by requestExecutionPermissions')
186+
// Handle error
187+
}
188+
189+
const { permissionContext: redelegatedPermissionContext } =
190+
await sessionAccountWalletClient.redelegatePermissionContextOpen({
191+
environment,
192+
permissionContext: permission!.context,
193+
})
194+
```
195+
196+
</TabItem>
197+
<TabItem value="config.ts">
198+
199+
```ts
200+
import { createWalletClient, http } from 'viem'
201+
import { base as chain } from 'viem/chains'
202+
import { getSmartAccountsEnvironment } from '@metamask/smart-accounts-kit'
203+
import { erc7710WalletActions } from '@metamask/smart-accounts-kit/actions'
204+
import { sessionAccount } from './session-account.ts'
205+
206+
export const environment = getSmartAccountsEnvironment(chain.id)
207+
208+
export const sessionAccountWalletClient = createWalletClient({
209+
account: sessionAccount,
210+
chain,
211+
transport: http(),
212+
}).extend(erc7710WalletActions())
213+
```
214+
215+
</TabItem>
216+
</Tabs>
217+
218+
### 6. Create the payment payload
219+
220+
Create a payment payload using the redelegated permission context and accepted requirements.
221+
For ERC-7710 (Smart Contract Delegation), x402 requires the payload fields `delegationManager`,
222+
`permissionContext`, and `delegator`. The facilitator uses `permissionContext` to simulate
223+
during verification and then settle the payment.
224+
225+
Encode the full x402 payment payload as base64, then send it in the `payment-signature` header.
226+
227+
<Tabs>
228+
<TabItem value="example.ts">
229+
230+
```ts
231+
import { PaymentPayload } from './types'
232+
233+
const permission = grantedPermissions[0]
234+
235+
const paymentPayload: PaymentPayload = {
236+
x402Version: 2,
237+
accepted,
238+
payload: {
239+
delegationManager: permission.delegationManager,
240+
permissionContext: redelegatedPermissionContext,
241+
delegator: permission.from,
242+
},
243+
}
244+
245+
const encodedPayment = Buffer.from(JSON.stringify(paymentPayload)).toString('base64')
246+
```
247+
248+
</TabItem>
249+
<TabItem value="types.ts">
250+
251+
```ts
252+
import { Address, Hex } from 'viem'
253+
254+
export type PaymentPayload = {
255+
x402Version: 2
256+
accepted: PaymentRequirements
257+
payload: {
258+
delegationManager: Address
259+
permissionContext: Hex
260+
delegator: Address
261+
}
262+
}
263+
```
264+
265+
</TabItem>
266+
</Tabs>
267+
268+
### 7. Make the paid request
269+
270+
Send the base64-encoded x402 payment payload in the `payment-signature` header.
271+
If verification succeeds, the server returns the protected data.
272+
273+
```ts
274+
const apiResponse = await fetch('https://api.example.com/paid-endpoint', {
275+
headers: {
276+
'payment-signature': encodedPayment,
277+
},
278+
})
279+
280+
if (!apiResponse.ok) {
281+
const errorBody = await apiResponse.json()
282+
console.error(errorBody.error ?? 'API request failed')
283+
// Handle error
284+
}
285+
286+
const data = await apiResponse.json()
287+
console.log('Protected API response:', data)
288+
```

0 commit comments

Comments
 (0)