Skip to content

Commit 35aaf24

Browse files
[SDK] Add payment scheme and improve type documentation for x402 (#8457)
1 parent ad2d175 commit 35aaf24

File tree

11 files changed

+213
-103
lines changed

11 files changed

+213
-103
lines changed

.changeset/chilly-taxis-search.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Add "upto" payment scheme option for x402 verify and settle
6+
7+
```typescript
8+
const paymentArgs = {
9+
resourceUrl: "https://api.example.com/premium-content",
10+
method: "GET",
11+
paymentData,
12+
payTo: "0x1234567890123456789012345678901234567890",
13+
network: arbitrum,
14+
scheme: "upto", // enables dynamic pricing
15+
price: "$0.10", // max payable amount
16+
facilitator: thirdwebFacilitator,
17+
};
18+
19+
// First verify the payment is valid for the max amount
20+
const verifyResult = await verifyPayment(paymentArgs);
21+
22+
if (verifyResult.status !== 200) {
23+
return Response.json(verifyResult.responseBody, {
24+
status: verifyResult.status,
25+
headers: verifyResult.responseHeaders,
26+
});
27+
}
28+
29+
// Do the expensive work that requires payment
30+
const { tokensUsed } = await doExpensiveWork();
31+
const pricePerTokenUsed = 0.00001;
32+
33+
// Now settle the payment based on actual usage
34+
const settleResult = await settlePayment({
35+
...paymentArgs,
36+
price: tokensUsed * pricePerTokenUsed, // adjust final price based on usage
37+
});
38+
```

apps/portal/src/app/x402/page.mdx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,6 @@ export async function GET(request: Request) {
123123
network: arbitrumSepolia,
124124
price: "$0.01",
125125
facilitator: thirdwebX402Facilitator,
126-
routeConfig: {
127-
description: "Access to premium API content",
128-
mimeType: "application/json",
129-
maxTimeoutSeconds: 300,
130-
},
131126
});
132127

133128
if (result.status === 200) {

apps/portal/src/app/x402/server/page.mdx

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage, createMetadata } from "@doc";
1+
import {
2+
Tabs,
3+
TabsList,
4+
TabsTrigger,
5+
TabsContent,
6+
DocImage,
7+
createMetadata,
8+
Stack,
9+
GithubTemplateCard
10+
} from "@doc";
211
import { Steps, Step } from "@doc";
312
import PaymentFlow from "./x402-protocol-flow.png";
413

514
export const metadata = createMetadata({
6-
image: {
7-
title: "x402 Server",
8-
icon: "payments",
9-
},
10-
title: "x402 Server",
11-
description: "Accept x402 payments in your APIs from any x402-compatible client.",
15+
image: {
16+
title: "x402 Server",
17+
icon: "payments",
18+
},
19+
title: "x402 Server",
20+
description:
21+
"Accept x402 payments in your APIs from any x402-compatible client.",
1222
});
1323

1424
# Server Side
@@ -28,53 +38,123 @@ The x402 protocol follows this flow:
2838
5. **Verify & Settle** - Server verifies and settles the payment
2939
6. **Success** - Server returns the protected content
3040

31-
## Verify vs Settle
41+
## Exact vs Upto Payment Schemes
3242

33-
You have two options for handling payments:
43+
The thirdweb x402 client/server stack supports two payment schemes: `exact` and `upto`.
3444

35-
### Option 1: Settle Payment Directly
45+
- `exact` - The client pays the exact amount specified in the payment requirements.
46+
- `upto` - The client pays any amount up to the specified maximum amount.
3647

37-
Use `settlePayment()` to verify and settle the payment in one step. This is the simplest approach:
48+
By default, the payment scheme is `exact`. You can specify the payment scheme in the `settlePayment()` or `verifyPayment()` arguments.
49+
50+
### Exact Payment Scheme
51+
52+
Use `settlePayment()` to verify and settle the payment in one step. This is the default and simplest approach:
3853

3954
```typescript
40-
const result = await settlePayment(paymentArgs);
55+
const result = await settlePayment({
56+
resourceUrl: "https://api.example.com/premium-content",
57+
method: "GET",
58+
paymentData,
59+
payTo: "0x1234567890123456789012345678901234567890",
60+
network: arbitrum,
61+
price: "$0.10",
62+
facilitator: thirdwebFacilitator,
63+
});
4164

4265
if (result.status === 200) {
43-
// Payment settled, do the paid work
44-
return Response.json({ data: "premium content" });
66+
// Payment settled, do the paid work
67+
return Response.json({ data: "premium content" });
4568
}
4669
```
4770

48-
### Option 2: Verify First, Then Settle
71+
### Upto Payment Scheme
72+
73+
For dynamic pricing, use `verifyPayment()` first, do the work, then `settlePayment()`:
74+
75+
- The final price can be dynamic based on the work performed
76+
- Ensures the payment is valid before doing the expensive work
4977

50-
Use `verifyPayment()` first, do the work, then `settlePayment()`. This is useful when:
51-
- The final price might be dynamic based on the work performed
52-
- You want to ensure the payment is valid before doing expensive work
53-
- You need to calculate resource usage before charging
78+
This is great for AI apis that need to charge based on the token usage for example. Check out a fully working example check out [this x402 ai inference example](https://github.com/thirdweb-example/x402-ai-inference).
79+
80+
<Stack>
81+
<GithubTemplateCard
82+
title="x402 AI Inference Example"
83+
description="A fully working example of charging an for AI inference with x402"
84+
href="https://github.com/thirdweb-example/x402-ai-inference"
85+
/>
86+
</Stack>
87+
88+
Here's a high level example of how to use the `upto` payment scheme with a dynamic price based on the token usage. First we verify the payment is valid for the max payable amount and then settle the payment based on the actual usage.
5489

5590
```typescript
56-
// First verify the payment is valid
91+
const paymentArgs = {
92+
resourceUrl: "https://api.example.com/premium-content",
93+
method: "GET",
94+
paymentData,
95+
payTo: "0x1234567890123456789012345678901234567890",
96+
network: arbitrum,
97+
scheme: "upto", // enables dynamic pricing
98+
price: "$0.10", // max payable amount
99+
facilitator: thirdwebFacilitator,
100+
};
101+
102+
// First verify the payment is valid for the max amount
57103
const verifyResult = await verifyPayment(paymentArgs);
58104

59105
if (verifyResult.status !== 200) {
60-
return Response.json(verifyResult.responseBody, {
61-
status: verifyResult.status,
62-
headers: verifyResult.responseHeaders,
63-
});
106+
return Response.json(verifyResult.responseBody, {
107+
status: verifyResult.status,
108+
headers: verifyResult.responseHeaders,
109+
});
64110
}
65111

66112
// Do the expensive work that requires payment
67-
const result = await doExpensiveWork();
113+
const { tokensUsed } = await callExpensiveAIModel();
68114

69115
// Now settle the payment based on actual usage
116+
const pricePerTokenUsed = 0.00001; // ex: $0.00001 per AI model token used
70117
const settleResult = await settlePayment({
71-
...paymentArgs,
72-
price: calculateDynamicPrice(result), // adjust price based on usage
118+
...paymentArgs,
119+
price: tokensUsed * pricePerTokenUsed, // adjust final price based on usage
73120
});
74121

75122
return Response.json(result);
76123
```
77124

125+
## Price and Token Configuration
126+
127+
You can specify prices in multiple ways:
128+
129+
### USD String
130+
131+
This will default to using USDC on the specified network.
132+
133+
```typescript
134+
network: polygon, // or any other EVM chain
135+
price: "$0.10" // 10 cents in USDC
136+
```
137+
138+
### ERC20 Token
139+
140+
You can use any ERC20 token that supports the ERC-2612 permit or ERC-3009 sign with authorization.
141+
142+
Simply pass the amount in base units and the token address.
143+
144+
```typescript
145+
network: arbitrum,
146+
price: {
147+
amount: "1000000000000000", // Amount in base units (0.001 tokens with 18 decimals)
148+
asset: {
149+
address: "0xf01E52B0BAC3E147f6CAf956a64586865A0aA928", // Token address
150+
}
151+
}
152+
```
153+
154+
### Native Token
155+
156+
Payments in native tokens are not currently supported.
157+
78158
## Dedicated Endpoint Examples
79159

80160
Protect individual API endpoints with x402 payments:
@@ -117,7 +197,6 @@ Protect individual API endpoints with x402 payments:
117197
routeConfig: {
118198
description: "Access to premium API content",
119199
mimeType: "application/json",
120-
maxTimeoutSeconds: 300,
121200
},
122201
});
123202

@@ -133,6 +212,7 @@ Protect individual API endpoints with x402 payments:
133212
}
134213
}
135214
```
215+
136216
</TabsContent>
137217

138218
<TabsContent value="express">
@@ -185,6 +265,7 @@ Protect individual API endpoints with x402 payments:
185265

186266
app.listen(3000);
187267
```
268+
188269
</TabsContent>
189270

190271
<TabsContent value="hono">
@@ -237,6 +318,7 @@ Protect individual API endpoints with x402 payments:
237318

238319
export default app;
239320
```
321+
240322
</TabsContent>
241323
</Tabs>
242324

@@ -311,6 +393,7 @@ Protect multiple endpoints with a shared middleware:
311393
matcher: ["/api/paid/:path*"],
312394
};
313395
```
396+
314397
</TabsContent>
315398

316399
<TabsContent value="express">
@@ -364,6 +447,7 @@ Protect multiple endpoints with a shared middleware:
364447
res.json({ message: "This is premium content!" });
365448
});
366449
```
450+
367451
</TabsContent>
368452

369453
<TabsContent value="hono">
@@ -418,38 +502,7 @@ Protect multiple endpoints with a shared middleware:
418502
return c.json({ message: "This is premium content!" });
419503
});
420504
```
505+
421506
</TabsContent>
422507
</Tabs>
423508

424-
## Price and Token Configuration
425-
426-
You can specify prices in multiple ways:
427-
428-
### USD String
429-
430-
This will default to using USDC on the specified network.
431-
432-
```typescript
433-
network: polygon, // or any other EVM chain
434-
price: "$0.10" // 10 cents in USDC
435-
```
436-
437-
### ERC20 Token
438-
439-
You can use any ERC20 token that supports the ERC-2612 permit or ERC-3009 sign with authorization.
440-
441-
Simply pass the amount in base units and the token address.
442-
443-
```typescript
444-
network: arbitrum,
445-
price: {
446-
amount: "1000000000000000", // Amount in base units (0.001 tokens with 18 decimals)
447-
asset: {
448-
address: "0xf01E52B0BAC3E147f6CAf956a64586865A0aA928", // Token address
449-
}
450-
}
451-
```
452-
453-
### Native Token
454-
455-
Payments in native tokens are not currently supported.

apps/portal/src/components/Document/Cards/GithubTemplateCard.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ import { GithubIcon } from "../GithubButtonLink";
33

44
export function GithubTemplateCard(props: {
55
title: string;
6+
description?: string;
67
href: string;
78
tag?: string;
89
}) {
910
return (
10-
<Link className="flex cursor-default" href={props.href} target="_blank">
11+
<Link className="flex" href={props.href} target="_blank">
1112
<article className="group/article flex w-full items-center overflow-hidden rounded-lg border bg-card transition-colors duration-300 hover:border-active-border">
12-
<div className="flex w-full items-center gap-3 p-4">
13+
<div className="flex w-full items-center gap-4 p-4">
1314
<GithubIcon className="size-5 shrink-0" />
14-
<h3 className="font-medium text-base">{props.title}</h3>
15+
<div className="flex flex-col gap-1">
16+
<h3 className="font-medium text-base">{props.title}</h3>
17+
{props.description && (
18+
<p className="text-muted-foreground text-sm">
19+
{props.description}
20+
</p>
21+
)}
22+
</div>
1523
{props.tag && (
1624
<div className="ml-auto shrink-0 rounded-lg border bg-muted px-2 py-1 text-foreground text-xs transition-colors">
1725
{props.tag}

packages/thirdweb/src/x402/common.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,10 @@ type GetPaymentRequirementsResult = {
3535
export async function decodePaymentRequest(
3636
args: PaymentArgs,
3737
): Promise<GetPaymentRequirementsResult | PaymentRequiredResult> {
38-
const {
39-
price,
40-
network,
41-
facilitator,
42-
payTo,
43-
resourceUrl,
44-
routeConfig = {},
45-
method,
46-
paymentData,
47-
extraMetadata,
48-
} = args;
38+
const { facilitator, routeConfig = {}, paymentData } = args;
4939
const { errorMessages } = routeConfig;
5040

51-
const paymentRequirementsResult = await facilitator.accepts({
52-
resourceUrl,
53-
method,
54-
network,
55-
price,
56-
routeConfig,
57-
payTo,
58-
extraMetadata,
59-
});
41+
const paymentRequirementsResult = await facilitator.accepts(args);
6042

6143
// Check for payment header, if none, return the payment requirements
6244
if (!paymentData) {

packages/thirdweb/src/x402/facilitator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ export function facilitator(
278278
method: args.method,
279279
network: caip2ChainId,
280280
price: args.price,
281+
scheme: args.scheme,
281282
routeConfig: args.routeConfig,
282283
serverWalletAddress: facilitator.address,
283284
recipientAddress: args.payTo,

0 commit comments

Comments
 (0)