diff --git a/apps/api/apps/api/data/db.json b/apps/api/apps/api/data/db.json deleted file mode 100644 index 83e849d..0000000 --- a/apps/api/apps/api/data/db.json +++ /dev/null @@ -1,362 +0,0 @@ -{ - "usage": [ - { - "id": "use_4Lobxvue84", - "mode": "scrape", - "endpoint": "/x402/scrape", - "providerId": "scrape.page", - "queryOrUrl": "https://ethereum.org/", - "priceUsd": 0.02, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_H-iaFvtF50Cl", - "createdAt": "2026-04-13T14:12:18.604Z", - "latencyMs": 1134 - }, - { - "id": "use_0HeP4FcgB1", - "mode": "scrape", - "endpoint": "/x402/scrape", - "providerId": "scrape.page", - "queryOrUrl": "https://ethereum.org/", - "priceUsd": 0.02, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_LVVtKvxldwDp", - "createdAt": "2026-04-13T14:03:23.561Z", - "latencyMs": 1310 - }, - { - "id": "use_WpIXOuyb1s", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.pro", - "queryOrUrl": "ethereum news", - "priceUsd": 0.02, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_L_ZhlLrnLiiK", - "createdAt": "2026-04-13T14:02:43.619Z", - "latencyMs": 1015 - }, - { - "id": "use_rzJKhozxL6", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.pro", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.02, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_YLemAiF1Jt7J", - "createdAt": "2026-04-13T12:35:10.845Z", - "latencyMs": 1201 - }, - { - "id": "use_xwViQT1Z1Y", - "mode": "scrape", - "endpoint": "/x402/scrape", - "providerId": "scrape.extract", - "queryOrUrl": "https://developers.stellar.org", - "priceUsd": 0.04, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_4yEpOBKooFMO", - "createdAt": "2026-04-12T22:26:59.707Z", - "latencyMs": 1674 - }, - { - "id": "use_P9f5qDsVHQ", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.basic", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.01, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_qdHv5ztwxJoJ", - "createdAt": "2026-04-12T22:26:16.493Z", - "latencyMs": 749 - }, - { - "id": "use_X4wz7SSFCW", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.pro", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.02, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_J_0DflTVTcB2", - "createdAt": "2026-04-12T21:50:20.895Z", - "latencyMs": 1151 - }, - { - "id": "use_ZVpN6uENcZ", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.basic", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.01, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_hokNqu0VvyLH", - "createdAt": "2026-04-12T21:48:34.398Z", - "latencyMs": 658 - }, - { - "id": "use_FE8gE2Z7mD", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.basic", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.01, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_xO4Xtq0oy_jX", - "createdAt": "2026-04-12T21:14:52.981Z", - "latencyMs": 821 - }, - { - "id": "use_cjhQbYbeNV", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.basic", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.01, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_pWh4Ef76QAjp", - "createdAt": "2026-04-12T21:08:44.130Z", - "latencyMs": 750 - }, - { - "id": "use_24CtIczMZv", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.basic", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.01, - "network": "stellar:testnet", - "paymentStatus": "paid", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "traceId": "trace_UCyX-SJnHlEh", - "createdAt": "2026-04-12T18:11:28.924Z", - "latencyMs": 616 - }, - { - "id": "use_fVzoPFCU0N", - "mode": "scrape", - "endpoint": "/x402/scrape", - "providerId": "scrape.extract", - "queryOrUrl": "https://developers.stellar.org", - "priceUsd": 0.04, - "network": "stellar:testnet", - "paymentStatus": "paid", - "paymentTxHash": "demo_tx_L1l4IAO_yL", - "facilitatorUrl": "https://facilitator.x402.org", - "traceId": "trace_SBmVHmUX9kp6", - "createdAt": "2026-04-12T15:35:40.177Z", - "latencyMs": 1783 - }, - { - "id": "use_hQIfdnmvYY", - "mode": "news", - "endpoint": "/x402/news", - "providerId": "news.deep", - "queryOrUrl": "stablecoin micropayments", - "priceUsd": 0.03, - "network": "stellar:testnet", - "paymentStatus": "paid", - "paymentTxHash": "demo_tx_4ofN2v3X37", - "facilitatorUrl": "https://facilitator.x402.org", - "traceId": "trace_aJl_JZP5cfjt", - "createdAt": "2026-04-12T15:35:38.387Z", - "latencyMs": 1490 - }, - { - "id": "use_KVvzTfDgb-", - "mode": "search", - "endpoint": "/x402/search", - "providerId": "search.pro", - "queryOrUrl": "latest stellar x402 updates", - "priceUsd": 0.02, - "network": "stellar:testnet", - "paymentStatus": "paid", - "paymentTxHash": "demo_tx_MS1JuypwLu", - "facilitatorUrl": "https://facilitator.x402.org", - "traceId": "trace_g2VMaEhrfy2u", - "createdAt": "2026-04-12T15:35:36.888Z", - "latencyMs": 1070 - } - ], - "payments": [ - { - "id": "pay_JmqVA-Chnq", - "endpoint": "/x402/scrape", - "providerId": "scrape.page", - "amountUsd": 0.02, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-13T14:12:18.604Z" - }, - { - "id": "pay_Lzam9hIlBB", - "endpoint": "/x402/scrape", - "providerId": "scrape.page", - "amountUsd": 0.02, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-13T14:03:23.561Z" - }, - { - "id": "pay_SrtUSToCX6", - "endpoint": "/x402/search", - "providerId": "search.pro", - "amountUsd": 0.02, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-13T14:02:43.619Z" - }, - { - "id": "pay_PqGl4AsNY3", - "endpoint": "/x402/search", - "providerId": "search.pro", - "amountUsd": 0.02, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-13T12:35:10.845Z" - }, - { - "id": "pay_5dGgiHgllc", - "endpoint": "/x402/scrape", - "providerId": "scrape.extract", - "amountUsd": 0.04, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T22:26:59.707Z" - }, - { - "id": "pay_INjnj7_1ya", - "endpoint": "/x402/search", - "providerId": "search.basic", - "amountUsd": 0.01, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T22:26:16.493Z" - }, - { - "id": "pay_4XPmT_PffZ", - "endpoint": "/x402/search", - "providerId": "search.pro", - "amountUsd": 0.02, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T21:50:20.895Z" - }, - { - "id": "pay_hfouY1xCGe", - "endpoint": "/x402/search", - "providerId": "search.basic", - "amountUsd": 0.01, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T21:48:34.398Z" - }, - { - "id": "pay_SSTBxrAgI7", - "endpoint": "/x402/search", - "providerId": "search.basic", - "amountUsd": 0.01, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T21:14:52.981Z" - }, - { - "id": "pay_V6s-Cer7Bh", - "endpoint": "/x402/search", - "providerId": "search.basic", - "amountUsd": 0.01, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T21:08:44.130Z" - }, - { - "id": "pay_ZNS-38N5TC", - "endpoint": "/x402/search", - "providerId": "search.basic", - "amountUsd": 0.01, - "network": "stellar:testnet", - "payToAddress": "GC5TXYP4F7ZEDLDFYBIFG67KU4UUPOS2IESICQYMSZYOY6GN5MIOUW3P", - "facilitatorUrl": "https://channels.openzeppelin.com/x402/testnet", - "status": "settled", - "createdAt": "2026-04-12T18:11:28.924Z" - }, - { - "id": "pay_JZvxvv3ycP", - "endpoint": "/x402/scrape", - "providerId": "scrape.extract", - "amountUsd": 0.04, - "network": "stellar:testnet", - "payToAddress": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "facilitatorUrl": "https://facilitator.x402.org", - "status": "settled", - "transactionHash": "demo_tx_L1l4IAO_yL", - "createdAt": "2026-04-12T15:35:40.177Z" - }, - { - "id": "pay_-iyfWPFukZ", - "endpoint": "/x402/news", - "providerId": "news.deep", - "amountUsd": 0.03, - "network": "stellar:testnet", - "payToAddress": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "facilitatorUrl": "https://facilitator.x402.org", - "status": "settled", - "transactionHash": "demo_tx_4ofN2v3X37", - "createdAt": "2026-04-12T15:35:38.387Z" - }, - { - "id": "pay_Lyd0pssE6d", - "endpoint": "/x402/search", - "providerId": "search.pro", - "amountUsd": 0.02, - "network": "stellar:testnet", - "payToAddress": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "facilitatorUrl": "https://facilitator.x402.org", - "status": "settled", - "transactionHash": "demo_tx_MS1JuypwLu", - "createdAt": "2026-04-12T15:35:36.888Z" - } - ] -} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 73ef387..832f722 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,7 +10,7 @@ "build": "tsc -p tsconfig.json", "start": "node dist/index.js", "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "vitest run && node --import tsx --test src/providers/registry.test.ts src/lib/scrape-url-safety.test.ts src/services/query-service.test.ts", + "test": "vitest run && node --import tsx --test tests/**/*.test.ts src/providers/registry.test.ts src/lib/scrape-url-safety.test.ts src/services/query-service.test.ts", "test:watch": "vitest", "lint": "echo 'No lint configured for api'", "format": "echo 'No format task configured for api'" diff --git a/apps/api/src/lib/persistence.ts b/apps/api/src/lib/persistence.ts index 069414f..6f95d29 100644 --- a/apps/api/src/lib/persistence.ts +++ b/apps/api/src/lib/persistence.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { nanoid } from "nanoid"; -import type { AnalyticsSummary, PaymentAttempt, PaymentSource, QueryMode, UsageEvent } from "@query402/shared"; +import type { AnalyticsSummary, PaymentAttempt, PaymentSource, QueryMode, UsageEvent, PaymentEvidence } from "@query402/shared"; import { config } from "./config.js"; interface PersistedDb { @@ -47,6 +47,38 @@ export function savePaymentAttempt(payment: PaymentAttempt) { writeDb(db); } +export function updatePaymentAttemptEvidence(id: string, evidence: PaymentEvidence) { + const db = readDb(); + const index = db.payments.findIndex(p => p.id === id); + if (index !== -1) { + db.payments[index].evidence = evidence; + writeDb(db); + } +} + +export function updateUsageEventEvidence(traceId: string, evidence: PaymentEvidence) { + const db = readDb(); + const index = db.usage.findIndex(u => u.traceId === traceId); + if (index !== -1) { + db.usage[index].evidence = evidence; + writeDb(db); + } +} + +export function updateUsageEventsByPaymentId(paymentId: string, evidence: PaymentEvidence) { + const db = readDb(); + let updated = false; + db.usage.forEach(u => { + if (u.paymentId === paymentId) { + u.evidence = evidence; + updated = true; + } + }); + if (updated) { + writeDb(db); + } +} + export function getUsageEvents() { return readDb().usage; } @@ -96,17 +128,22 @@ export function persistSponsoredPayment(input: { const paymentSource = input.paymentSource ?? "sponsored"; const sponsorPublicKey = input.sponsorPublicKey ?? config.DEMO_CLIENT_PUBLIC_KEY; + const evidence: PaymentEvidence = { + status: "settled", + network: config.STELLAR_NETWORK, + amountUsd: input.priceUsd, + payToAddress: config.X402_PAY_TO_ADDRESS, + facilitatorUrl: config.X402_FACILITATOR_URL, + payerPublicKey: input.walletPublicKey, + transactionHash: input.paymentResponseHeader ?? "sponsored_tx", + paymentPayload: "sponsored" + }; + savePaymentAttempt({ id: paymentId, endpoint: input.endpoint, providerId: input.provider, - amountUsd: input.priceUsd, - network: config.STELLAR_NETWORK, - payerPublicKey: input.walletPublicKey, - payToAddress: config.X402_PAY_TO_ADDRESS, - facilitatorUrl: config.X402_FACILITATOR_URL, - status: "settled", - transactionHash: input.paymentResponseHeader ?? undefined, + evidence, createdAt: now, sponsorshipGrantId: input.sponsorshipGrantId, policyDecision: input.policyDecision, @@ -121,12 +158,9 @@ export function persistSponsoredPayment(input: { providerId: input.provider, queryOrUrl: input.queryOrUrl, priceUsd: input.priceUsd, - network: config.STELLAR_NETWORK, - paymentStatus: "paid", - paymentTxHash: input.paymentResponseHeader ?? undefined, - facilitatorUrl: config.X402_FACILITATOR_URL, - payerPublicKey: input.walletPublicKey, + evidence, traceId: input.traceId, + paymentId, createdAt: now, latencyMs: input.latencyMs, sponsorshipGrantId: input.sponsorshipGrantId, diff --git a/apps/api/src/lib/x402.ts b/apps/api/src/lib/x402.ts index 92ea079..f3e0a9e 100644 --- a/apps/api/src/lib/x402.ts +++ b/apps/api/src/lib/x402.ts @@ -5,6 +5,8 @@ import type { NextFunction, Request, Response } from "express"; import type { HTTPRequestContext } from "@x402/core/server"; import { getProviderById, protectedRouteBasePrices } from "./pricing.js"; import { config } from "./config.js"; +import { savePaymentAttempt, updatePaymentAttemptEvidence, updateUsageEventsByPaymentId } from "./persistence.js"; +import { nanoid } from "nanoid"; type RouteMode = "search" | "news" | "scrape"; @@ -53,6 +55,30 @@ function demoMode402Middleware(req: Request, res: Response, next: NextFunction) const paymentResponse = req.header("payment-response"); if (paidHeader === "true" || typeof paymentResponse === "string") { + const paymentId = `pay_${nanoid(10)}`; + req.headers["x-payment-attempt-id"] = paymentId; + + const routeKey = `${req.method.toUpperCase()} ${req.path}`; + const price = protectedRouteBasePrices[routeKey] ?? "$0.01"; + + const rawProvider = req.query.provider; + const providerId = typeof rawProvider === "string" ? rawProvider : (Array.isArray(rawProvider) ? rawProvider[0] : "unknown"); + + savePaymentAttempt({ + id: paymentId, + endpoint: req.path, + providerId: providerId as string, + evidence: { + status: "demo-paid", + network: config.STELLAR_NETWORK, + amountUsd: Number(price.replace("$", "")), + payToAddress: config.X402_PAY_TO_ADDRESS, + facilitatorUrl: config.X402_FACILITATOR_URL, + demoId: paymentResponse ?? "demo-success" + }, + createdAt: new Date().toISOString() + }); + return next(); } @@ -74,6 +100,76 @@ function demoMode402Middleware(req: Request, res: Response, next: NextFunction) }); } +export const getX402LifecycleHandlers = (network: string) => ({ + onAfterVerify: async (ctx: any) => { + const transport = ctx.transportContext as { request?: Request, req?: Request } | Request; + const req = ('headers' in transport) ? transport : (transport.request || transport.req); + + if (req) { + const paymentId = `pay_${nanoid(10)}`; + req.headers["x-payment-attempt-id"] = paymentId; + + const providerId = getProviderFromContext(ctx.transportContext) ?? "unknown"; + + savePaymentAttempt({ + id: paymentId, + endpoint: req.path, + providerId, + evidence: { + status: "verified", + network, + amountUsd: Number(ctx.requirements.amount), + payToAddress: ctx.requirements.payTo, + facilitatorUrl: config.X402_FACILITATOR_URL, + paymentPayload: typeof ctx.paymentPayload === "string" ? ctx.paymentPayload : JSON.stringify(ctx.paymentPayload) + }, + createdAt: new Date().toISOString() + }); + } + }, + + onAfterSettle: async (ctx: any) => { + const transport = ctx.transportContext as { request?: Request, req?: Request } | Request; + const req = ('headers' in transport) ? transport : (transport.request || transport.req); + const paymentId = req?.headers?.["x-payment-attempt-id"] as string | undefined; + + if (paymentId) { + const evidence = { + status: "settled" as const, + network, + amountUsd: Number(ctx.requirements.amount), + payToAddress: ctx.requirements.payTo, + facilitatorUrl: config.X402_FACILITATOR_URL, + transactionHash: ctx.result.transaction, + paymentPayload: typeof ctx.paymentPayload === "string" ? ctx.paymentPayload : JSON.stringify(ctx.paymentPayload) + }; + updatePaymentAttemptEvidence(paymentId, evidence); + updateUsageEventsByPaymentId(paymentId, evidence); + } + }, + + onSettleFailure: async (ctx: any) => { + const transport = ctx.transportContext as { request?: Request, req?: Request } | Request; + const req = ('headers' in transport) ? transport : (transport.request || transport.req); + const paymentId = req?.headers?.["x-payment-attempt-id"] as string | undefined; + + if (paymentId) { + const evidence = { + status: "failed" as const, + network, + amountUsd: Number(ctx.requirements.amount), + payToAddress: ctx.requirements.payTo, + facilitatorUrl: config.X402_FACILITATOR_URL, + error: ctx.error?.message ?? "Payment settlement failed", + paymentPayload: ctx.paymentPayload ? (typeof ctx.paymentPayload === "string" ? ctx.paymentPayload : JSON.stringify(ctx.paymentPayload)) : undefined + }; + + updatePaymentAttemptEvidence(paymentId, evidence); + updateUsageEventsByPaymentId(paymentId, evidence); + } + } +}); + export function createX402Middleware() { if (config.demoMode) { return demoMode402Middleware; @@ -103,6 +199,11 @@ export function createX402Middleware() { new ExactStellarScheme() ); + const handlers = getX402LifecycleHandlers(network); + resourceServer.onAfterVerify(handlers.onAfterVerify); + resourceServer.onAfterSettle(handlers.onAfterSettle); + resourceServer.onSettleFailure(handlers.onSettleFailure); + const routeConfig = { "GET /x402/search": { accepts: { diff --git a/apps/api/src/routes/protected.ts b/apps/api/src/routes/protected.ts index 9250212..82a2ca3 100644 --- a/apps/api/src/routes/protected.ts +++ b/apps/api/src/routes/protected.ts @@ -3,11 +3,13 @@ import { nanoid } from "nanoid"; import { searchQuerySchema, newsQuerySchema, scrapeQuerySchema } from "@query402/shared"; import { executeQuery } from "../services/query-service.js"; import { config } from "../lib/config.js"; -import { savePaymentAttempt, saveUsageEvent } from "../lib/persistence.js"; +import { getPaymentAttempts, saveUsageEvent } from "../lib/persistence.js"; +import type { Request } from "express"; export const protectedRouter = Router(); -function persistPaidRequest(input: { +function persistUsageEvent(input: { + req: Request; mode: "search" | "news" | "scrape"; endpoint: string; provider: string; @@ -15,25 +17,14 @@ function persistPaidRequest(input: { priceUsd: number; latencyMs: number; traceId: string; - paymentResponseHeader: string | null; - payerPublicKey?: string; }) { - const now = new Date().toISOString(); - const paymentId = `pay_${nanoid(10)}`; + const paymentId = input.req.header("x-payment-attempt-id"); + if (!paymentId) throw new Error("Payment attempt ID missing from request headers"); - savePaymentAttempt({ - id: paymentId, - endpoint: input.endpoint, - providerId: input.provider, - amountUsd: input.priceUsd, - network: config.STELLAR_NETWORK, - payerPublicKey: input.payerPublicKey, - payToAddress: config.X402_PAY_TO_ADDRESS, - facilitatorUrl: config.X402_FACILITATOR_URL, - status: "settled", - transactionHash: input.paymentResponseHeader ?? undefined, - createdAt: now - }); + const paymentAttempt = getPaymentAttempts().find(p => p.id === paymentId); + if (!paymentAttempt) throw new Error("Payment attempt not found"); + + const now = new Date().toISOString(); saveUsageEvent({ id: `use_${nanoid(10)}`, @@ -42,15 +33,14 @@ function persistPaidRequest(input: { providerId: input.provider, queryOrUrl: input.queryOrUrl, priceUsd: input.priceUsd, - network: config.STELLAR_NETWORK, - paymentStatus: "paid", - paymentTxHash: input.paymentResponseHeader ?? undefined, - facilitatorUrl: config.X402_FACILITATOR_URL, - payerPublicKey: input.payerPublicKey, + evidence: paymentAttempt.evidence, traceId: input.traceId, + paymentId: paymentId, createdAt: now, latencyMs: input.latencyMs }); + + return { paymentId, evidence: paymentAttempt.evidence }; } protectedRouter.get("/x402/search", async (req, res, next) => { @@ -66,24 +56,26 @@ protectedRouter.get("/x402/search", async (req, res, next) => { q: parsed.data.q }); - const paymentHeader = req.header("payment-response") ?? null; - persistPaidRequest({ + const { paymentId, evidence } = persistUsageEvent({ + req, mode: "search", endpoint: "/x402/search", provider: parsed.data.provider, queryOrUrl: parsed.data.q, priceUsd: result.priceUsd, latencyMs: result.latencyMs, - traceId: result.traceId, - paymentResponseHeader: paymentHeader, - payerPublicKey: req.header("x-demo-payer") ?? undefined + traceId: result.traceId }); + res.setHeader("x-payment-attempt-id", paymentId); + res.setHeader("x-payment-trace-id", result.traceId); + return res.json({ payment: { network: config.STELLAR_NETWORK, facilitatorUrl: config.X402_FACILITATOR_URL, - paymentResponseHeader: paymentHeader + paymentResponseHeader: req.header("payment-response") ?? null, + evidence }, result }); @@ -105,24 +97,26 @@ protectedRouter.get("/x402/news", async (req, res, next) => { q: parsed.data.q }); - const paymentHeader = req.header("payment-response") ?? null; - persistPaidRequest({ + const { paymentId, evidence } = persistUsageEvent({ + req, mode: "news", endpoint: "/x402/news", provider: parsed.data.provider, queryOrUrl: parsed.data.q, priceUsd: result.priceUsd, latencyMs: result.latencyMs, - traceId: result.traceId, - paymentResponseHeader: paymentHeader, - payerPublicKey: req.header("x-demo-payer") ?? undefined + traceId: result.traceId }); + res.setHeader("x-payment-attempt-id", paymentId); + res.setHeader("x-payment-trace-id", result.traceId); + return res.json({ payment: { network: config.STELLAR_NETWORK, facilitatorUrl: config.X402_FACILITATOR_URL, - paymentResponseHeader: paymentHeader + paymentResponseHeader: req.header("payment-response") ?? null, + evidence }, result }); @@ -144,24 +138,26 @@ protectedRouter.get("/x402/scrape", async (req, res, next) => { url: parsed.data.url }); - const paymentHeader = req.header("payment-response") ?? null; - persistPaidRequest({ + const { paymentId, evidence } = persistUsageEvent({ + req, mode: "scrape", endpoint: "/x402/scrape", provider: parsed.data.provider, queryOrUrl: parsed.data.url, priceUsd: result.priceUsd, latencyMs: result.latencyMs, - traceId: result.traceId, - paymentResponseHeader: paymentHeader, - payerPublicKey: req.header("x-demo-payer") ?? undefined + traceId: result.traceId }); + res.setHeader("x-payment-attempt-id", paymentId); + res.setHeader("x-payment-trace-id", result.traceId); + return res.json({ payment: { network: config.STELLAR_NETWORK, facilitatorUrl: config.X402_FACILITATOR_URL, - paymentResponseHeader: paymentHeader + paymentResponseHeader: req.header("payment-response") ?? null, + evidence }, result }); diff --git a/apps/api/tests/setup.ts b/apps/api/tests/setup.ts new file mode 100644 index 0000000..ddcd0b4 --- /dev/null +++ b/apps/api/tests/setup.ts @@ -0,0 +1,2 @@ +process.env.NODE_ENV = "test"; +process.env.X402_PAY_TO_ADDRESS = "GDUMMY12345678901234567890123456789012345678901234567890"; diff --git a/apps/api/tests/x402.test.ts b/apps/api/tests/x402.test.ts new file mode 100644 index 0000000..b9db49f --- /dev/null +++ b/apps/api/tests/x402.test.ts @@ -0,0 +1,233 @@ +import "./setup.js"; +import test from "node:test"; +import assert from "node:assert"; +import { saveUsageEvent, getPaymentAttempts, getUsageEvents } from "../src/lib/persistence.js"; +import { getX402LifecycleHandlers } from "../src/lib/x402.js"; +import { config } from "../src/lib/config.js"; + +test("evidence pipeline updates correctly on settlement", async (t) => { + const req = { + path: "/x402/search", + headers: {} as Record + }; + + const verifyCtx = { + transportContext: { + req, + request: req, + adapter: { getQueryParam: (key: string) => key === "provider" ? "search.basic" : undefined } + }, + requirements: { amount: "0.01", payTo: "G..." }, + paymentPayload: "raw_header" + }; + + const handlers = getX402LifecycleHandlers(config.STELLAR_NETWORK as string); + + // 1. Simulate onAfterVerify which creates the payment attempt + await handlers.onAfterVerify(verifyCtx); + + const paymentId = req.headers["x-payment-attempt-id"]; + assert.ok(paymentId, "onAfterVerify should set x-payment-attempt-id"); + + const paymentAfterVerify = getPaymentAttempts().find(p => p.id === paymentId); + assert.strictEqual(paymentAfterVerify?.evidence.status, "verified"); + + // 2. Simulate the endpoint handler persisting the usage event + const traceId = "trace_test_456"; + saveUsageEvent({ + id: "use_test_123", + mode: "search", + endpoint: "/x402/search", + providerId: "search.basic", + queryOrUrl: "test", + priceUsd: 0.01, + evidence: paymentAfterVerify.evidence, + traceId, + paymentId, + createdAt: new Date().toISOString(), + latencyMs: 100 + }); + + const usageAfterVerify = getUsageEvents().find(u => u.traceId === traceId); + assert.strictEqual(usageAfterVerify?.evidence.status, "verified"); + + // 3. Simulate onAfterSettle updating the payment + const settleCtx = { + transportContext: { req, request: req }, + requirements: { amount: "0.01", payTo: "G..." }, + result: { transaction: "tx_hash_123" }, + paymentPayload: "raw_header" + }; + + await handlers.onAfterSettle(settleCtx); + + const paymentAfterSettle = getPaymentAttempts().find(p => p.id === paymentId); + const usageAfterSettle = getUsageEvents().find(u => u.traceId === traceId); + + assert.strictEqual(paymentAfterSettle?.evidence.status, "settled"); + if (paymentAfterSettle?.evidence.status === "settled") { + assert.strictEqual(paymentAfterSettle.evidence.transactionHash, "tx_hash_123"); + } + + assert.strictEqual(usageAfterSettle?.evidence.status, "settled"); + if (usageAfterSettle?.evidence.status === "settled") { + assert.strictEqual(usageAfterSettle.evidence.transactionHash, "tx_hash_123"); + } +}); + +test("evidence pipeline updates correctly on failure", async (t) => { + const req = { + path: "/x402/news", + headers: {} as Record + }; + + const verifyCtx = { + transportContext: { + req, + request: req, + adapter: { getQueryParam: (key: string) => key === "provider" ? "news.basic" : undefined } + }, + requirements: { amount: "0.015", payTo: "G..." }, + paymentPayload: "raw_header_2" + }; + + const handlers = getX402LifecycleHandlers(config.STELLAR_NETWORK as string); + + // 1. Simulate onAfterVerify which creates the payment attempt + await handlers.onAfterVerify(verifyCtx); + + const paymentId = req.headers["x-payment-attempt-id"]; + assert.ok(paymentId, "onAfterVerify should set x-payment-attempt-id"); + + const paymentAfterVerify = getPaymentAttempts().find(p => p.id === paymentId); + assert.strictEqual(paymentAfterVerify?.evidence.status, "verified"); + + // 2. Simulate the endpoint handler persisting the usage event + const traceId = "trace_test_789"; + saveUsageEvent({ + id: "use_test_456", + mode: "news", + endpoint: "/x402/news", + providerId: "news.basic", + queryOrUrl: "test", + priceUsd: 0.015, + evidence: paymentAfterVerify.evidence, + traceId, + paymentId, + createdAt: new Date().toISOString(), + latencyMs: 120 + }); + + // 3. Simulate onSettleFailure updating the payment and usage + const failCtx = { + transportContext: { req, request: req }, + requirements: { amount: "0.015", payTo: "G..." }, + error: new Error("Insufficient funds"), + paymentPayload: "raw_header_2" + }; + + await handlers.onSettleFailure(failCtx); + + const paymentAfterFail = getPaymentAttempts().find(p => p.id === paymentId); + const usageAfterFail = getUsageEvents().find(u => u.traceId === traceId); + + assert.strictEqual(paymentAfterFail?.evidence.status, "failed"); + if (paymentAfterFail?.evidence.status === "failed") { + assert.strictEqual(paymentAfterFail.evidence.error, "Insufficient funds"); + } + + assert.strictEqual(usageAfterFail?.evidence.status, "failed"); + if (usageAfterFail?.evidence.status === "failed") { + assert.strictEqual(usageAfterFail.evidence.error, "Insufficient funds"); + } +}); + +test("proof that forged demo headers cannot bypass real verification when demoMode is false", async (t) => { + const originalDemoMode = config.demoMode; + config.demoMode = false; + + // create middleware + const { createX402Middleware } = await import("../src/lib/x402.js"); + const middleware = createX402Middleware(); + + const req = { + method: "GET", + path: "/x402/search", + header: (k: string) => { + if (k === "x-query402-demo-paid") return "true"; + if (k === "payment-response") return "demo_tx_forged"; + return undefined; + }, + headers: {}, + query: { provider: "search.basic" } + }; + + let statusCode = 200; + let jsonResponse: any = null; + const res = { + status: (code: number) => { + statusCode = code; + return { json: (data: any) => { jsonResponse = data; } }; + } + }; + + let nextCalled = false; + const next = () => { nextCalled = true; }; + + await new Promise((resolve) => { + // Cast middleware to handle our mock req/res + (middleware as any)(req, res, () => { + nextCalled = true; + resolve(); + }); + // the real middleware will likely call res.status(402).json(...) synchronously or async + setTimeout(resolve, 50); + }); + + assert.strictEqual(nextCalled, false, "Should not call next() for real verification with forged headers"); + assert.strictEqual(statusCode, 402, "Should return 402 Payment Required"); + assert.ok(jsonResponse?.error === "Payment Required", "Should require payment"); + + config.demoMode = originalDemoMode; +}); + +test("demo flow creates demo-paid attempt when demoMode is true", async (t) => { + const originalDemoMode = config.demoMode; + config.demoMode = true; + + const { createX402Middleware } = await import("../src/lib/x402.js"); + const middleware = createX402Middleware(); + + const req = { + method: "GET", + path: "/x402/news", + header: (k: string) => { + if (k === "x-query402-demo-paid") return "true"; + if (k === "payment-response") return "demo_tx_123"; + return undefined; + }, + headers: {}, + query: { provider: "news.basic" } + }; + + const res = {}; + let nextCalled = false; + const next = () => { nextCalled = true; }; + + (middleware as any)(req, res, next); + + assert.strictEqual(nextCalled, true, "Should call next() in demo mode with headers"); + + const paymentId = (req.headers as any)["x-payment-attempt-id"]; + assert.ok(paymentId, "Should generate a payment attempt ID"); + + const payment = getPaymentAttempts().find(p => p.id === paymentId); + assert.ok(payment, "Payment attempt should be saved"); + assert.strictEqual(payment?.evidence.status, "demo-paid"); + if (payment?.evidence.status === "demo-paid") { + assert.strictEqual(payment.evidence.demoId, "demo_tx_123"); + } + + config.demoMode = originalDemoMode; +}); + diff --git a/apps/web/src/pages/ControlDeckPage.tsx b/apps/web/src/pages/ControlDeckPage.tsx index df0c893..4394f21 100644 --- a/apps/web/src/pages/ControlDeckPage.tsx +++ b/apps/web/src/pages/ControlDeckPage.tsx @@ -332,7 +332,17 @@ export default function ControlDeckPage() {
-

payment-response: {result.payment.paymentResponseHeader ?? ""}

+

evidence: {result.payment.evidence.status}

+

+ payload:{" "} + {result.payment.evidence.status === "demo-paid" + ? result.payment.evidence.demoId + : result.payment.evidence.status === "settled" + ? result.payment.evidence.transactionHash + : result.payment.evidence.status === "failed" + ? result.payment.evidence.error + : result.payment.paymentResponseHeader ?? ""} +

network: {result.payment.network}

@@ -387,7 +397,9 @@ export default function ControlDeckPage() { {tx.providerId} {money(tx.amountUsd)}

- {new Date(tx.createdAt).toLocaleString()} + + {tx.evidence.status.toUpperCase()} · {new Date(tx.createdAt).toLocaleString()} + ))} @@ -403,7 +415,7 @@ export default function ControlDeckPage() { {usage.latencyMs}ms

- {money(usage.priceUsd)} · {new Date(usage.createdAt).toLocaleString()} + {money(usage.priceUsd)} · {usage.evidence.status.toUpperCase()} · {new Date(usage.createdAt).toLocaleString()} ))} diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index 35dbdbf..f6fb63b 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -1,10 +1,11 @@ -import type { ProviderDefinition, QueryMode, QueryResult } from "@query402/shared"; +import type { ProviderDefinition, QueryMode, PaymentEvidence, QueryResult } from "@query402/shared"; export interface PaidQueryResponse { payment: { network: string; facilitatorUrl: string; paymentResponseHeader: string | null; + evidence: PaymentEvidence; }; result: QueryResult; } @@ -18,7 +19,7 @@ export interface AnalyticsResponse { amountUsd: number; endpoint: string; providerId: string; - status: string; + evidence: PaymentEvidence; createdAt: string; }>; recentUsage: Array<{ @@ -28,7 +29,7 @@ export interface AnalyticsResponse { priceUsd: number; createdAt: string; latencyMs: number; - paymentStatus: string; + evidence: PaymentEvidence; traceId: string; }>; } diff --git a/package-lock.json b/package-lock.json index f9a1331..144e70d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "typescript": "^5.8.3" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.0.0 <25.0.0" } }, "apps/agent-client": { @@ -395,6 +395,448 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 141df92..701ba52 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -35,6 +35,46 @@ export interface QueryResult { raw?: Record; } +export type PaymentEvidenceStatus = "demo-paid" | "verified" | "settled" | "failed"; + +export interface BasePaymentEvidence { + status: PaymentEvidenceStatus; + network: string; + amountUsd: number; + payToAddress: string; + facilitatorUrl: string; + payerPublicKey?: string; + error?: string; +} + +export interface DemoPaymentEvidence extends BasePaymentEvidence { + status: "demo-paid"; + demoId: string; +} + +export interface VerifiedPaymentEvidence extends BasePaymentEvidence { + status: "verified"; + paymentPayload: string; +} + +export interface SettledPaymentEvidence extends BasePaymentEvidence { + status: "settled"; + transactionHash: string; + paymentPayload: string; +} + +export interface FailedPaymentEvidence extends BasePaymentEvidence { + status: "failed"; + error: string; + paymentPayload?: string; +} + +export type PaymentEvidence = + | DemoPaymentEvidence + | VerifiedPaymentEvidence + | SettledPaymentEvidence + | FailedPaymentEvidence; + export interface UsageEvent { id: string; mode: QueryMode; @@ -42,12 +82,9 @@ export interface UsageEvent { providerId: string; queryOrUrl: string; priceUsd: number; - network: string; - paymentStatus: "paid" | "failed" | "demo-paid"; - paymentTxHash?: string; - facilitatorUrl?: string; - payerPublicKey?: string; + evidence: PaymentEvidence; traceId: string; + paymentId: string; createdAt: string; latencyMs: number; sponsorshipGrantId?: string; @@ -60,14 +97,7 @@ export interface PaymentAttempt { id: string; endpoint: string; providerId: string; - amountUsd: number; - network: string; - payerPublicKey?: string; - payToAddress: string; - facilitatorUrl: string; - status: "verified" | "settled" | "failed"; - transactionHash?: string; - error?: string; + evidence: PaymentEvidence; createdAt: string; sponsorshipGrantId?: string; policyDecision?: string;