@nickthelegend69/fund402 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ // Fund402 SERVER side — create x402-gated HTTP endpoints that are SETTLED BY THE
3
+ // LENDING POOL.
4
+ //
5
+ // A normal x402 paywall makes the *caller* pay from their own balance. Fund402's
6
+ // twist: the agent borrows just-in-time from the Fund402 vault, the vault (the
7
+ // liquidity pool) fronts the CEP-18 payment to YOU (the merchant), and the agent
8
+ // repays later. To you it looks like any x402 endpoint — except your callers can
9
+ // pay even with an empty wallet, because the pool settles on their behalf.
10
+ //
11
+ // This module is framework-agnostic. `paywall()` returns a tiny object you drive
12
+ // from any HTTP handler; thin adapters for Express / Hono / Next.js live in
13
+ // ./adapters and call straight into it.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.buildPaymentRequirements = buildPaymentRequirements;
16
+ exports.challengeBody = challengeBody;
17
+ exports.decodePaymentSignature = decodePaymentSignature;
18
+ exports.explorerTx = explorerTx;
19
+ exports.verifyPoolSettlement = verifyPoolSettlement;
20
+ exports.verifyWithFacilitator = verifyWithFacilitator;
21
+ exports.paywall = paywall;
22
+ const DEFAULT_NETWORK = "casper:casper-test";
23
+ function isTestnet(network) {
24
+ return network.includes("test");
25
+ }
26
+ function defaultRest(network) {
27
+ return isTestnet(network) ? "https://api.testnet.cspr.cloud" : "https://api.cspr.cloud";
28
+ }
29
+ function headerGet(headers, name) {
30
+ const want = name.toLowerCase();
31
+ for (const k of Object.keys(headers)) {
32
+ if (k.toLowerCase() === want) {
33
+ const v = headers[k];
34
+ return Array.isArray(v) ? v[0] : v ?? undefined;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ /** Build the x402 v2 `exact` PaymentRequirements for this paywall + resource. */
40
+ function buildPaymentRequirements(cfg, resource, description) {
41
+ const network = cfg.network ?? DEFAULT_NETWORK;
42
+ return {
43
+ scheme: "exact",
44
+ network,
45
+ payTo: cfg.payTo,
46
+ amount: String(BigInt(cfg.price)),
47
+ asset: cfg.asset.replace(/^0x/, ""),
48
+ resource,
49
+ description: description ?? cfg.description ?? "Fund402 x402-gated resource",
50
+ mimeType: "application/json",
51
+ maxTimeoutSeconds: cfg.maxTimeoutSeconds ?? 900,
52
+ extra: {
53
+ name: cfg.asset_meta?.name ?? "Cep18x402",
54
+ version: cfg.asset_meta?.version ?? "1",
55
+ decimals: cfg.asset_meta?.decimals ?? "9",
56
+ symbol: cfg.asset_meta?.symbol ?? "USDC",
57
+ },
58
+ };
59
+ }
60
+ /** Wrap requirements in the x402 v2 `402 Payment Required` envelope. */
61
+ function challengeBody(req) {
62
+ return { x402Version: 2, accepts: [req], error: "payment required" };
63
+ }
64
+ /** Decode a base64(JSON) x402 payment header. Returns null if malformed. */
65
+ function decodePaymentSignature(header) {
66
+ try {
67
+ return JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ /** cspr.live explorer URL for a deploy. */
74
+ function explorerTx(network, deployHash) {
75
+ return `https://cspr.live/deploy/${deployHash}?network=${isTestnet(network) ? "casper-test" : "casper"}`;
76
+ }
77
+ /**
78
+ * Verify, on-chain via CSPR.cloud, that the Fund402 vault `borrow_and_pay` deploy
79
+ * actually executed and paid the required merchant + amount. THIS is the real
80
+ * settlement proof — the pool already moved the funds to the merchant; the gateway
81
+ * trusts the chain, not the caller.
82
+ */
83
+ async function verifyPoolSettlement(cfg, deployHash, opts = {}) {
84
+ const network = cfg.network ?? DEFAULT_NETWORK;
85
+ const rest = cfg.csprCloudRest ?? defaultRest(network);
86
+ if (!cfg.csprCloudApiKey) {
87
+ return { valid: false, reason: "csprCloudApiKey not set — cannot verify settlement on-chain" };
88
+ }
89
+ if (!/^[0-9a-fA-F]{64}$/.test(deployHash)) {
90
+ return { valid: false, reason: "malformed deploy hash" };
91
+ }
92
+ // The deploy is RPC-confirmed before CSPR.cloud finishes indexing it, so a
93
+ // 404 / "pending" right after settlement is transient — poll a bounded window.
94
+ // An executed *failure* is terminal and breaks out immediately.
95
+ const tries = opts.tries ?? 12;
96
+ const intervalMs = opts.intervalMs ?? 3000;
97
+ let d;
98
+ let lastReason = "deploy not found on cspr.cloud yet";
99
+ for (let i = 0; i < tries; i++) {
100
+ try {
101
+ const res = await fetch(`${rest}/deploys/${deployHash}`, {
102
+ headers: { Authorization: cfg.csprCloudApiKey },
103
+ });
104
+ if (res.ok) {
105
+ const body = await res.json();
106
+ const cand = body?.data ?? body;
107
+ const st = cand?.status ?? "unknown";
108
+ if (cand?.error_message || /fail|error/i.test(st)) {
109
+ return { valid: false, reason: `deploy failed on-chain (status=${st})`, status: st };
110
+ }
111
+ if (st === "processed") {
112
+ d = cand;
113
+ break;
114
+ }
115
+ lastReason = `deploy not processed yet (status=${st})`;
116
+ }
117
+ else {
118
+ lastReason = `cspr.cloud /deploys ${res.status}`;
119
+ }
120
+ }
121
+ catch (e) {
122
+ lastReason = `cspr.cloud fetch failed: ${e?.message}`;
123
+ }
124
+ if (i < tries - 1)
125
+ await new Promise((r) => setTimeout(r, intervalMs));
126
+ }
127
+ if (!d)
128
+ return { valid: false, reason: lastReason };
129
+ const status = d?.status ?? "processed";
130
+ const expectAmount = String(BigInt(cfg.price));
131
+ const expectMerchant = cfg.payTo;
132
+ const args = d?.args ?? {};
133
+ const argAmount = String(args?.amount?.parsed ?? "");
134
+ const argMerchant = String(args?.merchant?.parsed ?? args?.merchant?.parsed?.Account ?? "");
135
+ const paidAmount = !argAmount || argAmount === expectAmount;
136
+ const paidMerchant = !argMerchant ||
137
+ argMerchant.toLowerCase().includes(expectMerchant.replace(/^00/, "").toLowerCase());
138
+ // The vault is addressed by its PACKAGE hash; CSPR.cloud exposes that as
139
+ // `contract_package_hash` (NOT `contract_hash`, which is the versioned contract).
140
+ const vault = (cfg.vaultContract ?? "").replace(/^(hash-|contract-package-|package-)/, "");
141
+ const onChainPkg = String(d?.contract_package_hash ?? d?.contract_hash ?? "").toLowerCase();
142
+ if (vault && onChainPkg && !onChainPkg.includes(vault.toLowerCase())) {
143
+ return { valid: false, reason: "deploy did not target the configured vault pool", status };
144
+ }
145
+ const valid = paidAmount && paidMerchant;
146
+ const paymentResponseHeader = Buffer.from(JSON.stringify({ success: valid, network, deployHash, explorer: explorerTx(network, deployHash) })).toString("base64");
147
+ return {
148
+ valid,
149
+ status,
150
+ deployHash,
151
+ settlement: { deployHash },
152
+ paymentResponseHeader,
153
+ paidMerchant,
154
+ paidAmount,
155
+ };
156
+ }
157
+ /**
158
+ * Optional defense-in-depth: ask an x402 facilitator to verify the agent's signed
159
+ * `exact` authorization. Returns `{ isValid }`. Never throws — network/decode
160
+ * problems resolve to `{ isValid:false, reason }`.
161
+ */
162
+ async function verifyWithFacilitator(facilitatorUrl, payload) {
163
+ try {
164
+ const res = await fetch(`${facilitatorUrl.replace(/\/$/, "")}/verify`, {
165
+ method: "POST",
166
+ headers: { "content-type": "application/json" },
167
+ body: JSON.stringify(payload),
168
+ });
169
+ const j = await res.json().catch(() => ({}));
170
+ return { isValid: !!(j?.isValid ?? j?.valid), reason: j?.invalidReason ?? j?.reason };
171
+ }
172
+ catch (e) {
173
+ return { isValid: false, reason: e?.message };
174
+ }
175
+ }
176
+ /**
177
+ * Create a paywall. Drive it from any framework (or use ./adapters).
178
+ *
179
+ * ```ts
180
+ * const pay = paywall({ payTo, asset, price: "1000000", vaultContract,
181
+ * csprCloudApiKey: process.env.CSPR_CLOUD_API_KEY });
182
+ * const g = await pay.guard({ url: fullUrl, headers: req.headers });
183
+ * if (!g.paid) return send(g.response); // 402 challenge / error
184
+ * res.setHeader("payment-response", g.paymentResponseHeader); // settled — serve it
185
+ * ```
186
+ */
187
+ function paywall(config) {
188
+ if (!config.payTo)
189
+ throw new Error("paywall: `payTo` (merchant account) is required");
190
+ if (!config.asset)
191
+ throw new Error("paywall: `asset` (CEP-18 package hash) is required");
192
+ const network = config.network ?? DEFAULT_NETWORK;
193
+ function challenge(resource, description) {
194
+ const req = buildPaymentRequirements(config, resource, description);
195
+ const body = challengeBody(req);
196
+ return {
197
+ status: 402,
198
+ headers: {
199
+ "content-type": "application/json",
200
+ "payment-required": Buffer.from(JSON.stringify(body)).toString("base64"),
201
+ },
202
+ body,
203
+ };
204
+ }
205
+ async function verify(paymentHeader) {
206
+ const payload = decodePaymentSignature(paymentHeader);
207
+ if (!payload)
208
+ return { valid: false, reason: "malformed payment header" };
209
+ const deployHash = payload?.payload?.settlement?.deployHash ?? payload?.settlement?.deployHash;
210
+ if (!deployHash)
211
+ return { valid: false, reason: "payment payload missing settlement.deployHash" };
212
+ const settled = await verifyPoolSettlement(config, deployHash);
213
+ if (!settled.valid)
214
+ return settled;
215
+ // Optional: also verify the signed authorization at the facilitator.
216
+ if (config.facilitatorUrl) {
217
+ const fac = await verifyWithFacilitator(config.facilitatorUrl, payload);
218
+ if (!fac.isValid) {
219
+ return { ...settled, valid: false, reason: `facilitator rejected signature: ${fac.reason}` };
220
+ }
221
+ }
222
+ return settled;
223
+ }
224
+ async function guard(req) {
225
+ const resource = req.url;
226
+ const header = headerGet(req.headers, "payment-signature") ?? headerGet(req.headers, "x-payment");
227
+ if (!header)
228
+ return { paid: false, response: challenge(resource) };
229
+ const result = await verify(header);
230
+ if (!result.valid) {
231
+ const malformed = result.reason?.startsWith("malformed");
232
+ return {
233
+ paid: false,
234
+ response: {
235
+ status: malformed ? 400 : 402,
236
+ headers: { "content-type": "application/json" },
237
+ body: malformed
238
+ ? { error: result.reason }
239
+ : { ...challengeBody(buildPaymentRequirements(config, resource)), reason: result.reason },
240
+ },
241
+ };
242
+ }
243
+ return {
244
+ paid: true,
245
+ deployHash: result.deployHash,
246
+ settlement: result,
247
+ paymentResponseHeader: result.paymentResponseHeader,
248
+ };
249
+ }
250
+ return { config: { ...config, network }, challenge, verify, guard };
251
+ }
@@ -0,0 +1,60 @@
1
+ /** CAIP-2 network id, e.g. "casper:casper-test" or "casper:casper". */
2
+ export type CasperNetwork = `casper:${string}`;
3
+ /** x402 v2 `exact` PaymentRequirements (one entry of a 402 challenge's `accepts`). */
4
+ export interface PaymentRequirements {
5
+ scheme: "exact";
6
+ network: string;
7
+ /** Merchant account, tagged: "00" + 32-byte account hash. */
8
+ payTo: string;
9
+ /** Token base units required for this call. */
10
+ amount: string;
11
+ /** CEP-18 contract **package** hash (64 hex) — the settlement asset. */
12
+ asset: string;
13
+ resource?: string;
14
+ description?: string;
15
+ mimeType?: string;
16
+ maxTimeoutSeconds?: number;
17
+ extra?: {
18
+ name?: string;
19
+ version?: string;
20
+ decimals?: string;
21
+ symbol?: string;
22
+ };
23
+ }
24
+ /** x402 v2 `402 Payment Required` body. */
25
+ export interface PaymentRequiredBody {
26
+ x402Version: 2;
27
+ accepts: PaymentRequirements[];
28
+ error?: string;
29
+ }
30
+ /** The Fund402 settlement extension carried inside the x402 payload. */
31
+ export interface Fund402Settlement {
32
+ /** The vault `borrow_and_pay` deploy hash that actually moved the funds. */
33
+ deployHash: string;
34
+ /** CEP-18 package hash that was transferred. */
35
+ asset?: string;
36
+ }
37
+ /** The decoded x402 `exact` payment payload an agent sends back (header value). */
38
+ export interface ExactPaymentPayload {
39
+ x402Version: 2;
40
+ scheme: "exact";
41
+ network: string;
42
+ resource?: {
43
+ url: string;
44
+ };
45
+ accepted?: Partial<PaymentRequirements>;
46
+ paymentRequirements?: Partial<PaymentRequirements>;
47
+ payload: {
48
+ signature: string;
49
+ publicKey: string;
50
+ authorization: {
51
+ from: string;
52
+ to: string;
53
+ value: string;
54
+ validAfter: string;
55
+ validBefore: string;
56
+ nonce: string;
57
+ };
58
+ settlement?: Fund402Settlement;
59
+ };
60
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Shared x402 types for the Fund402 SDK. The `exact` scheme over the casper:*
3
+ // network family, as the CSPR.cloud x402 facilitator expects.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@nickthelegend69/fund402",
3
+ "version": "0.1.0",
4
+ "description": "Create x402-gated HTTP endpoints settled by the Fund402 lending pool on Casper — and the agent client that pays them with just-in-time credit. Drop-in middleware for Express, Hono and Next.js.",
5
+ "keywords": [
6
+ "x402",
7
+ "casper",
8
+ "payments",
9
+ "402",
10
+ "ai-agents",
11
+ "micropayments",
12
+ "lending",
13
+ "cep18",
14
+ "stablecoin",
15
+ "paywall"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "nickthelegend",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/nickthelegend/fund402-sdk.git"
22
+ },
23
+ "homepage": "https://github.com/nickthelegend/fund402-casper",
24
+ "main": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "exports": {
27
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
28
+ "./server": { "types": "./dist/server.d.ts", "default": "./dist/server.js" },
29
+ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.js" },
30
+ "./express": { "types": "./dist/adapters/express.d.ts", "default": "./dist/adapters/express.js" },
31
+ "./hono": { "types": "./dist/adapters/hono.d.ts", "default": "./dist/adapters/hono.js" },
32
+ "./next": { "types": "./dist/adapters/next.d.ts", "default": "./dist/adapters/next.js" }
33
+ },
34
+ "files": ["dist", "README.md", "LICENSE"],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "prepublishOnly": "npm run build",
38
+ "test": "npm run build && node test/server.test.mjs && node test/eip712.test.mjs",
39
+ "test:e2e": "npm run build && node test/e2e-live.mjs"
40
+ },
41
+ "dependencies": {
42
+ "@noble/hashes": "^1.4.0",
43
+ "casper-js-sdk": "^5.0.12"
44
+ },
45
+ "peerDependencies": {
46
+ "axios": "^1.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "axios": { "optional": true }
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.20.0",
53
+ "typescript": "^5.6.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ }
58
+ }