chore: sync all changes to Gitea
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- Config, docs, scripts, and backup manifests - Submodule refs unchanged (m = modified content in submodules) Made-with: Cursor
This commit is contained in:
16
x402-api/.env.example
Normal file
16
x402-api/.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
# thirdweb (required for x402)
|
||||
# Get secret key: https://thirdweb.com → Dashboard → Settings → API Keys
|
||||
THIRDWEB_SECRET_KEY=your-thirdweb-secret-key
|
||||
|
||||
# Server wallet that receives payments (must be a valid address)
|
||||
SERVER_WALLET_ADDRESS=0x0000000000000000000000000000000000000000
|
||||
|
||||
# Optional: use Chain 138 for payments (default: false = use Arbitrum Sepolia + USDC for testing)
|
||||
# Set to true only after a Chain 138 token supports ERC-2612/ERC-3009. See docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md
|
||||
X402_USE_CHAIN_138=false
|
||||
|
||||
# Optional: Chain 138 RPC when X402_USE_CHAIN_138=true
|
||||
RPC_URL_138=https://rpc-http-pub.d-bis.org
|
||||
|
||||
# Optional: server port (default 4020)
|
||||
PORT=4020
|
||||
3
x402-api/.gitignore
vendored
Normal file
3
x402-api/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
60
x402-api/README.md
Normal file
60
x402-api/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# x402 API (Chain 138–ready)
|
||||
|
||||
Minimal Express API that accepts [x402](https://portal.thirdweb.com/x402) payments using thirdweb’s `settlePayment` and facilitator. Configured with a custom **Chain 138** definition; by default uses **Arbitrum Sepolia** and USDC so you can test without a Chain 138 token that supports permit.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- [thirdweb](https://portal.thirdweb.com) account and **secret key** (Dashboard → Settings → API Keys)
|
||||
- A server wallet address (EOA) that will receive payments
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd x402-api
|
||||
cp .env.example .env
|
||||
# Edit .env: set THIRDWEB_SECRET_KEY and SERVER_WALLET_ADDRESS
|
||||
npm install
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
npm start
|
||||
# or with auto-reload
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- **Health (no payment):** `GET http://localhost:4020/health`
|
||||
- **Paid routes (402 until payment):** `GET http://localhost:4020/api/premium`, `GET http://localhost:4020/api/paid`
|
||||
|
||||
Clients must send payment authorization in the `PAYMENT-SIGNATURE` or `X-PAYMENT` header (e.g. using thirdweb’s `useFetchWithPayment` or equivalent).
|
||||
|
||||
## Chain 138 and token support
|
||||
|
||||
x402 requires the payment token to support **ERC-2612 permit** or **ERC-3009**. Currently, cUSDT and cUSDC on Chain 138 do **not** support these (see [CHAIN138_X402_TOKEN_SUPPORT.md](../docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md)).
|
||||
|
||||
- **Default:** The API uses **Arbitrum Sepolia** and default USDC (`price: "$0.01"`) so you can test end-to-end without Chain 138.
|
||||
- **Chain 138:** Set `X402_USE_CHAIN_138=true` and optionally `RPC_URL_138` in `.env` once a Chain 138 token has permit/ERC-3009 (e.g. after adding ERC20Permit to compliant tokens and redeploying). The server then uses the custom Chain 138 definition and the configured token for settlement.
|
||||
|
||||
Verification script for token support:
|
||||
|
||||
```bash
|
||||
./scripts/verify/check-chain138-token-permit-support.sh [RPC_URL]
|
||||
```
|
||||
|
||||
## Env reference
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `THIRDWEB_SECRET_KEY` | Yes | thirdweb API secret key |
|
||||
| `SERVER_WALLET_ADDRESS` | Yes | Address that receives x402 payments |
|
||||
| `X402_USE_CHAIN_138` | No | `true` to use Chain 138 (default `false`) |
|
||||
| `RPC_URL_138` | No | Chain 138 RPC when using Chain 138 (default public RPC) |
|
||||
| `PORT` | No | Server port (default `4020`) |
|
||||
|
||||
## References
|
||||
|
||||
- [thirdweb x402 – Server](https://portal.thirdweb.com/x402/server)
|
||||
- [CHAIN138_X402_TOKEN_SUPPORT.md](../docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md) – Which Chain 138 tokens support permit/ERC-3009
|
||||
- [CHAIN138_TOKEN_ADDRESSES.md](../docs/11-references/CHAIN138_TOKEN_ADDRESSES.md) – Token addresses on Chain 138
|
||||
16
x402-api/package.json
Normal file
16
x402-api/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "x402-api",
|
||||
"version": "1.0.0",
|
||||
"description": "Minimal x402-enabled API using thirdweb settlePayment; supports Chain 138 when a token has permit/ERC-3009.",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "node --watch src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.0",
|
||||
"thirdweb": "^5.0.0"
|
||||
}
|
||||
}
|
||||
131
x402-api/src/index.js
Normal file
131
x402-api/src/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Minimal x402-enabled Express API using thirdweb settlePayment.
|
||||
* Supports custom Chain 138; default payment chain is Arbitrum Sepolia (USDC) for testing
|
||||
* until a Chain 138 token supports ERC-2612/ERC-3009. See docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md.
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { createThirdwebClient, defineChain } from "thirdweb";
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const PORT = process.env.PORT || 4020;
|
||||
const secretKey = process.env.THIRDWEB_SECRET_KEY;
|
||||
const serverWalletAddress = process.env.SERVER_WALLET_ADDRESS;
|
||||
const useChain138 = process.env.X402_USE_CHAIN_138 === "true";
|
||||
const rpcUrl138 = process.env.RPC_URL_138 || "https://rpc-http-pub.d-bis.org";
|
||||
|
||||
/** Custom Chain 138 for thirdweb (DeFi Oracle Meta Mainnet) */
|
||||
const chain138 = defineChain({
|
||||
id: 138,
|
||||
name: "DeFi Oracle Meta Mainnet",
|
||||
rpc: rpcUrl138,
|
||||
nativeCurrency: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
},
|
||||
});
|
||||
|
||||
const client = secretKey
|
||||
? createThirdwebClient({ secretKey })
|
||||
: null;
|
||||
const thirdwebFacilitator =
|
||||
client && serverWalletAddress
|
||||
? facilitator({
|
||||
client,
|
||||
serverWalletAddress,
|
||||
})
|
||||
: null;
|
||||
|
||||
/** Resolve network: Chain 138 if enabled and token supports permit, else Arbitrum Sepolia for USDC. */
|
||||
function getNetwork() {
|
||||
if (useChain138 && thirdwebFacilitator) {
|
||||
return chain138;
|
||||
}
|
||||
return arbitrumSepolia;
|
||||
}
|
||||
|
||||
/** Price: USD string (default USDC on chain) or token object for Chain 138 when permit is available. */
|
||||
function getPrice() {
|
||||
if (useChain138) {
|
||||
// When a Chain 138 token has permit, use e.g. cUSDC: 0xf22258f57794CC8E06237084b353Ab30fFfa640b, 6 decimals
|
||||
const cusdc138 = "0xf22258f57794CC8E06237084b353Ab30fFfa640b";
|
||||
return {
|
||||
amount: "10000",
|
||||
asset: { address: cusdc138, decimals: 6 },
|
||||
};
|
||||
}
|
||||
return "$0.01";
|
||||
}
|
||||
|
||||
/** Shared handler for paid routes (PAYMENT-SIGNATURE or X-PAYMENT header). */
|
||||
async function handlePaidRoute(req, res) {
|
||||
const paymentData =
|
||||
req.headers["payment-signature"] ||
|
||||
req.headers["PAYMENT-SIGNATURE"] ||
|
||||
req.headers["x-payment"] ||
|
||||
req.headers["X-PAYMENT"];
|
||||
|
||||
if (!thirdwebFacilitator || !serverWalletAddress) {
|
||||
return res.status(503).json({
|
||||
error: "x402 not configured",
|
||||
hint: "Set THIRDWEB_SECRET_KEY and SERVER_WALLET_ADDRESS in .env",
|
||||
});
|
||||
}
|
||||
|
||||
const resourceUrl =
|
||||
(req.protocol + "://" + req.get("host") + req.originalUrl) || "";
|
||||
const method = req.method;
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
paymentData: paymentData || undefined,
|
||||
payTo: serverWalletAddress,
|
||||
network: getNetwork(),
|
||||
price: getPrice(),
|
||||
facilitator: thirdwebFacilitator,
|
||||
routeConfig: {
|
||||
description: "Access to paid API content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 60 * 60,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
return res.json({
|
||||
data: "paid content",
|
||||
message: "Payment settled successfully",
|
||||
});
|
||||
}
|
||||
res
|
||||
.status(result.status)
|
||||
.set(result.responseHeaders || {})
|
||||
.json(result.responseBody ?? { error: "Payment required" });
|
||||
}
|
||||
|
||||
/** Protected routes: require x402 payment (PAYMENT-SIGNATURE or X-PAYMENT header). */
|
||||
app.get("/api/premium", handlePaidRoute);
|
||||
app.get("/api/paid", handlePaidRoute);
|
||||
|
||||
/** Health: no payment required. */
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({
|
||||
ok: true,
|
||||
x402: !!thirdwebFacilitator,
|
||||
chain: useChain138 ? "chain138" : "arbitrumSepolia",
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`x402-api listening on port ${PORT}`);
|
||||
if (!thirdwebFacilitator) {
|
||||
console.warn("THIRDWEB_SECRET_KEY or SERVER_WALLET_ADDRESS not set; /api/premium will return 503.");
|
||||
} else {
|
||||
console.log(`Payment chain: ${useChain138 ? "Chain 138" : "Arbitrum Sepolia (default USDC)"}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user