feat: add x402 premium content example route
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Public portal app
|
||||
|
||||
# thirdweb x402 server-side configuration for /api/premium-content
|
||||
THIRDWEB_SECRET_KEY=your-thirdweb-secret-key
|
||||
X402_SERVER_WALLET_ADDRESS=0x64357fCAdcc0a85C23B56a076A790d79328C6aD2
|
||||
|
||||
# Chain 138 GRU v2 x402 defaults
|
||||
# Supported symbols here: cUSDC_V2 or cUSDT_V2
|
||||
X402_PAYMENT_TOKEN_SYMBOL=cUSDC_V2
|
||||
# $0.01 at 6 decimals
|
||||
X402_PAYMENT_AMOUNT_BASE_UNITS=10000
|
||||
# Optional explicit token override. Defaults from repo inventory:
|
||||
# cUSDC_V2=0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d
|
||||
# cUSDT_V2=0x9FBfab33882Efe0038DAa608185718b772EE5660
|
||||
X402_PAYMENT_TOKEN_ADDRESS=
|
||||
# Optional explicit Chain 138 RPC override
|
||||
X402_RPC_URL_138=
|
||||
|
||||
# Optional: set when the app sits behind a proxy and the public origin differs from request.url
|
||||
# Example: https://the-order.sankofa.nexus
|
||||
X402_PUBLIC_BASE_URL=
|
||||
|
||||
# Optional: override the exact x402 resource URL if clients should always pay for one canonical URL
|
||||
# Example: https://the-order.sankofa.nexus/api/premium-content
|
||||
X402_RESOURCE_URL=
|
||||
@@ -22,3 +22,16 @@ pnpm start
|
||||
|
||||
See `.env.example` for required environment variables.
|
||||
|
||||
## x402 Example Route
|
||||
|
||||
This app now includes an App Router example at `GET /api/premium-content`.
|
||||
|
||||
It mirrors the standard `thirdweb/x402` settlement flow:
|
||||
|
||||
- settles a payment from the `x-payment` or `payment-signature` header
|
||||
- uses Chain 138 by default
|
||||
- defaults to the GRU v2 x402 surface: `cUSDC_V2` (`0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d`)
|
||||
- can be switched to `cUSDT_V2` with `X402_PAYMENT_TOKEN_SYMBOL=cUSDT_V2`
|
||||
- returns `{"data":"premium content"}` when payment succeeds
|
||||
|
||||
Set `THIRDWEB_SECRET_KEY` and `X402_SERVER_WALLET_ADDRESS` in your environment before calling the route.
|
||||
|
||||
@@ -12,37 +12,38 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"@the-order/api-client": "workspace:*",
|
||||
"@the-order/schemas": "workspace:*",
|
||||
"@the-order/ui": "workspace:*",
|
||||
"axios": "^1.6.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.0.6",
|
||||
"lucide-react": "^0.309.0",
|
||||
"next": "^14.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@the-order/ui": "workspace:*",
|
||||
"@the-order/schemas": "workspace:*",
|
||||
"@the-order/api-client": "workspace:*",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"zustand": "^4.4.7",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"thirdweb": "^5.118.2",
|
||||
"zod": "^3.22.4",
|
||||
"date-fns": "^3.0.6"
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"typescript": "^5.3.3",
|
||||
"tsx": "^4.20.5",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"postcss": "^8.4.33",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"baseline-browser-mapping": "^2.10.19",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"postcss": "^8.4.33",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { createThirdwebClient, defineChain } from 'thirdweb';
|
||||
import { facilitator, settlePayment } from 'thirdweb/x402';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
type HexAddress = `0x${string}`;
|
||||
|
||||
const chain138 = defineChain({
|
||||
id: 138,
|
||||
name: 'DeFi Oracle Meta Mainnet',
|
||||
rpc: process.env.X402_RPC_URL_138 || process.env.RPC_URL_138 || 'https://rpc-http-pub.d-bis.org',
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_CUSDC_V2_ADDRESS: HexAddress = '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d';
|
||||
const DEFAULT_CUSDT_V2_ADDRESS: HexAddress = '0x9FBfab33882Efe0038DAa608185718b772EE5660';
|
||||
|
||||
const secretKey = process.env.THIRDWEB_SECRET_KEY;
|
||||
const serverWalletAddress =
|
||||
process.env.X402_SERVER_WALLET_ADDRESS || process.env.SERVER_WALLET_ADDRESS;
|
||||
const configuredResourceUrl = process.env.X402_RESOURCE_URL?.trim();
|
||||
const publicBaseUrl = process.env.X402_PUBLIC_BASE_URL?.trim().replace(/\/+$/, '') || '';
|
||||
const paymentTokenSymbol = (process.env.X402_PAYMENT_TOKEN_SYMBOL || 'cUSDC_V2').trim();
|
||||
const defaultPaymentTokenAddress =
|
||||
paymentTokenSymbol === 'cUSDT_V2'
|
||||
? process.env.CUSDT_V2_ADDRESS_138 || DEFAULT_CUSDT_V2_ADDRESS
|
||||
: process.env.CUSDC_V2_ADDRESS_138 || DEFAULT_CUSDC_V2_ADDRESS;
|
||||
const paymentTokenAddress = (
|
||||
process.env.X402_PAYMENT_TOKEN_ADDRESS || defaultPaymentTokenAddress
|
||||
).trim() as HexAddress;
|
||||
const paymentAmount = process.env.X402_PAYMENT_AMOUNT_BASE_UNITS?.trim() || '10000';
|
||||
|
||||
const client = secretKey ? createThirdwebClient({ secretKey }) : null;
|
||||
|
||||
const thirdwebX402Facilitator =
|
||||
client && serverWalletAddress
|
||||
? facilitator({
|
||||
client,
|
||||
serverWalletAddress,
|
||||
})
|
||||
: null;
|
||||
|
||||
function getResourceUrl(request: Request): string {
|
||||
if (configuredResourceUrl) {
|
||||
return configuredResourceUrl;
|
||||
}
|
||||
|
||||
if (publicBaseUrl) {
|
||||
const url = new URL(request.url);
|
||||
return `${publicBaseUrl}${url.pathname}`;
|
||||
}
|
||||
|
||||
return request.url;
|
||||
}
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
if (!thirdwebX402Facilitator || !serverWalletAddress) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'x402 not configured',
|
||||
hint: 'Set THIRDWEB_SECRET_KEY and X402_SERVER_WALLET_ADDRESS (or SERVER_WALLET_ADDRESS).',
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl: getResourceUrl(request),
|
||||
method: 'GET',
|
||||
paymentData: request.headers.get('x-payment') || request.headers.get('payment-signature'),
|
||||
network: chain138,
|
||||
price: {
|
||||
amount: paymentAmount,
|
||||
asset: {
|
||||
address: paymentTokenAddress,
|
||||
decimals: 6,
|
||||
},
|
||||
},
|
||||
facilitator: thirdwebX402Facilitator,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
return Response.json({
|
||||
data: 'premium content',
|
||||
paymentTokenSymbol,
|
||||
paymentTokenAddress,
|
||||
network: chain138.id,
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
Generated
+6074
-157
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user