commit 8a97fe7882e0264953b267877d3adfb11dff8bd2 Author: cryptocommuniums-afk Date: Sun Feb 1 14:52:46 2026 +0800 Initial capay platform scaffold diff --git a/README.md b/README.md new file mode 100644 index 0000000..e744c97 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Capay 一站式加密货币支付平台(自托管模板) + +本仓库提供: +- 统一商户开通(简单 Merchant API) +- x402 Facilitator(EVM/Solana)配置模板 +- BTCPay / Keagate 对接占位 +- Webhook 聚合 `/payments/webhook` +- 低开销交易跟踪(免费套餐默认低频轮询) +- Nginx 反向代理与 HTTPS(Let's Encrypt)示例 + +## 目录结构 +``` +app/ # 统一业务 API +infra/nginx/sites-enabled/ # Nginx 配置 +infra/docker/ # BTCPay/Keagate 占位 +x402/facilitator/ # x402 配置模板 +``` + +## 快速开始(App) +```bash +cd app +npm install +npm run dev +``` + +服务默认监听 `:3000`。 + +### 免费套餐低频监控 +在 `app/.env` 中已设置: +- `TX_POLL_INTERVAL_MS=180000`(3 分钟) +- `MAX_TX_CHECK_PER_CYCLE=20` +- `MIN_CONFIRMATIONS=1` + +交易跟踪采用“待确认 tx 列表 + 轮询 receipt”的低开销方式。 +支持多个 Alchemy 免费 key:使用 `ALCHEMY_API_KEYS=key1,key2`,系统会按轮询周期进行轮转调用,降低单 key 的频率。 + +## API 说明(简化版) +### 1) 开通商户 +`POST /merchants` +```json +{ "name": "Acme", "email": "ops@acme.com", "webhookUrl": "https://merchant.example.com/webhook" } +``` +返回:`merchantId`、`apiKey`、`webhookSecret` + +### 2) 创建订单 +`POST /payments/orders` +Headers: `x-merchant-id`, `x-api-key` +```json +{ "amount": "9.99", "currency": "USD", "network": "evm:ethereum", "asset": "USDC", "description": "API Access" } +``` +返回:订单信息与 `paymentRequirementsBase64`。 + +### 3) 追踪链上交易(低开销) +`POST /payments/track` +Headers: `x-merchant-id`, `x-api-key` +```json +{ "orderId": "ORD123", "txHash": "0x...", "network": "evm:ethereum" } +``` + +### 4) Webhook 聚合入口 +`POST /payments/webhook` +- `X-Source: keagate | btcpay | x402` +- Keagate 校验:`x-keagate-sig`(sha512 HMAC) +- BTCPay 校验:`btcpay-sig`(sha256 HMAC) +- x402 校验:`x402-sig`(sha256 HMAC) + +## Nginx + HTTPS(免费) +配置在:`infra/nginx/sites-enabled/capay.conf` + +证书建议使用 certbot(Let’s Encrypt): +```bash +apt-get update && apt-get install -y certbot python3-certbot-nginx +certbot --nginx -d capay.hao.work -d pay.capay.hao.work -d btc.capay.hao.work -d doge.capay.hao.work +``` + +## x402 Facilitator +配置文件在:`x402/facilitator/`,已绑定 `pay.capay.hao.work`,并写入 Alchemy ETH RPC。 + +## 生产建议 +- 把 `.env` 中的密钥替换为强随机值 +- 生产环境建议使用数据库(PostgreSQL/MySQL)替换本地 JSON +- 重要订单建议提高 `MIN_CONFIRMATIONS` +- Webhook 需做好幂等与重试 diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..40f0ff3 --- /dev/null +++ b/app/.env @@ -0,0 +1,24 @@ +APP_PORT=3000 +APP_HOST=https://capay.hao.work +PLAN=free + +# Low-cost polling for free plan +TX_POLL_INTERVAL_MS=180000 +MAX_TX_CHECK_PER_CYCLE=20 +MIN_CONFIRMATIONS=1 + +# Alchemy Ethereum RPC (supports multiple free keys) +ALCHEMY_API_KEYS=P9kZiHB6Q7CLrBlMsUN3n +ALCHEMY_API_KEY=P9kZiHB6Q7CLrBlMsUN3n +ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/P9kZiHB6Q7CLrBlMsUN3n +# Optional: comma-separated RPC URLs (overrides/extends) +# ETHEREUM_RPC_URLS=https://eth-mainnet.g.alchemy.com/v2/KEY1,https://eth-mainnet.g.alchemy.com/v2/KEY2 + +# Webhook signature secrets (replace in production) +IPN_HMAC_SECRET=replace-with-strong-secret +BTCPAY_WEBHOOK_SECRET=replace-with-strong-secret +X402_WEBHOOK_SECRET=replace-with-strong-secret + +# Payment platform +DB_PATH=./data/db.json +WEBHOOK_TIMEOUT_MS=8000 diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..8a46e60 --- /dev/null +++ b/app/package.json @@ -0,0 +1,16 @@ +{ + "name": "capay-app", + "version": "0.1.0", + "private": true, + "main": "src/server.js", + "scripts": { + "dev": "node src/server.js", + "start": "node src/server.js" + }, + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.19.2", + "ethers": "^6.12.1", + "nanoid": "^5.0.7" + } +} diff --git a/app/src/server.js b/app/src/server.js new file mode 100644 index 0000000..82ccf90 --- /dev/null +++ b/app/src/server.js @@ -0,0 +1,280 @@ +require('dotenv').config(); + +const crypto = require('crypto'); +const express = require('express'); +const { nanoid } = require('nanoid'); +const { readDb, withDb } = require('./storage'); +const { startEvmWatcher } = require('./watchers/evmWatcher'); + +const app = express(); +app.use(express.json({ limit: '1mb' })); + +const PLAN = process.env.PLAN || 'free'; + +function nowIso() { + return new Date().toISOString(); +} + +function base64Json(data) { + return Buffer.from(JSON.stringify(data)).toString('base64'); +} + +function getHeader(req, name) { + const key = Object.keys(req.headers).find((header) => header.toLowerCase() === name.toLowerCase()); + return key ? req.headers[key] : undefined; +} + +async function requireMerchant(req, res) { + const merchantId = getHeader(req, 'x-merchant-id'); + const apiKey = getHeader(req, 'x-api-key'); + if (!merchantId || !apiKey) { + res.status(401).json({ error: 'missing merchant auth headers' }); + return null; + } + const db = await readDb(); + const merchant = db.merchants.find((item) => item.id === merchantId && item.apiKey === apiKey); + if (!merchant) { + res.status(403).json({ error: 'invalid merchant credentials' }); + return null; + } + return merchant; +} + +async function logEvent(source, payload) { + await withDb((db) => { + db.events.push({ + id: nanoid(), + source, + payload, + createdAt: nowIso() + }); + }); +} + +async function notifyMerchant(order, tx) { + if (!order) return; + const db = await readDb(); + const merchant = db.merchants.find((item) => item.id === order.merchantId); + if (!merchant || !merchant.webhookUrl) return; + + const payload = { + type: 'payment.confirmed', + orderId: order.id, + txHash: tx.txHash, + network: tx.network, + amount: order.amount, + currency: order.currency, + asset: order.asset, + confirmedAt: nowIso() + }; + + const body = JSON.stringify(payload); + const signature = crypto.createHmac('sha256', merchant.webhookSecret).update(body).digest('hex'); + + const timeoutMs = Number(process.env.WEBHOOK_TIMEOUT_MS || 8000); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + await fetch(merchant.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Capay-Signature': signature + }, + body, + signal: controller.signal + }); + } catch (error) { + console.warn('[webhook] failed to notify merchant', merchant.id, error.message); + } finally { + clearTimeout(timeout); + } +} + +app.get('/health', (req, res) => { + res.json({ status: 'ok', plan: PLAN }); +}); + +app.post('/merchants', async (req, res) => { + const { name, email, webhookUrl } = req.body || {}; + if (!name || !email) { + return res.status(400).json({ error: 'name and email are required' }); + } + + const merchant = { + id: nanoid(), + name, + email, + webhookUrl: webhookUrl || null, + apiKey: nanoid(32), + webhookSecret: nanoid(32), + createdAt: nowIso() + }; + + await withDb((db) => { + db.merchants.push(merchant); + }); + + res.status(201).json({ + merchantId: merchant.id, + apiKey: merchant.apiKey, + webhookSecret: merchant.webhookSecret + }); +}); + +app.get('/merchants/:id', async (req, res) => { + const db = await readDb(); + const merchant = db.merchants.find((item) => item.id === req.params.id); + if (!merchant) return res.status(404).json({ error: 'merchant not found' }); + const safe = { ...merchant }; + delete safe.apiKey; + delete safe.webhookSecret; + res.json(safe); +}); + +app.post('/payments/orders', async (req, res) => { + const merchant = await requireMerchant(req, res); + if (!merchant) return; + + const { amount, currency, network, asset, description, lockWindowSeconds, slippage, orderId } = req.body || {}; + if (!amount || !currency || !network || !asset) { + return res.status(400).json({ error: 'amount, currency, network, asset are required' }); + } + + const order = { + id: orderId || nanoid(), + merchantId: merchant.id, + amount: String(amount), + currency: String(currency), + network: String(network), + asset: String(asset), + description: description || null, + status: 'pending', + createdAt: nowIso(), + updatedAt: nowIso() + }; + + await withDb((db) => { + db.orders.push(order); + }); + + const paymentRequirements = { + requirements: [ + { + kind: 'exact', + network: order.network, + asset: order.asset, + amount: order.amount, + currency: order.currency, + description: order.description || 'Capay order' + } + ], + lockWindowSeconds: Number(lockWindowSeconds || 1200), + slippage: Number(slippage || 0.02), + orderId: order.id + }; + + const base64 = base64Json(paymentRequirements); + + res.status(201).json({ + order, + paymentRequirements, + paymentRequirementsBase64: base64 + }); +}); + +app.post('/payments/track', async (req, res) => { + const merchant = await requireMerchant(req, res); + if (!merchant) return; + + const { orderId, txHash, network } = req.body || {}; + if (!orderId || !txHash || !network) { + return res.status(400).json({ error: 'orderId, txHash, network are required' }); + } + + const txRecord = { + id: nanoid(), + orderId, + network, + txHash, + status: 'pending', + confirmations: 0, + lastCheckedAt: null, + createdAt: nowIso(), + updatedAt: nowIso() + }; + + await withDb((db) => { + db.txs.push(txRecord); + }); + + res.status(201).json({ tracked: true, tx: txRecord }); +}); + +app.get('/payments/orders/:id', async (req, res) => { + const merchant = await requireMerchant(req, res); + if (!merchant) return; + + const db = await readDb(); + const order = db.orders.find((item) => item.id === req.params.id && item.merchantId === merchant.id); + if (!order) return res.status(404).json({ error: 'order not found' }); + res.json(order); +}); + +app.post('/payments/webhook', async (req, res) => { + const source = (getHeader(req, 'x-source') || '').toLowerCase(); + + if (!source) { + return res.status(400).json({ error: 'missing X-Source header' }); + } + + if (source === 'keagate') { + const secret = process.env.IPN_HMAC_SECRET || ''; + const sorted = JSON.stringify(req.body, Object.keys(req.body || {}).sort()); + const hmac = crypto.createHmac('sha512', secret).update(sorted).digest('hex'); + const sig = getHeader(req, 'x-keagate-sig'); + if (!sig || sig !== hmac) { + return res.status(403).json({ error: 'invalid keagate signature' }); + } + await logEvent('keagate', req.body); + } else if (source === 'btcpay') { + const secret = process.env.BTCPAY_WEBHOOK_SECRET || ''; + const payload = JSON.stringify(req.body || {}); + const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex'); + const sig = getHeader(req, 'btcpay-sig') || getHeader(req, 'btcpay-signature'); + if (sig && sig !== expected) { + return res.status(403).json({ error: 'invalid btcpay signature' }); + } + await logEvent('btcpay', req.body); + } else if (source === 'x402') { + const secret = process.env.X402_WEBHOOK_SECRET || ''; + const payload = JSON.stringify(req.body || {}); + const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex'); + const sig = getHeader(req, 'x402-sig'); + if (sig && sig !== expected) { + return res.status(403).json({ error: 'invalid x402 signature' }); + } + await logEvent('x402', req.body); + } else { + return res.status(400).json({ error: 'unknown source' }); + } + + const orderId = req.body?.orderId || req.body?.metadata?.orderId; + if (orderId) { + await withDb((db) => { + const order = db.orders.find((item) => item.id === orderId); + if (order) { + order.status = 'paid'; + order.updatedAt = nowIso(); + } + }); + } + + res.status(200).json({ received: true }); +}); + +const port = Number(process.env.APP_PORT || 3000); +app.listen(port, () => { + console.log(`capay app listening on :${port}`); + startEvmWatcher({ notifyMerchant }); +}); diff --git a/app/src/storage.js b/app/src/storage.js new file mode 100644 index 0000000..902a32f --- /dev/null +++ b/app/src/storage.js @@ -0,0 +1,56 @@ +const fs = require('fs/promises'); +const path = require('path'); + +const DEFAULT_DB = { + merchants: [], + orders: [], + txs: [], + events: [] +}; + +function resolveDbPath() { + const dbPath = process.env.DB_PATH || './data/db.json'; + if (path.isAbsolute(dbPath)) return dbPath; + return path.join(process.cwd(), dbPath); +} + +async function ensureDbFile() { + const dbPath = resolveDbPath(); + await fs.mkdir(path.dirname(dbPath), { recursive: true }); + try { + await fs.access(dbPath); + } catch (error) { + await fs.writeFile(dbPath, JSON.stringify(DEFAULT_DB, null, 2)); + } +} + +async function readDb() { + await ensureDbFile(); + const dbPath = resolveDbPath(); + const raw = await fs.readFile(dbPath, 'utf-8'); + try { + return JSON.parse(raw); + } catch (error) { + return { ...DEFAULT_DB }; + } +} + +async function writeDb(db) { + const dbPath = resolveDbPath(); + const tmpPath = `${dbPath}.tmp`; + await fs.writeFile(tmpPath, JSON.stringify(db, null, 2)); + await fs.rename(tmpPath, dbPath); +} + +async function withDb(mutator) { + const db = await readDb(); + const result = await mutator(db); + await writeDb(db); + return result; +} + +module.exports = { + readDb, + writeDb, + withDb +}; diff --git a/app/src/utils/rpc.js b/app/src/utils/rpc.js new file mode 100644 index 0000000..6757916 --- /dev/null +++ b/app/src/utils/rpc.js @@ -0,0 +1,50 @@ +function parseList(value) { + if (!value) return []; + return value + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} + +function buildAlchemyUrls(keys) { + return keys.map((key) => `https://eth-mainnet.g.alchemy.com/v2/${key}`); +} + +function unique(items) { + return Array.from(new Set(items)); +} + +function getEthereumRpcUrls() { + const urls = []; + + const explicitUrls = parseList(process.env.ETHEREUM_RPC_URLS); + urls.push(...explicitUrls); + + if (process.env.ETHEREUM_RPC_URL) { + urls.push(process.env.ETHEREUM_RPC_URL); + } + + const keyList = parseList(process.env.ALCHEMY_API_KEYS); + if (keyList.length > 0) { + urls.push(...buildAlchemyUrls(keyList)); + } else if (process.env.ALCHEMY_API_KEY) { + urls.push(...buildAlchemyUrls([process.env.ALCHEMY_API_KEY])); + } + + return unique(urls); +} + +function createRoundRobinPicker(items) { + let index = 0; + return function pick() { + if (!items.length) return null; + const item = items[index % items.length]; + index = (index + 1) % items.length; + return item; + }; +} + +module.exports = { + getEthereumRpcUrls, + createRoundRobinPicker +}; diff --git a/app/src/watchers/evmWatcher.js b/app/src/watchers/evmWatcher.js new file mode 100644 index 0000000..762ea83 --- /dev/null +++ b/app/src/watchers/evmWatcher.js @@ -0,0 +1,76 @@ +const { JsonRpcProvider } = require('ethers'); +const { readDb, writeDb } = require('../storage'); +const { getEthereumRpcUrls, createRoundRobinPicker } = require('../utils/rpc'); + +function startEvmWatcher({ notifyMerchant }) { + const rpcUrls = getEthereumRpcUrls(); + if (!rpcUrls.length) { + console.warn('[evmWatcher] No Ethereum RPC URLs configured. Skipping watcher.'); + return; + } + + const pollInterval = Number(process.env.TX_POLL_INTERVAL_MS || 180000); + const maxPerCycle = Number(process.env.MAX_TX_CHECK_PER_CYCLE || 20); + const minConfirmations = Number(process.env.MIN_CONFIRMATIONS || 1); + const pickRpcUrl = createRoundRobinPicker(rpcUrls); + + let running = false; + + async function poll() { + if (running) return; + running = true; + try { + const db = await readDb(); + const pending = db.txs.filter((tx) => tx.network === 'evm:ethereum' && tx.status === 'pending'); + if (pending.length === 0) { + running = false; + return; + } + + const rpcUrl = pickRpcUrl(); + const provider = new JsonRpcProvider(rpcUrl); + const latestBlock = await provider.getBlockNumber(); + const now = new Date().toISOString(); + const confirmedEvents = []; + + for (const tx of pending.slice(0, maxPerCycle)) { + const receipt = await provider.getTransactionReceipt(tx.txHash); + tx.lastCheckedAt = now; + + if (!receipt) continue; + + const confirmations = latestBlock - receipt.blockNumber + 1; + tx.confirmations = confirmations; + tx.updatedAt = now; + + if (receipt.status === 1 && confirmations >= minConfirmations) { + tx.status = 'confirmed'; + const order = db.orders.find((item) => item.id === tx.orderId); + if (order) { + order.status = 'paid'; + order.updatedAt = now; + confirmedEvents.push({ order, tx }); + } + } else if (receipt.status === 0) { + tx.status = 'failed'; + } + } + + await writeDb(db); + + for (const event of confirmedEvents) { + await notifyMerchant(event.order, event.tx); + } + } catch (error) { + console.error('[evmWatcher] poll error', error.message); + } finally { + running = false; + } + } + + poll(); + setInterval(poll, pollInterval); + console.log(`[evmWatcher] started with interval ${pollInterval}ms`); +} + +module.exports = { startEvmWatcher }; diff --git a/infra/docker/btcpay/.env.example b/infra/docker/btcpay/.env.example new file mode 100644 index 0000000..8d5eb29 --- /dev/null +++ b/infra/docker/btcpay/.env.example @@ -0,0 +1,6 @@ +BTCPAY_HOST=btc.capay.hao.work +LETSENCRYPT_EMAIL=ops@capay.hao.work +BTCPAYGEN_EXCLUDE_FRAGMENTS=nginx-https +BTCPAYGEN_ADDITIONAL_FRAGMENTS=opt-save-storage,bitcoin +BTCPAYGEN_LIGHTNING=lnd +NBITCOIN_NETWORK=mainnet diff --git a/infra/docker/keagate/local.json b/infra/docker/keagate/local.json new file mode 100644 index 0000000..2925f21 --- /dev/null +++ b/infra/docker/keagate/local.json @@ -0,0 +1,26 @@ +{ + "HOST": "https://doge.capay.hao.work", + "PORT": 8081, + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "MONGO_KEAGATE_DB": "keagate", + + "IP_WHITELIST": ["127.0.0.1"], + "TRANSACTION_TIMEOUT": 1200000, + "TRANSACTION_MIN_REFRESH_TIME": 30000, + "TRANSACTION_SLIPPAGE_TOLERANCE": 0.02, + + "SEED": "hex-128-bit-seed", + "KEAGATE_API_KEY": "replace-api-key", + "INVOICE_ENC_KEY": "hex-32-bytes", + "IPN_HMAC_SECRET": "replace-hmac-secret", + + "DOGE": { + "ADMIN_PUBLIC_KEY": "DogePublicAddress", + "ADMIN_PRIVATE_KEY": null + }, + + "SOL": { + "ADMIN_PUBLIC_KEY": "SolPublicAddress", + "ADMIN_PRIVATE_KEY": null + } +} diff --git a/infra/nginx/sites-enabled/capay.conf b/infra/nginx/sites-enabled/capay.conf new file mode 100644 index 0000000..448bed3 --- /dev/null +++ b/infra/nginx/sites-enabled/capay.conf @@ -0,0 +1,80 @@ +server { + listen 80; + server_name capay.hao.work pay.capay.hao.work btc.capay.hao.work doge.capay.hao.work; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name capay.hao.work; + + # ssl_certificate /etc/letsencrypt/live/capay.hao.work/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/capay.hao.work/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 443 ssl http2; + server_name pay.capay.hao.work; + + # ssl_certificate /etc/letsencrypt/live/pay.capay.hao.work/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/pay.capay.hao.work/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location /verify { + proxy_pass http://127.0.0.1:4020/verify; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /settle { + proxy_pass http://127.0.0.1:4020/settle; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 443 ssl http2; + server_name btc.capay.hao.work; + + # ssl_certificate /etc/letsencrypt/live/btc.capay.hao.work/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/btc.capay.hao.work/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://127.0.0.1:23000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 443 ssl http2; + server_name doge.capay.hao.work; + + # ssl_certificate /etc/letsencrypt/live/doge.capay.hao.work/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/doge.capay.hao.work/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/x402/facilitator/.env b/x402/facilitator/.env new file mode 100644 index 0000000..0b3f7e9 --- /dev/null +++ b/x402/facilitator/.env @@ -0,0 +1,17 @@ +PORT=4020 +HOST=https://pay.capay.hao.work +LOG_LEVEL=info + +FACILITATOR_JWT_SECRET=replace-with-strong-secret +PAYMENT_HMAC_SECRET=replace-with-strong-secret + +# Alchemy Ethereum RPC +ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/P9kZiHB6Q7CLrBlMsUN3n + +# Optional BSC RPC if needed +BSC_RPC_URL=https://bsc-dataseed.binance.org + +# Solana RPC (optional) +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com + +REDIS_URL=redis://127.0.0.1:6379 diff --git a/x402/facilitator/networks.json b/x402/facilitator/networks.json new file mode 100644 index 0000000..18c9ca7 --- /dev/null +++ b/x402/facilitator/networks.json @@ -0,0 +1,21 @@ +{ + "evm": { + "ethereum": { + "chainId": 1, + "rpcEnv": "ETHEREUM_RPC_URL", + "explorer": "https://etherscan.io/tx/" + }, + "bsc": { + "chainId": 56, + "rpcEnv": "BSC_RPC_URL", + "explorer": "https://bscscan.com/tx/" + } + }, + "svm": { + "solana": { + "cluster": "mainnet-beta", + "rpcEnv": "SOLANA_RPC_URL", + "explorer": "https://solscan.io/tx/" + } + } +} diff --git a/x402/facilitator/tokens.json b/x402/facilitator/tokens.json new file mode 100644 index 0000000..e05e574 --- /dev/null +++ b/x402/facilitator/tokens.json @@ -0,0 +1,21 @@ +{ + "evm": { + "ethereum": { + "USDC": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6 }, + "USDT": { "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "decimals": 6 }, + "ETH": { "native": true, "decimals": 18 } + }, + "bsc": { + "USDC": { "address": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", "decimals": 18 }, + "USDT": { "address": "0x55d398326f99059fF775485246999027B3197955", "decimals": 18 }, + "BNB": { "native": true, "decimals": 18 } + } + }, + "svm": { + "solana": { + "USDC": { "mint": "EPjFWdd5AufqSSqeM2qE1z3vY2Z9K5xkqkQ3yqC4wR5Z", "decimals": 6 }, + "USDT": { "mint": "Es9vMFrzaCERzmxEtpmJieE5s3bD4ZjbFj9a2yq6VQ8G", "decimals": 6 }, + "SOL": { "native": true, "decimals": 9 } + } + } +}