crumb-alpha-sdk 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.
@@ -0,0 +1,4 @@
1
+
2
+ > crumb-alpha-sdk@0.1.0 build /Users/taylorferran/Desktop/dev/crumb-sdk/packages/sdk
3
+ > tsc
4
+
@@ -0,0 +1,32 @@
1
+
2
+ 
3
+ > @crumb/sdk@0.1.0 test /Users/taylorferran/Desktop/dev/crumb-sdk/packages/sdk
4
+ > vitest run
5
+
6
+ [?25l
7
+  RUN  v4.0.18 /Users/taylorferran/Desktop/dev/crumb-sdk/packages/sdk
8
+
9
+ [?2026h
10
+  ❯ test/provider.test.ts [queued]
11
+
12
+  Test Files 0 passed (3)
13
+  Tests 0 passed (0)
14
+  Start at 11:08:54
15
+  Duration 101ms
16
+ [?2026l[?2026h ✓ test/middleware.test.ts (4 tests) 2ms
17
+ ✓ test/provider.test.ts (6 tests) 3ms
18
+
19
+  ❯ test/client.test.ts 0/15
20
+
21
+  Test Files 2 passed (3)
22
+  Tests 10 passed (25)
23
+  Start at 11:08:54
24
+  Duration 302ms
25
+ [?2026l ✓ test/client.test.ts (15 tests) 25ms
26
+
27
+  Test Files  3 passed (3)
28
+  Tests  25 passed (25)
29
+  Start at  11:08:54
30
+  Duration  400ms (transform 129ms, setup 0ms, import 354ms, tests 31ms, environment 0ms)
31
+
32
+ [?25h
@@ -0,0 +1,49 @@
1
+ import { GatewayClient } from 'crumb-alpha-core';
2
+ import type { FetchOptions, FetchResult, SupportedChainName, Balances } from 'crumb-alpha-core';
3
+ export declare class Crumb {
4
+ private wallet;
5
+ private gateway;
6
+ private chain;
7
+ private lowBalanceHook?;
8
+ private lowBalanceThreshold;
9
+ constructor(config: {
10
+ privateKey: string;
11
+ chain?: SupportedChainName;
12
+ rpcUrl?: string;
13
+ lowBalanceThreshold?: number;
14
+ onLowBalance?: (balance: string) => void;
15
+ });
16
+ get address(): string;
17
+ /**
18
+ * Pay for an x402-protected resource. Handles the full 402 flow automatically.
19
+ */
20
+ fetch<T = unknown>(url: string, options?: FetchOptions): Promise<FetchResult<T>>;
21
+ /**
22
+ * Deposit USDC into Gateway for gasless payments.
23
+ */
24
+ deposit(amount: string): Promise<import("@circle-fin/x402-batching/client").DepositResult>;
25
+ /**
26
+ * Withdraw USDC from Gateway.
27
+ */
28
+ withdraw(amount: string, options?: {
29
+ chain?: SupportedChainName;
30
+ recipient?: string;
31
+ }): Promise<import("@circle-fin/x402-batching/client").WithdrawResult>;
32
+ /**
33
+ * Get all balances (wallet + gateway).
34
+ */
35
+ balances(): Promise<Balances>;
36
+ /**
37
+ * Get gateway available balance as a formatted string.
38
+ */
39
+ balance(): Promise<string>;
40
+ /**
41
+ * Check if a URL supports Gateway batching.
42
+ */
43
+ supports(url: string): Promise<import("@circle-fin/x402-batching/client").SupportsResult>;
44
+ onLowBalance(threshold: number, fn: (balance: string) => void): void;
45
+ /** Access the underlying GatewayClient for advanced operations. */
46
+ get client(): GatewayClient;
47
+ private checkLowBalance;
48
+ }
49
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAKd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAEV,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACT,MAAM,kBAAkB,CAAA;AAEzB,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,cAAc,CAAC,CAA2B;IAClD,OAAO,CAAC,mBAAmB,CAAc;gBAE7B,MAAM,EAAE;QAClB,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,kBAAkB,CAAA;QAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KACzC;IAmBD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IA8B1F;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM;IAI5B;;OAEG;IACG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QACvC,KAAK,CAAC,EAAE,kBAAkB,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB;IAOD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAWnC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IAWhC;;OAEG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM;IAI1B,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAK7D,mEAAmE;IACnE,IAAI,MAAM,IAAI,aAAa,CAE1B;YAEa,eAAe;CAW9B"}
package/dist/client.js ADDED
@@ -0,0 +1,122 @@
1
+ import { GatewayClient, checkPriceCeiling, deriveAddress, } from 'crumb-alpha-core';
2
+ export class Crumb {
3
+ wallet;
4
+ gateway;
5
+ chain;
6
+ lowBalanceHook;
7
+ lowBalanceThreshold = 1.0;
8
+ constructor(config) {
9
+ this.chain = config.chain ?? 'arcTestnet';
10
+ this.wallet = {
11
+ address: deriveAddress(config.privateKey),
12
+ privateKey: config.privateKey,
13
+ };
14
+ this.gateway = new GatewayClient({
15
+ chain: this.chain,
16
+ privateKey: config.privateKey,
17
+ rpcUrl: config.rpcUrl,
18
+ });
19
+ if (config.lowBalanceThreshold !== undefined) {
20
+ this.lowBalanceThreshold = config.lowBalanceThreshold;
21
+ }
22
+ if (config.onLowBalance) {
23
+ this.lowBalanceHook = config.onLowBalance;
24
+ }
25
+ }
26
+ get address() {
27
+ return this.wallet.address;
28
+ }
29
+ /**
30
+ * Pay for an x402-protected resource. Handles the full 402 flow automatically.
31
+ */
32
+ async fetch(url, options = {}) {
33
+ if (options.maxPayment !== undefined) {
34
+ // Pre-check: probe the URL to see the price before paying
35
+ const supported = await this.gateway.supports(url);
36
+ if (supported.supported && supported.requirements) {
37
+ const amount = supported.requirements?.amount;
38
+ if (amount) {
39
+ checkPriceCeiling(amount, options.maxPayment);
40
+ }
41
+ }
42
+ }
43
+ const result = await this.gateway.pay(url, {
44
+ method: options.method,
45
+ body: options.body,
46
+ headers: options.headers,
47
+ });
48
+ // Check low balance after payment
49
+ this.checkLowBalance();
50
+ return {
51
+ data: result.data,
52
+ amount: result.amount,
53
+ formattedAmount: result.formattedAmount,
54
+ txHash: result.transaction,
55
+ status: result.status,
56
+ };
57
+ }
58
+ /**
59
+ * Deposit USDC into Gateway for gasless payments.
60
+ */
61
+ async deposit(amount) {
62
+ return this.gateway.deposit(amount);
63
+ }
64
+ /**
65
+ * Withdraw USDC from Gateway.
66
+ */
67
+ async withdraw(amount, options) {
68
+ return this.gateway.withdraw(amount, {
69
+ chain: options?.chain,
70
+ recipient: options?.recipient,
71
+ });
72
+ }
73
+ /**
74
+ * Get all balances (wallet + gateway).
75
+ */
76
+ async balances() {
77
+ const result = await this.gateway.getBalances();
78
+ const available = result.gateway.formattedAvailable;
79
+ if (parseFloat(available) < this.lowBalanceThreshold) {
80
+ this.lowBalanceHook?.(available);
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Get gateway available balance as a formatted string.
86
+ */
87
+ async balance() {
88
+ const result = await this.gateway.getBalances();
89
+ const available = result.gateway.formattedAvailable;
90
+ if (parseFloat(available) < this.lowBalanceThreshold) {
91
+ this.lowBalanceHook?.(available);
92
+ }
93
+ return available;
94
+ }
95
+ /**
96
+ * Check if a URL supports Gateway batching.
97
+ */
98
+ async supports(url) {
99
+ return this.gateway.supports(url);
100
+ }
101
+ onLowBalance(threshold, fn) {
102
+ this.lowBalanceThreshold = threshold;
103
+ this.lowBalanceHook = fn;
104
+ }
105
+ /** Access the underlying GatewayClient for advanced operations. */
106
+ get client() {
107
+ return this.gateway;
108
+ }
109
+ async checkLowBalance() {
110
+ try {
111
+ const result = await this.gateway.getBalances();
112
+ const available = parseFloat(result.gateway.formattedAvailable);
113
+ if (available < this.lowBalanceThreshold) {
114
+ this.lowBalanceHook?.(result.gateway.formattedAvailable);
115
+ }
116
+ }
117
+ catch {
118
+ // Don't fail the payment if balance check fails
119
+ }
120
+ }
121
+ }
122
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { Crumb } from './client.js';
2
+ export { createProvider } from './provider.js';
3
+ export type { GatewayMiddleware, PaymentRequest } from './provider.js';
4
+ export { crumbMiddleware } from './middleware/express.js';
5
+ export type { CrumbWallet, SettlementResult, FetchOptions, FetchResult, MiddlewareConfig, SupportedChainName, Balances, PayResult, } from 'crumb-alpha-core';
6
+ export { CrumbError, GatewayClient, CHAIN_CONFIGS, GATEWAY_DOMAINS, } from 'crumb-alpha-core';
7
+ export type { CrumbErrorCode } from 'crumb-alpha-core';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAGnC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGtE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,SAAS,GACV,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Agent-side
2
+ export { Crumb } from './client.js';
3
+ // Provider-side
4
+ export { createProvider } from './provider.js';
5
+ // Express middleware (convenience re-export)
6
+ export { crumbMiddleware } from './middleware/express.js';
7
+ export { CrumbError, GatewayClient, CHAIN_CONFIGS, GATEWAY_DOMAINS, } from 'crumb-alpha-core';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAEnC,gBAAgB;AAChB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,6CAA6C;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAczD,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,30 @@
1
+ import type { MiddlewareConfig } from 'crumb-alpha-core';
2
+ /**
3
+ * Express middleware that gates routes behind x402 payments via Circle Gateway.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { crumbMiddleware } from 'crumb-alpha-sdk/middleware/express'
8
+ *
9
+ * const gateway = crumbMiddleware({ sellerAddress: '0x...' })
10
+ *
11
+ * app.get('/resource', gateway.require('$0.01'), (req, res) => {
12
+ * console.log('Payer:', req.payment?.payer)
13
+ * res.json({ data: 'paid content' })
14
+ * })
15
+ * ```
16
+ */
17
+ export declare function crumbMiddleware(config: MiddlewareConfig): {
18
+ require: (price: string) => (req: import("@circle-fin/x402-batching/server").PaymentRequest, res: import("@circle-fin/x402-batching/server").PaymentResponse, next: (err?: unknown) => void) => void | Promise<void>;
19
+ verify: (payment: unknown) => Promise<{
20
+ valid: boolean;
21
+ payer?: string;
22
+ error?: string;
23
+ }>;
24
+ settle: (payment: unknown) => Promise<{
25
+ success: boolean;
26
+ transaction?: string;
27
+ error?: string;
28
+ }>;
29
+ };
30
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB;qBASnC,MAAM;;;aAkBspO,CAAC;aAAuB,CAAC;;;;mBAA6J,CAAC;aAAuB,CAAC;;EAD/3O"}
@@ -0,0 +1,43 @@
1
+ import { createGatewayMiddleware } from '@circle-fin/x402-batching/server';
2
+ /**
3
+ * Express middleware that gates routes behind x402 payments via Circle Gateway.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { crumbMiddleware } from 'crumb-alpha-sdk/middleware/express'
8
+ *
9
+ * const gateway = crumbMiddleware({ sellerAddress: '0x...' })
10
+ *
11
+ * app.get('/resource', gateway.require('$0.01'), (req, res) => {
12
+ * console.log('Payer:', req.payment?.payer)
13
+ * res.json({ data: 'paid content' })
14
+ * })
15
+ * ```
16
+ */
17
+ export function crumbMiddleware(config) {
18
+ const gateway = createGatewayMiddleware({
19
+ sellerAddress: config.sellerAddress,
20
+ networks: config.networks,
21
+ facilitatorUrl: config.facilitatorUrl,
22
+ description: config.description,
23
+ });
24
+ return {
25
+ require: (price) => {
26
+ const mw = gateway.require(price);
27
+ if (config.onPayment) {
28
+ return async (req, res, next) => {
29
+ await mw(req, res, () => {
30
+ if (req.payment) {
31
+ config.onPayment(req.payment);
32
+ }
33
+ next();
34
+ });
35
+ };
36
+ }
37
+ return mw;
38
+ },
39
+ verify: gateway.verify,
40
+ settle: gateway.settle,
41
+ };
42
+ }
43
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAA;AAG1E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,MAAwB;IACtD,MAAM,OAAO,GAAG,uBAAuB,CAAC;QACtC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAA;IAEF,OAAO;QACL,OAAO,EAAE,CAAC,KAAa,EAAE,EAAE;YACzB,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACjC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACrB,OAAO,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;oBAC7C,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;wBACtB,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;4BAChB,MAAM,CAAC,SAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;wBAChC,CAAC;wBACD,IAAI,EAAE,CAAA;oBACR,CAAC,CAAC,CAAA;gBACJ,CAAC,CAAA;YACH,CAAC;YACD,OAAO,EAAE,CAAA;QACX,CAAC;QACD,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAA;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { MiddlewareConfig } from 'crumb-alpha-core';
2
+ import type { FastifyPluginAsync } from 'fastify';
3
+ declare module 'fastify' {
4
+ interface FastifyRequest {
5
+ crumbPayment?: import('crumb-alpha-core').SettlementResult;
6
+ }
7
+ }
8
+ export declare const crumbPlugin: FastifyPluginAsync<MiddlewareConfig>;
9
+ //# sourceMappingURL=fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAKnD,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAA;AAE/E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,YAAY,CAAC,EAAE,OAAO,aAAa,EAAE,gBAAgB,CAAA;KACtD;CACF;AAcD,eAAO,MAAM,WAAW,EAAE,kBAAkB,CAAC,gBAAgB,CAyC5D,CAAA"}
@@ -0,0 +1,44 @@
1
+ import { verifyAndSettle, CrumbError, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_USDC, } from '@crumb/core';
2
+ import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, } from '@x402/core/http';
3
+ function buildRequirement(config) {
4
+ return {
5
+ scheme: 'exact',
6
+ network: config.network ?? BASE_SEPOLIA_NETWORK,
7
+ amount: config.price,
8
+ asset: config.asset ?? BASE_SEPOLIA_USDC,
9
+ payTo: config.address,
10
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? 300,
11
+ extra: {},
12
+ };
13
+ }
14
+ export const crumbPlugin = async (fastify, config) => {
15
+ const requirement = buildRequirement(config);
16
+ const paymentRequired = {
17
+ x402Version: 2,
18
+ accepts: [requirement],
19
+ resource: {},
20
+ };
21
+ fastify.addHook('preHandler', async (request, reply) => {
22
+ const paymentHeader = request.headers['x-payment'];
23
+ if (!paymentHeader) {
24
+ reply.header('X-Payment-Required', encodePaymentRequiredHeader(paymentRequired));
25
+ return reply.status(402).send(paymentRequired);
26
+ }
27
+ try {
28
+ const paymentPayload = decodePaymentSignatureHeader(paymentHeader);
29
+ const result = await verifyAndSettle(paymentPayload, requirement, config.facilitatorUrl);
30
+ request.crumbPayment = result;
31
+ config.onPayment?.(result);
32
+ }
33
+ catch (err) {
34
+ if (err instanceof CrumbError) {
35
+ return reply.status(402).send({
36
+ error: err.code,
37
+ detail: err.detail,
38
+ });
39
+ }
40
+ throw err;
41
+ }
42
+ });
43
+ };
44
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AASxB,SAAS,gBAAgB,CAAC,MAAwB;IAChD,OAAO;QACL,MAAM,EAAE,OAAO;QACf,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,oBAAoB;QAC/C,MAAM,EAAE,MAAM,CAAC,KAAK;QACpB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,iBAAiB;QACxC,KAAK,EAAE,MAAM,CAAC,OAAO;QACrB,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,GAAG;QAClD,KAAK,EAAE,EAAE;KACV,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAyC,KAAK,EACpE,OAAO,EACP,MAAM,EACN,EAAE;IACF,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC5C,MAAM,eAAe,GAAG;QACtB,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,QAAQ,EAAE,EAAE;KACb,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACnF,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAuB,CAAA;QAExE,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,KAAK,CAAC,MAAM,CACV,oBAAoB,EACpB,2BAA2B,CAAC,eAAsB,CAAC,CACpD,CAAA;YACD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;QAChD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,4BAA4B,CAAC,aAAa,CAAC,CAAA;YAClE,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,cAAc,EACd,WAAkB,EAClB,MAAM,CAAC,cAAc,CACtB,CAAA;YACD,OAAO,CAAC,YAAY,GAAG,MAAM,CAAA;YAC7B,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;QAC5B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;gBAC9B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC5B,KAAK,EAAE,GAAG,CAAC,IAAI;oBACf,MAAM,EAAE,GAAG,CAAC,MAAM;iBACnB,CAAC,CAAA;YACJ,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ import type { MiddlewareConfig, SettlementResult } from 'crumb-alpha-core';
2
+ export interface CrumbFetchHandlerResult {
3
+ paid: boolean;
4
+ payment?: SettlementResult;
5
+ }
6
+ export declare function createFetchHandler(config: MiddlewareConfig): (request: Request) => Promise<Response | CrumbFetchHandlerResult>;
7
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/middleware/fetch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAMrE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AAcD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,IAQ3C,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,GAAG,uBAAuB,CAAC,CAsC7E"}
@@ -0,0 +1,49 @@
1
+ import { verifyAndSettle, CrumbError, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_USDC, } from '@crumb/core';
2
+ import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, } from '@x402/core/http';
3
+ function buildRequirement(config) {
4
+ return {
5
+ scheme: 'exact',
6
+ network: config.network ?? BASE_SEPOLIA_NETWORK,
7
+ amount: config.price,
8
+ asset: config.asset ?? BASE_SEPOLIA_USDC,
9
+ payTo: config.address,
10
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? 300,
11
+ extra: {},
12
+ };
13
+ }
14
+ export function createFetchHandler(config) {
15
+ const requirement = buildRequirement(config);
16
+ const paymentRequired = {
17
+ x402Version: 2,
18
+ accepts: [requirement],
19
+ resource: {},
20
+ };
21
+ return async (request) => {
22
+ const paymentHeader = request.headers.get('x-payment');
23
+ if (!paymentHeader) {
24
+ return new Response(JSON.stringify(paymentRequired), {
25
+ status: 402,
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ 'X-Payment-Required': encodePaymentRequiredHeader(paymentRequired),
29
+ },
30
+ });
31
+ }
32
+ try {
33
+ const paymentPayload = decodePaymentSignatureHeader(paymentHeader);
34
+ const result = await verifyAndSettle(paymentPayload, requirement, config.facilitatorUrl);
35
+ config.onPayment?.(result);
36
+ return { paid: true, payment: result };
37
+ }
38
+ catch (err) {
39
+ if (err instanceof CrumbError) {
40
+ return new Response(JSON.stringify({ error: err.code, detail: err.detail }), {
41
+ status: 402,
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ }
45
+ throw err;
46
+ }
47
+ };
48
+ }
49
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/middleware/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AAOxB,SAAS,gBAAgB,CAAC,MAAwB;IAChD,OAAO;QACL,MAAM,EAAE,OAAO;QACf,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,oBAAoB;QAC/C,MAAM,EAAE,MAAM,CAAC,KAAK;QACpB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,iBAAiB;QACxC,KAAK,EAAE,MAAM,CAAC,OAAO;QACrB,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,GAAG;QAClD,KAAK,EAAE,EAAE;KACV,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAwB;IACzD,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC5C,MAAM,eAAe,GAAG;QACtB,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,QAAQ,EAAE,EAAE;KACb,CAAA;IAED,OAAO,KAAK,EAAE,OAAgB,EAA+C,EAAE;QAC7E,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAEtD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,EAC/B;gBACE,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,oBAAoB,EAAE,2BAA2B,CAAC,eAAsB,CAAC;iBAC1E;aACF,CACF,CAAA;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,4BAA4B,CAAC,aAAa,CAAC,CAAA;YAClE,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,cAAc,EACd,WAAkB,EAClB,MAAM,CAAC,cAAc,CACtB,CAAA;YACD,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;YAC1B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;gBAC9B,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,EACvD;oBACE,MAAM,EAAE,GAAG;oBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;iBAChD,CACF,CAAA;YACH,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { MiddlewareConfig } from 'crumb-alpha-core';
2
+ export type { GatewayMiddleware, PaymentRequest } from '@circle-fin/x402-batching/server';
3
+ /**
4
+ * Create a CrumbProvider — wraps Circle's createGatewayMiddleware.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const gateway = createProvider({
9
+ * sellerAddress: '0x...',
10
+ * })
11
+ *
12
+ * app.get('/resource', gateway.require('$0.01'), (req, res) => {
13
+ * res.json({ data: 'paid content' })
14
+ * })
15
+ * ```
16
+ */
17
+ export declare function createProvider(config: MiddlewareConfig): {
18
+ /** Require payment — returns Express middleware */
19
+ require: (price: string) => (req: import("@circle-fin/x402-batching/server").PaymentRequest, res: import("@circle-fin/x402-batching/server").PaymentResponse, next: (err?: unknown) => void) => void | Promise<void>;
20
+ /** Verify a payment without settling */
21
+ verify: (payment: unknown) => Promise<{
22
+ valid: boolean;
23
+ payer?: string;
24
+ error?: string;
25
+ }>;
26
+ /** Settle a verified payment */
27
+ settle: (payment: unknown) => Promise<{
28
+ success: boolean;
29
+ transaction?: string;
30
+ error?: string;
31
+ }>;
32
+ };
33
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEzF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB;IASnD,mDAAmD;qBAClC,MAAM;IAevB,wCAAwC;;;aAM++N,CAAC;aAAuB,CAAC;;IAJhjO,gCAAgC;;;mBAI6qO,CAAC;aAAuB,CAAC;;EADzuO"}
@@ -0,0 +1,46 @@
1
+ import { createGatewayMiddleware } from '@circle-fin/x402-batching/server';
2
+ /**
3
+ * Create a CrumbProvider — wraps Circle's createGatewayMiddleware.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const gateway = createProvider({
8
+ * sellerAddress: '0x...',
9
+ * })
10
+ *
11
+ * app.get('/resource', gateway.require('$0.01'), (req, res) => {
12
+ * res.json({ data: 'paid content' })
13
+ * })
14
+ * ```
15
+ */
16
+ export function createProvider(config) {
17
+ const middleware = createGatewayMiddleware({
18
+ sellerAddress: config.sellerAddress,
19
+ networks: config.networks,
20
+ facilitatorUrl: config.facilitatorUrl,
21
+ description: config.description,
22
+ });
23
+ return {
24
+ /** Require payment — returns Express middleware */
25
+ require: (price) => {
26
+ const mw = middleware.require(price);
27
+ // Wrap to call onPayment hook
28
+ if (config.onPayment) {
29
+ return async (req, res, next) => {
30
+ await mw(req, res, () => {
31
+ if (req.payment) {
32
+ config.onPayment(req.payment);
33
+ }
34
+ next();
35
+ });
36
+ };
37
+ }
38
+ return mw;
39
+ },
40
+ /** Verify a payment without settling */
41
+ verify: middleware.verify,
42
+ /** Settle a verified payment */
43
+ settle: middleware.settle,
44
+ };
45
+ }
46
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAA;AAK1E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,MAAwB;IACrD,MAAM,UAAU,GAAG,uBAAuB,CAAC;QACzC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAA;IAEF,OAAO;QACL,mDAAmD;QACnD,OAAO,EAAE,CAAC,KAAa,EAAE,EAAE;YACzB,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACpC,8BAA8B;YAC9B,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACrB,OAAO,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;oBAC7C,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;wBACtB,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;4BAChB,MAAM,CAAC,SAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;wBAChC,CAAC;wBACD,IAAI,EAAE,CAAA;oBACR,CAAC,CAAC,CAAA;gBACJ,CAAC,CAAA;YACH,CAAC;YACD,OAAO,EAAE,CAAA;QACX,CAAC;QACD,wCAAwC;QACxC,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,gCAAgC;QAChC,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "crumb-alpha-sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./middleware/express": {
14
+ "types": "./dist/middleware/express.d.ts",
15
+ "import": "./dist/middleware/express.js",
16
+ "default": "./dist/middleware/express.js"
17
+ }
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@circle-fin/x402-batching": "^2.0.4",
24
+ "crumb-alpha-core": "0.1.0"
25
+ },
26
+ "peerDependencies": {
27
+ "express": ">=4.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "express": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@types/express": "^5.0.0"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "dev": "tsc --watch",
40
+ "clean": "rm -rf dist",
41
+ "test": "vitest run"
42
+ }
43
+ }
package/src/client.ts ADDED
@@ -0,0 +1,161 @@
1
+ import {
2
+ GatewayClient,
3
+ getBalances,
4
+ checkPriceCeiling,
5
+ deriveAddress,
6
+ CrumbError,
7
+ } from 'crumb-alpha-core'
8
+ import type {
9
+ CrumbWallet,
10
+ FetchOptions,
11
+ FetchResult,
12
+ SupportedChainName,
13
+ Balances,
14
+ } from 'crumb-alpha-core'
15
+
16
+ export class Crumb {
17
+ private wallet: CrumbWallet
18
+ private gateway: GatewayClient
19
+ private chain: SupportedChainName
20
+ private lowBalanceHook?: (balance: string) => void
21
+ private lowBalanceThreshold: number = 1.0
22
+
23
+ constructor(config: {
24
+ privateKey: string
25
+ chain?: SupportedChainName
26
+ rpcUrl?: string
27
+ lowBalanceThreshold?: number
28
+ onLowBalance?: (balance: string) => void
29
+ }) {
30
+ this.chain = config.chain ?? 'arcTestnet'
31
+ this.wallet = {
32
+ address: deriveAddress(config.privateKey),
33
+ privateKey: config.privateKey,
34
+ }
35
+ this.gateway = new GatewayClient({
36
+ chain: this.chain,
37
+ privateKey: config.privateKey as `0x${string}`,
38
+ rpcUrl: config.rpcUrl,
39
+ })
40
+ if (config.lowBalanceThreshold !== undefined) {
41
+ this.lowBalanceThreshold = config.lowBalanceThreshold
42
+ }
43
+ if (config.onLowBalance) {
44
+ this.lowBalanceHook = config.onLowBalance
45
+ }
46
+ }
47
+
48
+ get address(): string {
49
+ return this.wallet.address
50
+ }
51
+
52
+ /**
53
+ * Pay for an x402-protected resource. Handles the full 402 flow automatically.
54
+ */
55
+ async fetch<T = unknown>(url: string, options: FetchOptions = {}): Promise<FetchResult<T>> {
56
+ if (options.maxPayment !== undefined) {
57
+ // Pre-check: probe the URL to see the price before paying
58
+ const supported = await this.gateway.supports(url)
59
+ if (supported.supported && supported.requirements) {
60
+ const amount = (supported.requirements as any)?.amount
61
+ if (amount) {
62
+ checkPriceCeiling(amount, options.maxPayment)
63
+ }
64
+ }
65
+ }
66
+
67
+ const result = await this.gateway.pay<T>(url, {
68
+ method: options.method,
69
+ body: options.body,
70
+ headers: options.headers,
71
+ })
72
+
73
+ // Check low balance after payment
74
+ this.checkLowBalance()
75
+
76
+ return {
77
+ data: result.data,
78
+ amount: result.amount,
79
+ formattedAmount: result.formattedAmount,
80
+ txHash: result.transaction,
81
+ status: result.status,
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Deposit USDC into Gateway for gasless payments.
87
+ */
88
+ async deposit(amount: string) {
89
+ return this.gateway.deposit(amount)
90
+ }
91
+
92
+ /**
93
+ * Withdraw USDC from Gateway.
94
+ */
95
+ async withdraw(amount: string, options?: {
96
+ chain?: SupportedChainName
97
+ recipient?: string
98
+ }) {
99
+ return this.gateway.withdraw(amount, {
100
+ chain: options?.chain,
101
+ recipient: options?.recipient as `0x${string}`,
102
+ })
103
+ }
104
+
105
+ /**
106
+ * Get all balances (wallet + gateway).
107
+ */
108
+ async balances(): Promise<Balances> {
109
+ const result = await this.gateway.getBalances()
110
+
111
+ const available = result.gateway.formattedAvailable
112
+ if (parseFloat(available) < this.lowBalanceThreshold) {
113
+ this.lowBalanceHook?.(available)
114
+ }
115
+
116
+ return result
117
+ }
118
+
119
+ /**
120
+ * Get gateway available balance as a formatted string.
121
+ */
122
+ async balance(): Promise<string> {
123
+ const result = await this.gateway.getBalances()
124
+ const available = result.gateway.formattedAvailable
125
+
126
+ if (parseFloat(available) < this.lowBalanceThreshold) {
127
+ this.lowBalanceHook?.(available)
128
+ }
129
+
130
+ return available
131
+ }
132
+
133
+ /**
134
+ * Check if a URL supports Gateway batching.
135
+ */
136
+ async supports(url: string) {
137
+ return this.gateway.supports(url)
138
+ }
139
+
140
+ onLowBalance(threshold: number, fn: (balance: string) => void) {
141
+ this.lowBalanceThreshold = threshold
142
+ this.lowBalanceHook = fn
143
+ }
144
+
145
+ /** Access the underlying GatewayClient for advanced operations. */
146
+ get client(): GatewayClient {
147
+ return this.gateway
148
+ }
149
+
150
+ private async checkLowBalance() {
151
+ try {
152
+ const result = await this.gateway.getBalances()
153
+ const available = parseFloat(result.gateway.formattedAvailable)
154
+ if (available < this.lowBalanceThreshold) {
155
+ this.lowBalanceHook?.(result.gateway.formattedAvailable)
156
+ }
157
+ } catch {
158
+ // Don't fail the payment if balance check fails
159
+ }
160
+ }
161
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ // Agent-side
2
+ export { Crumb } from './client.js'
3
+
4
+ // Provider-side
5
+ export { createProvider } from './provider.js'
6
+ export type { GatewayMiddleware, PaymentRequest } from './provider.js'
7
+
8
+ // Express middleware (convenience re-export)
9
+ export { crumbMiddleware } from './middleware/express.js'
10
+
11
+ // Re-export core types for convenience
12
+ export type {
13
+ CrumbWallet,
14
+ SettlementResult,
15
+ FetchOptions,
16
+ FetchResult,
17
+ MiddlewareConfig,
18
+ SupportedChainName,
19
+ Balances,
20
+ PayResult,
21
+ } from 'crumb-alpha-core'
22
+
23
+ export {
24
+ CrumbError,
25
+ GatewayClient,
26
+ CHAIN_CONFIGS,
27
+ GATEWAY_DOMAINS,
28
+ } from 'crumb-alpha-core'
29
+ export type { CrumbErrorCode } from 'crumb-alpha-core'
@@ -0,0 +1,45 @@
1
+ import { createGatewayMiddleware } from '@circle-fin/x402-batching/server'
2
+ import type { MiddlewareConfig } from 'crumb-alpha-core'
3
+
4
+ /**
5
+ * Express middleware that gates routes behind x402 payments via Circle Gateway.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { crumbMiddleware } from 'crumb-alpha-sdk/middleware/express'
10
+ *
11
+ * const gateway = crumbMiddleware({ sellerAddress: '0x...' })
12
+ *
13
+ * app.get('/resource', gateway.require('$0.01'), (req, res) => {
14
+ * console.log('Payer:', req.payment?.payer)
15
+ * res.json({ data: 'paid content' })
16
+ * })
17
+ * ```
18
+ */
19
+ export function crumbMiddleware(config: MiddlewareConfig) {
20
+ const gateway = createGatewayMiddleware({
21
+ sellerAddress: config.sellerAddress,
22
+ networks: config.networks,
23
+ facilitatorUrl: config.facilitatorUrl,
24
+ description: config.description,
25
+ })
26
+
27
+ return {
28
+ require: (price: string) => {
29
+ const mw = gateway.require(price)
30
+ if (config.onPayment) {
31
+ return async (req: any, res: any, next: any) => {
32
+ await mw(req, res, () => {
33
+ if (req.payment) {
34
+ config.onPayment!(req.payment)
35
+ }
36
+ next()
37
+ })
38
+ }
39
+ }
40
+ return mw
41
+ },
42
+ verify: gateway.verify,
43
+ settle: gateway.settle,
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import { createGatewayMiddleware } from '@circle-fin/x402-batching/server'
2
+ import type { MiddlewareConfig } from 'crumb-alpha-core'
3
+
4
+ export type { GatewayMiddleware, PaymentRequest } from '@circle-fin/x402-batching/server'
5
+
6
+ /**
7
+ * Create a CrumbProvider — wraps Circle's createGatewayMiddleware.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const gateway = createProvider({
12
+ * sellerAddress: '0x...',
13
+ * })
14
+ *
15
+ * app.get('/resource', gateway.require('$0.01'), (req, res) => {
16
+ * res.json({ data: 'paid content' })
17
+ * })
18
+ * ```
19
+ */
20
+ export function createProvider(config: MiddlewareConfig) {
21
+ const middleware = createGatewayMiddleware({
22
+ sellerAddress: config.sellerAddress,
23
+ networks: config.networks,
24
+ facilitatorUrl: config.facilitatorUrl,
25
+ description: config.description,
26
+ })
27
+
28
+ return {
29
+ /** Require payment — returns Express middleware */
30
+ require: (price: string) => {
31
+ const mw = middleware.require(price)
32
+ // Wrap to call onPayment hook
33
+ if (config.onPayment) {
34
+ return async (req: any, res: any, next: any) => {
35
+ await mw(req, res, () => {
36
+ if (req.payment) {
37
+ config.onPayment!(req.payment)
38
+ }
39
+ next()
40
+ })
41
+ }
42
+ }
43
+ return mw
44
+ },
45
+ /** Verify a payment without settling */
46
+ verify: middleware.verify,
47
+ /** Settle a verified payment */
48
+ settle: middleware.settle,
49
+ }
50
+ }
@@ -0,0 +1,243 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ const mockPay = vi.fn()
4
+ const mockDeposit = vi.fn()
5
+ const mockWithdraw = vi.fn()
6
+ const mockGetBalances = vi.fn()
7
+ const mockSupports = vi.fn()
8
+
9
+ class MockGatewayClient {
10
+ pay = mockPay
11
+ deposit = mockDeposit
12
+ withdraw = mockWithdraw
13
+ getBalances = mockGetBalances
14
+ supports = mockSupports
15
+ constructor(_config: any) {}
16
+ }
17
+
18
+ vi.mock('@circle-fin/x402-batching/client', () => ({
19
+ GatewayClient: MockGatewayClient,
20
+ }))
21
+
22
+ vi.mock('crumb-alpha-core', async () => {
23
+ const actual = await vi.importActual<any>('crumb-alpha-core')
24
+ return {
25
+ ...actual,
26
+ GatewayClient: MockGatewayClient,
27
+ }
28
+ })
29
+
30
+ const { Crumb } = await import('../src/client.js')
31
+
32
+ const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
33
+
34
+ describe('Crumb', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks()
37
+ mockGetBalances.mockResolvedValue({
38
+ wallet: { formatted: '100.00' },
39
+ gateway: { formattedAvailable: '50.00', available: 50000000n },
40
+ })
41
+ })
42
+
43
+ describe('constructor', () => {
44
+ it('derives address from private key', () => {
45
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
46
+ expect(crumb.address).toBe('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')
47
+ })
48
+
49
+ it('defaults chain to arcTestnet', () => {
50
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
51
+ expect(crumb).toBeDefined()
52
+ })
53
+
54
+ it('exposes the underlying GatewayClient', () => {
55
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
56
+ expect(crumb.client).toBeDefined()
57
+ })
58
+ })
59
+
60
+ describe('fetch', () => {
61
+ it('calls gateway.pay with url and options', async () => {
62
+ mockPay.mockResolvedValue({
63
+ data: { results: [] },
64
+ amount: 1000n,
65
+ formattedAmount: '0.001',
66
+ transaction: '0xtx123',
67
+ status: 200,
68
+ })
69
+ mockSupports.mockResolvedValue({ supported: false })
70
+
71
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
72
+ const result = await crumb.fetch('http://localhost:3001/search', {
73
+ method: 'POST',
74
+ body: { query: 'test' },
75
+ })
76
+
77
+ expect(mockPay).toHaveBeenCalledWith('http://localhost:3001/search', {
78
+ method: 'POST',
79
+ body: { query: 'test' },
80
+ headers: undefined,
81
+ })
82
+ expect(result.data).toEqual({ results: [] })
83
+ expect(result.formattedAmount).toBe('0.001')
84
+ expect(result.txHash).toBe('0xtx123')
85
+ expect(result.status).toBe(200)
86
+ })
87
+
88
+ it('checks price ceiling when maxPayment is set', async () => {
89
+ mockSupports.mockResolvedValue({
90
+ supported: true,
91
+ requirements: { amount: '$0.05' },
92
+ })
93
+
94
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
95
+
96
+ await expect(
97
+ crumb.fetch('http://localhost:3001/search', { maxPayment: 0.01 }),
98
+ ).rejects.toThrow()
99
+ })
100
+
101
+ it('proceeds when price is within maxPayment', async () => {
102
+ mockSupports.mockResolvedValue({
103
+ supported: true,
104
+ requirements: { amount: '$0.001' },
105
+ })
106
+ mockPay.mockResolvedValue({
107
+ data: {},
108
+ amount: 1000n,
109
+ formattedAmount: '0.001',
110
+ transaction: '0xtx',
111
+ status: 200,
112
+ })
113
+
114
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
115
+ const result = await crumb.fetch('http://localhost:3001/search', { maxPayment: 0.01 })
116
+ expect(result.formattedAmount).toBe('0.001')
117
+ })
118
+ })
119
+
120
+ describe('deposit', () => {
121
+ it('delegates to gateway.deposit', async () => {
122
+ mockDeposit.mockResolvedValue({ success: true })
123
+
124
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
125
+ await crumb.deposit('10.0')
126
+
127
+ expect(mockDeposit).toHaveBeenCalledWith('10.0')
128
+ })
129
+ })
130
+
131
+ describe('withdraw', () => {
132
+ it('delegates to gateway.withdraw', async () => {
133
+ mockWithdraw.mockResolvedValue({ success: true })
134
+
135
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
136
+ await crumb.withdraw('5.0')
137
+
138
+ expect(mockWithdraw).toHaveBeenCalledWith('5.0', {
139
+ chain: undefined,
140
+ recipient: undefined,
141
+ })
142
+ })
143
+
144
+ it('passes chain and recipient options', async () => {
145
+ mockWithdraw.mockResolvedValue({ success: true })
146
+
147
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
148
+ await crumb.withdraw('5.0', { chain: 'arcTestnet', recipient: '0xrecipient' })
149
+
150
+ expect(mockWithdraw).toHaveBeenCalledWith('5.0', {
151
+ chain: 'arcTestnet',
152
+ recipient: '0xrecipient',
153
+ })
154
+ })
155
+ })
156
+
157
+ describe('balance / balances', () => {
158
+ it('returns formatted gateway balance', async () => {
159
+ mockGetBalances.mockResolvedValue({
160
+ wallet: { formatted: '100.00' },
161
+ gateway: { formattedAvailable: '42.50', available: 42500000n },
162
+ })
163
+
164
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
165
+ const bal = await crumb.balance()
166
+ expect(bal).toBe('42.50')
167
+ })
168
+
169
+ it('returns full balances object', async () => {
170
+ const mockResult = {
171
+ wallet: { formatted: '100.00' },
172
+ gateway: { formattedAvailable: '42.50', available: 42500000n },
173
+ }
174
+ mockGetBalances.mockResolvedValue(mockResult)
175
+
176
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
177
+ const result = await crumb.balances()
178
+ expect(result).toEqual(mockResult)
179
+ })
180
+
181
+ it('triggers low balance hook when below threshold', async () => {
182
+ mockGetBalances.mockResolvedValue({
183
+ wallet: { formatted: '0.50' },
184
+ gateway: { formattedAvailable: '0.25', available: 250000n },
185
+ })
186
+
187
+ const hook = vi.fn()
188
+ const crumb = new Crumb({
189
+ privateKey: TEST_PRIVATE_KEY,
190
+ lowBalanceThreshold: 1.0,
191
+ onLowBalance: hook,
192
+ })
193
+
194
+ await crumb.balance()
195
+ expect(hook).toHaveBeenCalledWith('0.25')
196
+ })
197
+
198
+ it('does not trigger hook when above threshold', async () => {
199
+ mockGetBalances.mockResolvedValue({
200
+ wallet: { formatted: '100.00' },
201
+ gateway: { formattedAvailable: '50.00', available: 50000000n },
202
+ })
203
+
204
+ const hook = vi.fn()
205
+ const crumb = new Crumb({
206
+ privateKey: TEST_PRIVATE_KEY,
207
+ lowBalanceThreshold: 1.0,
208
+ onLowBalance: hook,
209
+ })
210
+
211
+ await crumb.balance()
212
+ expect(hook).not.toHaveBeenCalled()
213
+ })
214
+ })
215
+
216
+ describe('supports', () => {
217
+ it('delegates to gateway.supports', async () => {
218
+ mockSupports.mockResolvedValue({ supported: true, requirements: {} })
219
+
220
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
221
+ const result = await crumb.supports('http://example.com/api')
222
+
223
+ expect(mockSupports).toHaveBeenCalledWith('http://example.com/api')
224
+ expect(result.supported).toBe(true)
225
+ })
226
+ })
227
+
228
+ describe('onLowBalance', () => {
229
+ it('updates threshold and hook', async () => {
230
+ mockGetBalances.mockResolvedValue({
231
+ wallet: { formatted: '5.00' },
232
+ gateway: { formattedAvailable: '3.00', available: 3000000n },
233
+ })
234
+
235
+ const hook = vi.fn()
236
+ const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
237
+ crumb.onLowBalance(5.0, hook)
238
+
239
+ await crumb.balance()
240
+ expect(hook).toHaveBeenCalledWith('3.00')
241
+ })
242
+ })
243
+ })
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+
3
+ const mockRequire = vi.fn().mockReturnValue(vi.fn())
4
+ const mockVerify = vi.fn()
5
+ const mockSettle = vi.fn()
6
+
7
+ vi.mock('@circle-fin/x402-batching/server', () => ({
8
+ createGatewayMiddleware: vi.fn().mockImplementation(() => ({
9
+ require: mockRequire,
10
+ verify: mockVerify,
11
+ settle: mockSettle,
12
+ })),
13
+ }))
14
+
15
+ const { crumbMiddleware } = await import('../src/middleware/express.js')
16
+
17
+ describe('crumbMiddleware', () => {
18
+ it('returns an object with require, verify, settle', () => {
19
+ const gateway = crumbMiddleware({ sellerAddress: '0xseller' })
20
+ expect(gateway).toHaveProperty('require')
21
+ expect(gateway).toHaveProperty('verify')
22
+ expect(gateway).toHaveProperty('settle')
23
+ })
24
+
25
+ it('require returns middleware', () => {
26
+ const gateway = crumbMiddleware({ sellerAddress: '0xseller' })
27
+ const mw = gateway.require('$0.01')
28
+ expect(mockRequire).toHaveBeenCalledWith('$0.01')
29
+ expect(mw).toBeDefined()
30
+ })
31
+
32
+ it('wraps middleware with onPayment hook', async () => {
33
+ const onPayment = vi.fn()
34
+ const innerMw = vi.fn().mockImplementation((_req: any, _res: any, next: () => void) => {
35
+ next()
36
+ })
37
+ mockRequire.mockReturnValue(innerMw)
38
+
39
+ const gateway = crumbMiddleware({
40
+ sellerAddress: '0xseller',
41
+ onPayment,
42
+ })
43
+
44
+ const mw = gateway.require('$0.01')
45
+ const req = { payment: { verified: true, payer: '0xpayer', amount: '0.01', network: 'arcTestnet' } }
46
+ const res = {}
47
+ const next = vi.fn()
48
+
49
+ await mw(req, res, next)
50
+
51
+ expect(onPayment).toHaveBeenCalledWith(req.payment)
52
+ expect(next).toHaveBeenCalled()
53
+ })
54
+
55
+ it('returns raw middleware when no onPayment', () => {
56
+ const innerMw = vi.fn()
57
+ mockRequire.mockReturnValue(innerMw)
58
+
59
+ const gateway = crumbMiddleware({ sellerAddress: '0xseller' })
60
+ const mw = gateway.require('$0.01')
61
+ expect(mw).toBe(innerMw)
62
+ })
63
+ })
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+
3
+ const mockRequire = vi.fn().mockReturnValue(vi.fn())
4
+ const mockVerify = vi.fn()
5
+ const mockSettle = vi.fn()
6
+
7
+ vi.mock('@circle-fin/x402-batching/server', () => ({
8
+ createGatewayMiddleware: vi.fn().mockImplementation(() => ({
9
+ require: mockRequire,
10
+ verify: mockVerify,
11
+ settle: mockSettle,
12
+ })),
13
+ }))
14
+
15
+ const { createProvider } = await import('../src/provider.js')
16
+
17
+ describe('createProvider', () => {
18
+ it('returns an object with require, verify, settle', () => {
19
+ const provider = createProvider({ sellerAddress: '0xseller' })
20
+ expect(provider).toHaveProperty('require')
21
+ expect(provider).toHaveProperty('verify')
22
+ expect(provider).toHaveProperty('settle')
23
+ })
24
+
25
+ it('passes config to createGatewayMiddleware', async () => {
26
+ const { createGatewayMiddleware } = await import('@circle-fin/x402-batching/server')
27
+
28
+ createProvider({
29
+ sellerAddress: '0xseller',
30
+ description: 'Test provider',
31
+ networks: 'arcTestnet',
32
+ })
33
+
34
+ expect(createGatewayMiddleware).toHaveBeenCalledWith({
35
+ sellerAddress: '0xseller',
36
+ description: 'Test provider',
37
+ networks: 'arcTestnet',
38
+ facilitatorUrl: undefined,
39
+ })
40
+ })
41
+
42
+ it('require returns middleware from gateway', () => {
43
+ const provider = createProvider({ sellerAddress: '0xseller' })
44
+ const mw = provider.require('$0.01')
45
+ expect(mockRequire).toHaveBeenCalledWith('$0.01')
46
+ expect(mw).toBeDefined()
47
+ })
48
+
49
+ it('wraps middleware to call onPayment hook', async () => {
50
+ const onPayment = vi.fn()
51
+ const innerMw = vi.fn().mockImplementation((_req: any, _res: any, next: () => void) => {
52
+ next()
53
+ })
54
+ mockRequire.mockReturnValue(innerMw)
55
+
56
+ const provider = createProvider({
57
+ sellerAddress: '0xseller',
58
+ onPayment,
59
+ })
60
+
61
+ const mw = provider.require('$0.01')
62
+
63
+ const req = { payment: { verified: true, payer: '0xpayer', amount: '0.01', network: 'arcTestnet' } }
64
+ const res = {}
65
+ const next = vi.fn()
66
+
67
+ await mw(req, res, next)
68
+
69
+ expect(onPayment).toHaveBeenCalledWith(req.payment)
70
+ expect(next).toHaveBeenCalled()
71
+ })
72
+
73
+ it('does not wrap middleware when no onPayment hook', () => {
74
+ const innerMw = vi.fn()
75
+ mockRequire.mockReturnValue(innerMw)
76
+
77
+ const provider = createProvider({ sellerAddress: '0xseller' })
78
+ const mw = provider.require('$0.01')
79
+
80
+ // Without onPayment, it should return the raw middleware
81
+ expect(mw).toBe(innerMw)
82
+ })
83
+
84
+ it('exposes verify and settle from gateway', () => {
85
+ const provider = createProvider({ sellerAddress: '0xseller' })
86
+ expect(provider.verify).toBe(mockVerify)
87
+ expect(provider.settle).toBe(mockSettle)
88
+ })
89
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }