feat: add x402 premium content example route

This commit is contained in:
defiQUG
2026-04-24 12:55:09 -07:00
parent bbb6ce6a6c
commit 675801449b
5 changed files with 6231 additions and 175 deletions
+25
View File
@@ -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=
+13
View File
@@ -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.
+19 -18
View File
@@ -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,
});
}
+6074 -157
View File
File diff suppressed because it is too large Load Diff