Skip to content

Conversation

@amircheikh
Copy link
Contributor

@amircheikh amircheikh commented Nov 13, 2025

Summary & Motivation

x402 with Turnkey Embedded Wallets

This example demonstrates how to use Coinbase's x402 payment protocol with Turnkey's embedded wallets for seamless payment authentication.

Overview

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.

How It Works

1. Middleware-Based Access Control

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:

  • If the cookie exists: The user has made a valid payment and can access the protected content
  • If the cookie is missing: The user is redirected to the paywall page to complete payment

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.

2. Turnkey-Powered Payment Authorization

The payment page (app/paywall/page.tsx) uses Turnkey's embedded wallets to sign the payment authorization:

  1. Authentication: Users log in to a Turnkey sub-org using the @turnkey/react-wallet-kit package, which handles authentication with different methods
  2. Sign EIP-712 Payload: The user signs an EIP-3009 TransferWithAuthorization message for USDC on Base Sepolia using the @turnkey/viem package. This is a gasless transfer authorization that includes:
    • The sender (from) and recipient (to) addresses
    • The payment amount (0.01 USDC in this demo)
    • Validity period (5 minutes)
    • A unique nonce to prevent replay attacks
  3. Encode & Submit: The signed authorization is encoded in x402 format using the x402 package and submitted to the server for verification

3. Payment Verification with Public Facilitator

After the payload is signed, the server route uses a public x402 facilitator to verify the payment:

  1. Decode Payment: The EIP-712 signed payment payload is decoded from the x402 format using the x402 package
  2. Verify: The facilitator's /verify endpoint validates that:
    • The signature is valid and signed by the claimed payer
    • The payment meets the requirements (amount, asset, recipient, timing)
    • The nonce hasn't been used before
  3. Settle: If valid, the facilitator's /settle endpoint records the payment as settled
  4. Set Cookie: Upon successful verification, a payment-session cookie is set with a 5-minute expiration

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.

Running The App

To start, ensure you have a Turnkey organization setup with Auth Proxy enabled.

Configure your .env.local file

Copy the .env.local.example file and rename it to .env.local then fill in the following fields:

NEXT_PUBLIC_ORGANIZATION_ID="your-turnkey-organization-id"
NEXT_PUBLIC_AUTH_PROXY_ID="your-turnkey-auth-proxy-config-id"

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
NEXT_PUBLIC_RESOURCE_WALLET_ADDRESS=0xYourResourceWalletAddressHere # This is the resource wallet address where payments will be sent. Replace with your own eth wallet address.

You can use any Ethereum wallet address to act as a resource wallet. The Base Sepolia USDC will go there.

Start the Development Server

Install the packages and run the dev server:

pnpm i
pnpm run dev

You'll see the demo running on http://localhost:3000.

@codesandbox-ci
Copy link

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit ad71151:

Sandbox Source
@turnkey/example-react-components Configuration

@socket-security
Copy link

socket-security bot commented Nov 13, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​svgr/​webpack@​8.1.09910010080100
Addedtailwindcss@​4.1.111001008598100
Addedx402@​0.7.2981009195100

View full report

@amircheikh amircheikh marked this pull request as ready for review November 14, 2025 21:08
Copy link
Contributor

@r-n-o r-n-o left a comment

Choose a reason for hiding this comment

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

👏

Some minor things below, but overall great work!

Comment on lines 5 to 13
// This middleware will intercept requests to the protected route(s). It checks for the presence of a "payment-session" cookie,
// which indicates that the user has made a valid payment. If the cookie is not present, the user is redirected to the paywall page.

const paymentHeader = request.cookies.get("payment-session");
if (!paymentHeader) {
return NextResponse.rewrite(new URL("/paywall", request.url));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe worth a comment here: this validation is really weak because it only checks for the presence of a payment-session header and doesn't validate its value. So a good TODO would be using a x402 validator to ensure the payment header is valid and contain a payment that has been made correctly, to the right address.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comment

};

useEffect(() => {
setScale(100);
Copy link
Contributor

Choose a reason for hiding this comment

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

crank it up to 1000

I'm 50% joking 50% not joking

Copy link
Contributor Author

Choose a reason for hiding this comment

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

500 is a good compromise

Comment on lines +36 to +32
const facilitatorUrl =
process.env.NEXT_PUBLIC_FACILITATOR_URL ||
"https://www.x402.org/facilitator";

// Call facilitator to verify
const verifyResponse = await fetch(`${facilitatorUrl}/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
paymentPayload: decodedPayment,
paymentRequirements: PAYMENT_REQUIREMENTS,
}),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh! So we do use a public facilitator already. Glorious.

mimeType: "text/html",
payTo: process.env.NEXT_PUBLIC_RESOURCE_WALLET_ADDRESS!,
maxTimeoutSeconds: COOKIE_TIMEOUT_SECONDS,
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia
Copy link
Contributor

Choose a reason for hiding this comment

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

(nit) you already have this in the other file (const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";). Define it in a common place and import it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added constants.ts

@amircheikh amircheikh force-pushed the amir/x402 branch 2 times, most recently from 705c99f to 0da27ad Compare November 17, 2025 15:38
@amircheikh amircheikh merged commit 8466ca7 into main Nov 17, 2025
3 of 4 checks passed
@amircheikh amircheikh deleted the amir/x402 branch November 17, 2025 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants