Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .changeset/chilly-taxis-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"thirdweb": minor
---

Add "upto" payment scheme option for x402 verify and settle

```typescript
const paymentArgs = {
resourceUrl: "https://api.example.com/premium-content",
method: "GET",
paymentData,
payTo: "0x1234567890123456789012345678901234567890",
network: arbitrum,
scheme: "upto", // enables dynamic pricing
price: "$0.10", // max payable amount
facilitator: thirdwebFacilitator,
};

// First verify the payment is valid for the max amount
const verifyResult = await verifyPayment(paymentArgs);

if (verifyResult.status !== 200) {
return Response.json(verifyResult.responseBody, {
status: verifyResult.status,
headers: verifyResult.responseHeaders,
});
}

// Do the expensive work that requires payment
const { tokensUsed } = await doExpensiveWork();
const pricePerTokenUsed = 0.00001;

// Now settle the payment based on actual usage
const settleResult = await settlePayment({
...paymentArgs,
price: tokensUsed * pricePerTokenUsed, // adjust final price based on usage
});
```
5 changes: 0 additions & 5 deletions apps/portal/src/app/x402/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,6 @@ export async function GET(request: Request) {
network: arbitrumSepolia,
price: "$0.01",
facilitator: thirdwebX402Facilitator,
routeConfig: {
description: "Access to premium API content",
mimeType: "application/json",
maxTimeoutSeconds: 300,
},
});

if (result.status === 200) {
Expand Down
173 changes: 113 additions & 60 deletions apps/portal/src/app/x402/server/page.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage, createMetadata } from "@doc";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
DocImage,
createMetadata,
Stack,
GithubTemplateCard
} from "@doc";
import { Steps, Step } from "@doc";
import PaymentFlow from "./x402-protocol-flow.png";

export const metadata = createMetadata({
image: {
title: "x402 Server",
icon: "payments",
},
title: "x402 Server",
description: "Accept x402 payments in your APIs from any x402-compatible client.",
image: {
title: "x402 Server",
icon: "payments",
},
title: "x402 Server",
description:
"Accept x402 payments in your APIs from any x402-compatible client.",
});

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

## Verify vs Settle
## Exact vs Upto Payment Schemes

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

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

Use `settlePayment()` to verify and settle the payment in one step. This is the simplest approach:
By default, the payment scheme is `exact`. You can specify the payment scheme in the `settlePayment()` or `verifyPayment()` arguments.

### Exact Payment Scheme

Use `settlePayment()` to verify and settle the payment in one step. This is the default and simplest approach:

```typescript
const result = await settlePayment(paymentArgs);
const result = await settlePayment({
resourceUrl: "https://api.example.com/premium-content",
method: "GET",
paymentData,
payTo: "0x1234567890123456789012345678901234567890",
network: arbitrum,
price: "$0.10",
facilitator: thirdwebFacilitator,
});

if (result.status === 200) {
// Payment settled, do the paid work
return Response.json({ data: "premium content" });
// Payment settled, do the paid work
return Response.json({ data: "premium content" });
}
```

### Option 2: Verify First, Then Settle
### Upto Payment Scheme

For dynamic pricing, use `verifyPayment()` first, do the work, then `settlePayment()`:

- The final price can be dynamic based on the work performed
- Ensures the payment is valid before doing the expensive work

Use `verifyPayment()` first, do the work, then `settlePayment()`. This is useful when:
- The final price might be dynamic based on the work performed
- You want to ensure the payment is valid before doing expensive work
- You need to calculate resource usage before charging
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).

<Stack>
<GithubTemplateCard
title="x402 AI Inference Example"
description="A fully working example of charging an for AI inference with x402"
href="https://github.com/thirdweb-example/x402-ai-inference"
/>
</Stack>

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Hyphenate "high-level".

Per grammar conventions, compound adjectives before a noun should be hyphenated.

-Here's a high level example of how to use the `upto` payment scheme
+Here's a high-level example of how to use the `upto` payment scheme
🧰 Tools
🪛 LanguageTool

[grammar] ~88-~88: Use a hyphen to join words.
Context: ...ai-inference" /> Here's a high level example of how to use the upto p...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In apps/portal/src/app/x402/server/page.mdx around line 88, the phrase "high
level example" should be hyphenated as "high-level" when used as a compound
adjective before the noun; update the sentence to use "high-level example" to
conform to grammar conventions.


```typescript
// First verify the payment is valid
const paymentArgs = {
resourceUrl: "https://api.example.com/premium-content",
method: "GET",
paymentData,
payTo: "0x1234567890123456789012345678901234567890",
network: arbitrum,
scheme: "upto", // enables dynamic pricing
price: "$0.10", // max payable amount
facilitator: thirdwebFacilitator,
};

// First verify the payment is valid for the max amount
const verifyResult = await verifyPayment(paymentArgs);

if (verifyResult.status !== 200) {
return Response.json(verifyResult.responseBody, {
status: verifyResult.status,
headers: verifyResult.responseHeaders,
});
return Response.json(verifyResult.responseBody, {
status: verifyResult.status,
headers: verifyResult.responseHeaders,
});
}

// Do the expensive work that requires payment
const result = await doExpensiveWork();
const { tokensUsed } = await callExpensiveAIModel();

// Now settle the payment based on actual usage
const pricePerTokenUsed = 0.00001; // ex: $0.00001 per AI model token used
const settleResult = await settlePayment({
...paymentArgs,
price: calculateDynamicPrice(result), // adjust price based on usage
...paymentArgs,
price: tokensUsed * pricePerTokenUsed, // adjust final price based on usage
});

return Response.json(result);
```

## Price and Token Configuration

You can specify prices in multiple ways:

### USD String

This will default to using USDC on the specified network.

```typescript
network: polygon, // or any other EVM chain
price: "$0.10" // 10 cents in USDC
```

### ERC20 Token

You can use any ERC20 token that supports the ERC-2612 permit or ERC-3009 sign with authorization.

Simply pass the amount in base units and the token address.

```typescript
network: arbitrum,
price: {
amount: "1000000000000000", // Amount in base units (0.001 tokens with 18 decimals)
asset: {
address: "0xf01E52B0BAC3E147f6CAf956a64586865A0aA928", // Token address
}
}
```

### Native Token

Payments in native tokens are not currently supported.

## Dedicated Endpoint Examples

Protect individual API endpoints with x402 payments:
Expand Down Expand Up @@ -117,7 +197,6 @@ Protect individual API endpoints with x402 payments:
routeConfig: {
description: "Access to premium API content",
mimeType: "application/json",
maxTimeoutSeconds: 300,
},
});

Expand All @@ -133,6 +212,7 @@ Protect individual API endpoints with x402 payments:
}
}
```

</TabsContent>

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

app.listen(3000);
```

</TabsContent>

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

export default app;
```

</TabsContent>
</Tabs>

Expand Down Expand Up @@ -311,6 +393,7 @@ Protect multiple endpoints with a shared middleware:
matcher: ["/api/paid/:path*"],
};
```

</TabsContent>

<TabsContent value="express">
Expand Down Expand Up @@ -364,6 +447,7 @@ Protect multiple endpoints with a shared middleware:
res.json({ message: "This is premium content!" });
});
```

</TabsContent>

<TabsContent value="hono">
Expand Down Expand Up @@ -418,38 +502,7 @@ Protect multiple endpoints with a shared middleware:
return c.json({ message: "This is premium content!" });
});
```

</TabsContent>
</Tabs>

## Price and Token Configuration

You can specify prices in multiple ways:

### USD String

This will default to using USDC on the specified network.

```typescript
network: polygon, // or any other EVM chain
price: "$0.10" // 10 cents in USDC
```

### ERC20 Token

You can use any ERC20 token that supports the ERC-2612 permit or ERC-3009 sign with authorization.

Simply pass the amount in base units and the token address.

```typescript
network: arbitrum,
price: {
amount: "1000000000000000", // Amount in base units (0.001 tokens with 18 decimals)
asset: {
address: "0xf01E52B0BAC3E147f6CAf956a64586865A0aA928", // Token address
}
}
```

### Native Token

Payments in native tokens are not currently supported.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ import { GithubIcon } from "../GithubButtonLink";

export function GithubTemplateCard(props: {
title: string;
description?: string;
href: string;
tag?: string;
}) {
return (
<Link className="flex cursor-default" href={props.href} target="_blank">
<Link className="flex" href={props.href} target="_blank">
<article className="group/article flex w-full items-center overflow-hidden rounded-lg border bg-card transition-colors duration-300 hover:border-active-border">
<div className="flex w-full items-center gap-3 p-4">
<div className="flex w-full items-center gap-4 p-4">
<GithubIcon className="size-5 shrink-0" />
<h3 className="font-medium text-base">{props.title}</h3>
<div className="flex flex-col gap-1">
<h3 className="font-medium text-base">{props.title}</h3>
{props.description && (
<p className="text-muted-foreground text-sm">
{props.description}
</p>
)}
</div>
{props.tag && (
<div className="ml-auto shrink-0 rounded-lg border bg-muted px-2 py-1 text-foreground text-xs transition-colors">
{props.tag}
Expand Down
22 changes: 2 additions & 20 deletions packages/thirdweb/src/x402/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,10 @@ type GetPaymentRequirementsResult = {
export async function decodePaymentRequest(
args: PaymentArgs,
): Promise<GetPaymentRequirementsResult | PaymentRequiredResult> {
const {
price,
network,
facilitator,
payTo,
resourceUrl,
routeConfig = {},
method,
paymentData,
extraMetadata,
} = args;
const { facilitator, routeConfig = {}, paymentData } = args;
const { errorMessages } = routeConfig;

const paymentRequirementsResult = await facilitator.accepts({
resourceUrl,
method,
network,
price,
routeConfig,
payTo,
extraMetadata,
});
const paymentRequirementsResult = await facilitator.accepts(args);

// Check for payment header, if none, return the payment requirements
if (!paymentData) {
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/src/x402/facilitator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export function facilitator(
method: args.method,
network: caip2ChainId,
price: args.price,
scheme: args.scheme,
routeConfig: args.routeConfig,
serverWalletAddress: facilitator.address,
recipientAddress: args.payTo,
Expand Down
Loading
Loading