Skip to content
Draft
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
6 changes: 6 additions & 0 deletions examples/privy-next-x402-payments/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Get your Privy App ID from https://dashboard.privy.io
NEXT_PUBLIC_PRIVY_APP_ID=

# Wallet address to receive x402 payments (for the demo API endpoint)
# This is a testnet address for demonstration
RECEIVING_WALLET_ADDRESS=0x7c19Ceb4c1f2adD71537366497EE32e166D6D3D7
152 changes: 152 additions & 0 deletions examples/privy-next-x402-payments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# x402 Payments with Privy

This example demonstrates how to integrate the [x402 payment protocol](https://x402.org) with Privy embedded wallets to enable automatic USDC payments for API access.

## What is x402?

x402 is an open payment protocol that enables instant, programmatic payments for APIs and digital content over HTTP. When a resource requires payment, the server responds with `402 Payment Required`. The client automatically constructs an `X-PAYMENT` header with a signed authorization and retries the request.

## What You'll Learn

- Using Privy's `useX402Fetch` hook for automatic payments
- Building x402-enabled API endpoints that require payment
- Verifying and settling payments with Coinbase's x402 facilitator
- Decoding payment receipts with transaction details

## Prerequisites

- Node.js 18+ and npm
- A Privy account ([sign up here](https://dashboard.privy.io))
- Testnet USDC on Base Sepolia ([get from Circle's faucet](https://faucet.circle.com/))

## Setup

1. **Clone and install**:

```bash
git clone https://github.com/privy-io/examples.git
cd examples/examples/privy-next-x402-payments
npm install
```

2. **Configure environment**:

```bash
cp .env.example .env.local
```

Edit `.env.local` and add your Privy App ID from [dashboard.privy.io](https://dashboard.privy.io).

3. **Run the example**:

```bash
npm run dev
```

4. **Open** [http://localhost:3200](http://localhost:3200)

## How It Works

### Client Side (`pages/index.tsx`)

```typescript
import { useX402Fetch, useWallets } from "@privy-io/react-auth";

const { wallets } = useWallets();
const { wrapFetchWithPayment } = useX402Fetch();

// Wrap fetch to automatically handle 402 payments
const fetchWithPayment = wrapFetchWithPayment({
walletAddress: wallets[0].address,
fetch,
maxValue: BigInt(10000000), // Max 10 USDC
});

// Use like normal fetch - automatically handles payments
const response = await fetchWithPayment("/api/weather");
```

### Server Side (`pages/api/weather.ts`)

The API endpoint:

1. Returns `402 Payment Required` if no `X-PAYMENT` header is present
2. Verifies the payment with Coinbase's facilitator
3. Settles the payment onchain
4. Returns the weather data with a payment receipt

## Testing

1. **Login** with Privy (creates an embedded wallet)
2. **Get testnet USDC** from [Circle's faucet](https://faucet.circle.com/) for Base Sepolia
3. **Click** "Fetch Paid Weather Data"
4. **Sign** the payment authorization when prompted
5. **See** the weather data and transaction receipt!

## Payment Flow

1. Client calls `fetchWithPayment('/api/weather')`
2. Server responds with `402 Payment Required` + payment requirements
3. Hook builds EIP-712 typed data for USDC transfer authorization
4. Privy prompts user to sign (no gas required - facilitator pays)
5. Hook retries request with `X-PAYMENT` header
6. Server verifies payment with Coinbase facilitator
7. Facilitator settles payment onchain on Base Sepolia
8. Server returns weather data + `X-PAYMENT-RESPONSE` receipt
9. Client displays data and transaction link

## Code Walkthrough

### useX402Fetch Hook

The `useX402Fetch` hook wraps the native fetch API to automatically:

- Detect `402 Payment Required` responses
- Build and sign payment authorizations using Privy embedded wallets
- Retry requests with the `X-PAYMENT` header
- Protect users with max payment limits

### Payment Requirements

The server specifies:

- **Scheme**: `exact` (pay exact amount)
- **Network**: `base-sepolia` (testnet)
- **Amount**: `1000` (0.001 USDC, 6 decimals)
- **Asset**: USDC contract address
- **PayTo**: Receiving wallet address

### EIP-3009 Authorization

Payments use USDC's `transferWithAuthorization` method:

- User signs an off-chain authorization
- Facilitator submits it onchain (pays gas)
- USDC transfers from user to merchant
- No ETH needed by the user!

## Resources

- [x402 Protocol Specification](https://github.com/coinbase/x402)
- [Coinbase x402 Documentation](https://docs.cdp.coinbase.com/x402/welcome)
- [Privy x402 Recipe](https://docs.privy.io/recipes/x402)
- [Privy Documentation](https://docs.privy.io)
- [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009)

## Production Considerations

For production use:

- Use mainnet (`base` network) instead of `base-sepolia`
- Use Coinbase's mainnet facilitator (requires CDP API keys)
- Set appropriate `maxValue` limits
- Add proper error handling and user feedback
- Monitor payment settlement and handle failures

See the [Coinbase x402 Mainnet Guide](https://docs.cdp.coinbase.com/x402/quickstart-for-sellers#running-on-mainnet) for production setup.

## Questions?

- Join the [Privy Discord](https://privy.io/discord)
- Check out [Privy docs](https://docs.privy.io)
- Visit [x402.org](https://x402.org)
10 changes: 10 additions & 0 deletions examples/privy-next-x402-payments/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack: (config) => {
config.resolve.fallback = { fs: false, net: false, tls: false };
return config;
},
};

module.exports = nextConfig;
32 changes: 32 additions & 0 deletions examples/privy-next-x402-payments/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"private": true,
"engines": {
"npm": ">=8.0.0 <11.0.0",
"node": ">=18.0.0"
},
"scripts": {
"dev": "next dev -p 3200",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.0.12",
"@privy-io/react-auth": "^3.6.1",
"next": "^15.1.7",
"react": "18.3.1",
"react-dom": "18.3.1",
"x402": "^0.7.1"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "18.3.0",
"autoprefixer": "^10.4.7",
"eslint": "^8.23.0",
"eslint-config-next": "15.1.7",
"postcss": "^8.4.14",
"tailwindcss": "^4",
"@tailwindcss/postcss": "^4",
"typescript": "^5"
}
}
54 changes: 54 additions & 0 deletions examples/privy-next-x402-payments/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Head from "next/head";
import { PrivyProvider } from "@privy-io/react-auth";

function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>Privy x402 Payments Example</title>
<meta
name="description"
content="Privy x402 payment protocol integration example"
/>
<link rel="icon" href="/privy-logo.png" />
</Head>

<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ""}
config={{
embeddedWallets: {
createOnLogin: "users-without-wallets",
},
defaultChain: {
id: 84532, // Base Sepolia
name: "Base Sepolia",
network: "base-sepolia",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: ["https://sepolia.base.org"],
},
},
blockExplorers: {
default: {
name: "BaseScan",
url: "https://sepolia.basescan.org",
},
},
testnet: true,
},
}}
>
<Component {...pageProps} />
</PrivyProvider>
</>
);
}

export default MyApp;
103 changes: 103 additions & 0 deletions examples/privy-next-x402-payments/pages/api/weather.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { exact } from "x402/schemes";
import { useFacilitator } from "x402/verify";

/**
* x402-enabled weather API endpoint
*
* This endpoint demonstrates how to accept x402 payments for API access.
* It requires payment of 0.001 USDC on Base Sepolia via Coinbase's facilitator.
*/
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const xPayment = req.headers["x-payment"] as string | undefined;

const paymentRequirements = {
scheme: "exact" as const,
network: "base-sepolia" as const,
maxAmountRequired: "1000", // 0.001 USDC (6 decimals)
resource: `http://localhost:3200/api/weather`, // Full URL required by x402 spec
description: "Weather API access",
mimeType: "application/json",
payTo:
process.env.RECEIVING_WALLET_ADDRESS ||
"0x7c19Ceb4c1f2adD71537366497EE32e166D6D3D7",
maxTimeoutSeconds: 120,
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia
extra: {
name: "USDC",
version: "2",
},
};

// If no payment header, return 402 Payment Required
if (!xPayment) {
return res.status(402).json({
x402Version: 1,
accepts: [paymentRequirements],
});
}

// Verify and settle payment with Coinbase facilitator
try {
const { verify, settle } = useFacilitator({
url: "https://x402.org/facilitator",
});
const paymentPayload = exact.evm.decodePayment(xPayment);

// Step 1: Verify payment
const verifyResult = await verify(
paymentPayload as any,
paymentRequirements as any
);

if (!verifyResult.isValid) {
return res.status(402).json({
x402Version: 1,
error: `Payment verification failed: ${verifyResult.invalidReason}`,
accepts: [paymentRequirements],
});
}

// Step 2: Settle payment onchain
const settleResult = await settle(
paymentPayload as any,
paymentRequirements as any
);

if (!settleResult.success) {
return res.status(500).json({
error: `Settlement failed: ${
settleResult.errorReason || "Unknown error"
}`,
});
}

// Step 3: Set payment receipt header
res.setHeader(
"X-PAYMENT-RESPONSE",
Buffer.from(
JSON.stringify({
transaction: settleResult.transaction,
network: settleResult.network,
payer: settleResult.payer,
})
).toString("base64")
);

// Step 4: Return weather data
return res.status(200).json({
report: {
weather: "sunny",
temperature: 72,
},
});
} catch (error) {
return res.status(500).json({
error: "Payment processing error",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
Loading