Skip to content

Commit 705c99f

Browse files
committed
x402 X Turnkey demo added
1 parent a1fa36b commit 705c99f

File tree

26 files changed

+2498
-277
lines changed

26 files changed

+2498
-277
lines changed

.changeset/chubby-peas-sip.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
Updated the `@turnkey/gas-station` SDK to align with the audited smart contract changes. The audit resulted in several interface updates:
66

77
**Contract Changes:**
8+
89
- **New contract addresses**: Updated both delegate and execution contract addresses to the newly deployed versions
910
- **EIP-712 field name changes**: The canonical delegate contract interface uses simplified field names (`to`, `value`, `data`) instead of the previous descriptive names (`outputContract`, `ethAmount`, `arguments`)
1011

1112
**SDK Updates:**
13+
1214
- Updated `DEFAULT_EXECUTION_CONTRACT` address from `0x4ece92b06C7d2d99d87f052E0Fca47Fb180c3348` to `0x00000000008c57a1CE37836a5e9d36759D070d8c`
1315
- Updated `DEFAULT_DELEGATE_CONTRACT` address from `0xC2a37Ee08cAc3778d9d05FF0a93FD5B553C77E3a` to `0x000066a00056CD44008768E2aF00696e19A30084`
1416
- Updated EIP-712 Execution typehash field names to match the contract's canonical interface
@@ -17,6 +19,7 @@ Updated the `@turnkey/gas-station` SDK to align with the audited smart contract
1719
- Updated documentation and examples to reflect the new field names
1820

1921
**Files Modified:**
22+
2023
- `packages/gas-station/src/config.ts` - Updated contract addresses
2124
- `packages/gas-station/src/intentBuilder.ts` - Updated EIP-712 type definitions and message objects
2225
- `packages/gas-station/src/policyUtils.ts` - Updated policy condition field references and documentation
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
NEXT_PUBLIC_ORGANIZATION_ID="your-turnkey-organization-id"
2+
NEXT_PUBLIC_AUTH_PROXY_ID="your-turnkey-auth-proxy-config-id"
3+
4+
NEXT_PUBLIC_FACILITATOR_URL=https://www.x402.org/facilitator # This is a public, free, community maintained facilitator URL for x402; replace if you have your own
5+
NEXT_PUBLIC_RESOURCE_WALLET_ADDRESS=0xYourResourceWalletAddressHere # This is the resource wallet address where payments will be sent. Replace with your own eth wallet address.

examples/with-x402/.eslintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

examples/with-x402/.gitignore

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts
36+
37+
# Playwright
38+
node_modules/
39+
/test-results/
40+
/playwright-report/
41+
/blob-report/
42+
/playwright/.cache/

examples/with-x402/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# x402 with Turnkey Embedded Wallets
2+
3+
This example demonstrates how to use Coinbase's x402 payment protocol with Turnkey's embedded wallets for seamless payment authentication.
4+
5+
## Overview
6+
7+
This demo shows how to build a custom x402 middleware that integrates with Turnkey's embedded wallet system instead of relying on browser wallet extensions.
8+
9+
## How It Works
10+
11+
### 1. Middleware-Based Access Control
12+
13+
The demo uses Next.js middleware to gate access to protected content. The middleware (`middleware.ts`) intercepts requests to `/protected/*` routes and checks for a `payment-session` cookie:
14+
15+
- **If the cookie exists**: The user has made a valid payment and can access the protected content
16+
- **If the cookie is missing**: The user is redirected to the paywall page to complete payment
17+
18+
This means the server is responsible for gating the access to the protected page. The cookie only lasts for 5 minutes and can be cleared before if needed.
19+
20+
### 2. Turnkey-Powered Payment Authorization
21+
22+
The payment page (`app/paywall/page.tsx`) uses Turnkey's embedded wallets to sign the payment authorization:
23+
24+
1. **Authentication**: Users log in to a Turnkey sub-org using the [`@turnkey/react-wallet-kit`](../../packages/react-native-wallet-kit/) package, which handles authentication with different methods
25+
2. **Sign EIP-712 Payload**: The user signs an EIP-3009 `TransferWithAuthorization` message for USDC on Base Sepolia using the [`@turnkey/viem`](../../packages/viem/) package. This is a gasless transfer authorization that includes:
26+
- The sender (`from`) and recipient (`to`) addresses
27+
- The payment amount (0.01 USDC in this demo)
28+
- Validity period (5 minutes)
29+
- A unique nonce to prevent replay attacks
30+
3. **Encode & Submit**: The signed authorization is encoded in x402 format using the [`x402`](https://www.npmjs.com/package/x402) package and submitted to the server for verification
31+
32+
### 3. Payment Verification with Public Facilitator
33+
34+
After the payload is signed, the [server route](./app/api/verify-payment/route.ts) uses a [public x402 facilitator](https://www.x402.org/facilitator) to verify the payment:
35+
36+
1. **Decode Payment**: The EIP-712 signed payment payload is decoded from the x402 format using the [`x402`](https://www.npmjs.com/package/x402) package
37+
2. **Verify**: The facilitator's `/verify` endpoint validates that:
38+
- The signature is valid and signed by the claimed payer
39+
- The payment meets the requirements (amount, asset, recipient, timing)
40+
- The nonce hasn't been used before
41+
3. **Settle**: If valid, the facilitator's `/settle` endpoint records the payment as settled
42+
4. **Set Cookie**: Upon successful verification, a `payment-session` cookie is set with a 5-minute expiration
43+
44+
The facilitator acts as a neutral third party that verifies payments without requiring the merchant to run their own verification infrastructure. You can also host your own facilitator.
45+
46+
## Running The App
47+
48+
To start, ensure you have a [Turnkey organization setup](https://docs.turnkey.com/getting-started/quickstart) with [Auth Proxy enabled.](https://docs.turnkey.com/sdks/react/getting-started#turnkey-organization-setup)
49+
50+
### Configure your `.env.local` file
51+
52+
Copy the [`.env.local.example`](./.env.local.example) file and rename it to `.env.local` then fill in the following fields:
53+
54+
```
55+
NEXT_PUBLIC_ORGANIZATION_ID="your-turnkey-organization-id"
56+
NEXT_PUBLIC_AUTH_PROXY_ID="your-turnkey-auth-proxy-config-id"
57+
58+
NEXT_PUBLIC_FACILITATOR_URL=https://www.x402.org/facilitator # This is a public, free, community maintained facilitator URL for x402; replace if you have your own
59+
NEXT_PUBLIC_RESOURCE_WALLET_ADDRESS=0xYourResourceWalletAddressHere # This is the resource wallet address where payments will be sent. Replace with your own eth wallet address.
60+
```
61+
62+
You can use any Ethereum wallet address to act as a resource wallet. The Base Sepolia USDC will go there.
63+
64+
### Start the Development Server
65+
66+
Install the packages and run the dev server:
67+
68+
```bash
69+
pnpm i
70+
pnpm run dev
71+
```
72+
73+
You'll see the demo running on http://localhost:3000.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { NextResponse } from "next/server";
2+
import { cookies } from "next/headers";
3+
import { exact } from "x402/schemes";
4+
import { PAYMENT_REQUIREMENTS, COOKIE_TIMEOUT_SECONDS } from "../../constants";
5+
6+
export async function POST(req: Request) {
7+
try {
8+
const { paymentHeader } = await req.json();
9+
10+
if (!paymentHeader) {
11+
return NextResponse.json(
12+
{ success: false, error: "Payment header is required" },
13+
{ status: 400 },
14+
);
15+
}
16+
17+
// Decode the payment header
18+
const decodedPayment = exact.evm.decodePayment(paymentHeader);
19+
20+
const facilitatorUrl =
21+
process.env.NEXT_PUBLIC_FACILITATOR_URL ||
22+
"https://www.x402.org/facilitator";
23+
24+
// Call facilitator to verify
25+
const verifyResponse = await fetch(`${facilitatorUrl}/verify`, {
26+
method: "POST",
27+
headers: { "Content-Type": "application/json" },
28+
body: JSON.stringify({
29+
paymentPayload: decodedPayment,
30+
paymentRequirements: PAYMENT_REQUIREMENTS,
31+
}),
32+
});
33+
34+
const verifyResult = await verifyResponse.json();
35+
36+
if (!verifyResult.isValid) {
37+
return NextResponse.json(
38+
{
39+
success: false,
40+
x402Version: 1,
41+
error: verifyResult.invalidReason || "Payment verification failed",
42+
accepts: [PAYMENT_REQUIREMENTS],
43+
payer: verifyResult.payer,
44+
},
45+
{ status: 402 }, // Payment Required!
46+
);
47+
}
48+
49+
// Call facilitator to settle the payment
50+
const settleResponse = await fetch(`${facilitatorUrl}/settle`, {
51+
method: "POST",
52+
headers: { "Content-Type": "application/json" },
53+
body: JSON.stringify({
54+
paymentPayload: decodedPayment,
55+
paymentRequirements: PAYMENT_REQUIREMENTS,
56+
}),
57+
});
58+
59+
const settleResult = await settleResponse.json();
60+
61+
if (!settleResult.success) {
62+
return NextResponse.json(
63+
{
64+
success: false,
65+
error: settleResult.errorReason || "Settle failed",
66+
},
67+
{ status: 500 },
68+
);
69+
}
70+
71+
// Payment is valid and settled, set the cookie
72+
const cookieStore = await cookies();
73+
// This should be a JWT signed by the server following best practices for a session token
74+
// See: https://nextjs.org/docs/app/guides/authentication#stateless-sessions
75+
cookieStore.set("payment-session", paymentHeader, {
76+
maxAge: COOKIE_TIMEOUT_SECONDS, // 5 minutes
77+
});
78+
79+
return NextResponse.json(
80+
{
81+
success: true,
82+
payer: verifyResult.payer,
83+
},
84+
{ status: 200 },
85+
);
86+
} catch (error) {
87+
console.error("Error verifying payment:", error);
88+
return NextResponse.json(
89+
{
90+
success: false,
91+
x402Version: 1,
92+
error: error instanceof Error ? error.message : "Internal server error",
93+
accepts: [PAYMENT_REQUIREMENTS],
94+
},
95+
{ status: 500 },
96+
);
97+
}
98+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// USDC address on Base Sepolia
2+
export const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
3+
4+
// 0.01 USDC (6 decimals)
5+
export const PAYMENT_AMOUNT = "10000";
6+
7+
// Cookie timeout
8+
export const COOKIE_TIMEOUT_SECONDS = 300;
9+
10+
// Payment requirements
11+
export const PAYMENT_REQUIREMENTS = {
12+
scheme: "exact" as const,
13+
network: "base-sepolia" as const,
14+
maxAmountRequired: PAYMENT_AMOUNT, // 0.01 USDC
15+
resource: "http://example.com/",
16+
description: "Access to protected content",
17+
mimeType: "text/html",
18+
payTo: process.env.NEXT_PUBLIC_RESOURCE_WALLET_ADDRESS!,
19+
maxTimeoutSeconds: COOKIE_TIMEOUT_SECONDS,
20+
asset: USDC_ADDRESS, // USDC on Base Sepolia
21+
extra: { name: "USDC", version: "2" },
22+
};

examples/with-x402/app/globals.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@import "tailwindcss";
2+
3+
@font-face {
4+
font-family: "Inter";
5+
font-style: normal;
6+
font-weight: 400;
7+
font-display: swap;
8+
src: url("/fonts/inter/Inter-Regular.woff2?v=3.19") format("woff2");
9+
}
10+
11+
@font-face {
12+
font-family: "Inter";
13+
font-style: normal;
14+
font-weight: 600;
15+
font-display: swap;
16+
src: url("/fonts/inter/Inter-SemiBold.woff2?v=3.19") format("woff2");
17+
}
18+
19+
* {
20+
font-family:
21+
"Inter",
22+
-apple-system,
23+
BlinkMacSystemFont,
24+
sans-serif;
25+
}
26+
27+
:root {
28+
font-family:
29+
"Inter",
30+
-apple-system,
31+
BlinkMacSystemFont,
32+
sans-serif;
33+
}
34+
35+
button {
36+
cursor: pointer;
37+
transition: transform 0.1s ease;
38+
}
39+
40+
button:active {
41+
transform: scale(0.95);
42+
}

examples/with-x402/app/layout.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import { Geist, Geist_Mono } from "next/font/google";
4+
import "./globals.css";
5+
import { CreateSubOrgParams, TurnkeyProvider } from "@turnkey/react-wallet-kit";
6+
import "@turnkey/react-wallet-kit/styles.css";
7+
8+
const geistSans = Geist({
9+
variable: "--font-geist-sans",
10+
subsets: ["latin"],
11+
});
12+
13+
const geistMono = Geist_Mono({
14+
variable: "--font-geist-mono",
15+
subsets: ["latin"],
16+
});
17+
18+
const createSubOrgParams: CreateSubOrgParams = {
19+
customWallet: {
20+
walletName: "ETH Wallet",
21+
walletAccounts: [
22+
{
23+
addressFormat: "ADDRESS_FORMAT_ETHEREUM",
24+
curve: "CURVE_SECP256K1",
25+
pathFormat: "PATH_FORMAT_BIP32",
26+
path: "m/44'/60'/0'/0/0",
27+
},
28+
],
29+
},
30+
};
31+
32+
export default function RootLayout({
33+
children,
34+
}: Readonly<{
35+
children: React.ReactNode;
36+
}>) {
37+
return (
38+
<html lang="en">
39+
<head>
40+
<title>Turnkey x402</title>
41+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
42+
</head>
43+
<body
44+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
45+
>
46+
<TurnkeyProvider
47+
config={{
48+
organizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!,
49+
authProxyConfigId: process.env.NEXT_PUBLIC_AUTH_PROXY_ID!,
50+
auth: {
51+
createSuborgParams: {
52+
emailOtpAuth: createSubOrgParams,
53+
smsOtpAuth: createSubOrgParams,
54+
walletAuth: createSubOrgParams,
55+
passkeyAuth: createSubOrgParams,
56+
oauth: createSubOrgParams,
57+
},
58+
},
59+
}}
60+
>
61+
{children}
62+
</TurnkeyProvider>
63+
</body>
64+
</html>
65+
);
66+
}

0 commit comments

Comments
 (0)