-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathx402.ts
More file actions
126 lines (111 loc) · 4.86 KB
/
Copy pathx402.ts
File metadata and controls
126 lines (111 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
import { x402_payments_received_total } from '../metrics'
import { checkQuota, recordUsage, parseCents, getQuotaConfig } from '../x402/metering'
import fp from 'fastify-plugin'
// @ts-ignore — @x402 packages ship ESM-only types incompatible with commonjs moduleResolution
import { x402ResourceServer, HTTPFacilitatorClient } from '@x402/core/server'
// @ts-ignore
import { ExactStellarScheme } from '@x402/stellar/exact/server'
// Routes gated by x402 and their prices
const GATED_ROUTES: Record<string, { price: string; description: string }> = {
'/price': { price: '$0.10', description: 'Unified SDEX+AMM price with VWAP and best route' },
'/pools': { price: '$0.05', description: 'AMM liquidity pool reserves and spot prices' },
'/candles': { price: '$0.05', description: 'OHLCV candle data for trading charts' },
'/graphql': { price: '$0.10', description: 'GraphQL queries for price data and market information' },
}
/**
* Fastify plugin that gates matching routes behind x402 USDC micropayments.
* Only active when ORACLE_PAYMENT_ADDRESS is set in env.
*/
async function x402Plugin(app: FastifyInstance) {
// Read at plugin init time (not module load) so tests can inject env vars before app.register()
const PAYMENT_ADDRESS = process.env.ORACLE_PAYMENT_ADDRESS
const FACILITATOR_URL = process.env.X402_FACILITATOR_URL ?? 'https://facilitator.stellar.org'
const NETWORK = (process.env.STELLAR_NETWORK === 'mainnet' ? 'stellar:pubnet' : 'stellar:testnet') as string
if (!PAYMENT_ADDRESS) {
app.log.warn('[oracle] ORACLE_PAYMENT_ADDRESS not set — x402 gating disabled')
return
}
const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL })
const resourceServer: any = new x402ResourceServer(facilitatorClient)
.register(NETWORK as `${string}:${string}`, new ExactStellarScheme())
await resourceServer.initialize()
app.log.info('[oracle] x402 payment gating enabled')
app.addHook('preHandler', async (req: FastifyRequest, reply: FastifyReply) => {
// Gate GET requests on matching path prefixes, or POST requests to /graphql
const matchedRoute = Object.keys(GATED_ROUTES).find(prefix => {
const pathMatches = req.url.startsWith(prefix)
const methodAllowed = prefix === '/graphql' ? req.method === 'POST' : req.method === 'GET'
return pathMatches && methodAllowed
})
if (!matchedRoute) return
const { price, description } = GATED_ROUTES[matchedRoute]
const paymentHeader = req.headers['x-payment'] as string | undefined
const requirements = {
scheme: 'exact' as const,
price,
network: NETWORK,
payTo: PAYMENT_ADDRESS,
}
// No payment header — return 402 with requirements
if (!paymentHeader) {
reply.status(402).send({
x402Version: 1,
accepts: [requirements],
error: 'Payment required',
description,
})
return
}
// Verify the payment with the facilitator
try {
let payload: unknown
try {
payload = JSON.parse(Buffer.from(paymentHeader, 'base64').toString())
} catch {
payload = JSON.parse(paymentHeader)
}
const result = await resourceServer.verify(payload, requirements)
if (!result.isValid) {
reply.status(402).send({ error: 'Payment invalid', reason: result.invalidReason })
return
}
// Valid — increment metric
x402_payments_received_total.inc()
// Valid — enforce quota if the request carries an API key
const apiKeyId = (req as any).apiKey?.id as string | undefined
if (apiKeyId) {
const quotaOk = await checkQuota(apiKeyId)
if (!quotaOk.allowed) {
const config = await getQuotaConfig(apiKeyId)
if (config.overagePolicy === 'allow_overage') {
// Record usage and let through (overage billing applies)
await recordUsage(apiKeyId, parseCents(price))
} else if (config.overagePolicy === 'charge_402') {
reply.status(402).send({
error: 'Quota exceeded — additional payment required',
policy: 'charge_402',
})
return
} else {
// default: block
reply.status(402).send({
error: 'Quota exceeded',
policy: 'block',
})
return
}
} else {
await recordUsage(apiKeyId, parseCents(price))
}
}
// Valid — settle asynchronously and let the request through
resourceServer.settle(payload, requirements).catch((err: unknown) => {
app.log.error({ err }, '[oracle] x402 settle error')
})
} catch (err) {
reply.status(402).send({ error: 'Payment verification failed', reason: (err as Error).message })
}
})
}
export const registerX402 = fp(x402Plugin, { name: 'x402' })