@agentspend/sdk 0.3.7 → 0.3.9

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,20 @@
1
+ import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallPaymentContext } from "../core/types.js";
2
+ export * from "../core/index.js";
3
+ export interface HonoContextLike {
4
+ req: {
5
+ header(name: string): string | undefined;
6
+ json(): Promise<unknown>;
7
+ url: string;
8
+ method: string;
9
+ };
10
+ json(body: unknown, status?: number): Response;
11
+ header(name: string, value: string): void;
12
+ set(key: string, value: unknown): void;
13
+ get(key: string): unknown;
14
+ }
15
+ export declare function getPaymentContext(c: HonoContextLike): PaywallPaymentContext | null;
16
+ export interface AgentSpend {
17
+ charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
18
+ paywall(opts: PaywallOptions): (c: HonoContextLike, next: () => Promise<void>) => Promise<Response | void>;
19
+ }
20
+ export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
@@ -0,0 +1,49 @@
1
+ import { createAgentSpendClient } from "../core/client.js";
2
+ export * from "../core/index.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Payment context helper
5
+ // ---------------------------------------------------------------------------
6
+ const PAYMENT_CONTEXT_KEY = "payment";
7
+ export function getPaymentContext(c) {
8
+ const ctx = c.get(PAYMENT_CONTEXT_KEY);
9
+ return ctx ?? null;
10
+ }
11
+ // ---------------------------------------------------------------------------
12
+ // Factory
13
+ // ---------------------------------------------------------------------------
14
+ export function createAgentSpend(options) {
15
+ const client = createAgentSpendClient(options);
16
+ function paywall(opts) {
17
+ return async function paywallMiddleware(c, next) {
18
+ const body = await c.req.json().catch(() => ({}));
19
+ const request = {
20
+ url: c.req.url,
21
+ method: c.req.method,
22
+ headers: {
23
+ "x-card-id": c.req.header("x-card-id"),
24
+ "payment-signature": c.req.header("payment-signature"),
25
+ "x-payment": c.req.header("x-payment"),
26
+ "x-request-id": c.req.header("x-request-id"),
27
+ "idempotency-key": c.req.header("idempotency-key"),
28
+ },
29
+ body
30
+ };
31
+ const result = await client.processPaywall(opts, request);
32
+ switch (result.outcome) {
33
+ case "charged":
34
+ case "crypto_paid":
35
+ c.set(PAYMENT_CONTEXT_KEY, result.paymentContext);
36
+ await next();
37
+ return;
38
+ case "payment_required":
39
+ for (const [key, value] of Object.entries(result.headers)) {
40
+ c.header(key, value);
41
+ }
42
+ return c.json(result.body, result.statusCode);
43
+ case "error":
44
+ return c.json(result.body, result.statusCode);
45
+ }
46
+ };
47
+ }
48
+ return { charge: client.charge, paywall };
49
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallPaymentContext } from "../core/types.js";
2
+ export * from "../core/index.js";
3
+ interface NextRequest {
4
+ url: string;
5
+ method: string;
6
+ headers: {
7
+ get(name: string): string | null;
8
+ };
9
+ json(): Promise<unknown>;
10
+ }
11
+ export interface AgentSpend {
12
+ charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
13
+ withPaywall(opts: PaywallOptions, handler: (req: NextRequest, paymentContext: PaywallPaymentContext) => Promise<Response>): (req: NextRequest) => Promise<Response>;
14
+ }
15
+ export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
@@ -0,0 +1,39 @@
1
+ import { createAgentSpendClient } from "../core/client.js";
2
+ export * from "../core/index.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Factory
5
+ // ---------------------------------------------------------------------------
6
+ export function createAgentSpend(options) {
7
+ const client = createAgentSpendClient(options);
8
+ function withPaywall(opts, handler) {
9
+ return async function wrappedHandler(req) {
10
+ const body = await req.json().catch(() => ({}));
11
+ const request = {
12
+ url: req.url,
13
+ method: req.method,
14
+ headers: {
15
+ "x-card-id": req.headers.get("x-card-id") ?? undefined,
16
+ "payment-signature": req.headers.get("payment-signature") ?? undefined,
17
+ "x-payment": req.headers.get("x-payment") ?? undefined,
18
+ "x-request-id": req.headers.get("x-request-id") ?? undefined,
19
+ "idempotency-key": req.headers.get("idempotency-key") ?? undefined,
20
+ },
21
+ body
22
+ };
23
+ const result = await client.processPaywall(opts, request);
24
+ switch (result.outcome) {
25
+ case "charged":
26
+ case "crypto_paid":
27
+ return handler(req, result.paymentContext);
28
+ case "payment_required":
29
+ return Response.json(result.body, {
30
+ status: result.statusCode,
31
+ headers: result.headers
32
+ });
33
+ case "error":
34
+ return Response.json(result.body, { status: result.statusCode });
35
+ }
36
+ };
37
+ }
38
+ return { charge: client.charge, withPaywall };
39
+ }
package/package.json CHANGED
@@ -1,8 +1,31 @@
1
1
  {
2
2
  "name": "@agentspend/sdk",
3
- "version": "0.3.7",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
3
+ "version": "0.3.9",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/core/index.js",
8
+ "types": "./dist/core/index.d.ts"
9
+ },
10
+ "./hono": {
11
+ "import": "./dist/hono/index.js",
12
+ "types": "./dist/hono/index.d.ts"
13
+ },
14
+ "./express": {
15
+ "import": "./dist/express/index.js",
16
+ "types": "./dist/express/index.d.ts"
17
+ },
18
+ "./fastify": {
19
+ "import": "./dist/fastify/index.js",
20
+ "types": "./dist/fastify/index.d.ts"
21
+ },
22
+ "./next": {
23
+ "import": "./dist/next/index.js",
24
+ "types": "./dist/next/index.d.ts"
25
+ }
26
+ },
27
+ "main": "dist/core/index.js",
28
+ "types": "dist/core/index.d.ts",
6
29
  "publishConfig": {
7
30
  "access": "public"
8
31
  },
@@ -10,6 +33,30 @@
10
33
  "@x402/core": "^2.3.1",
11
34
  "@x402/evm": "^2.3.1"
12
35
  },
36
+ "peerDependencies": {
37
+ "hono": ">=4",
38
+ "express": ">=4",
39
+ "@types/express": ">=4",
40
+ "fastify": ">=4",
41
+ "next": ">=14"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "hono": {
45
+ "optional": true
46
+ },
47
+ "express": {
48
+ "optional": true
49
+ },
50
+ "@types/express": {
51
+ "optional": true
52
+ },
53
+ "fastify": {
54
+ "optional": true
55
+ },
56
+ "next": {
57
+ "optional": true
58
+ }
59
+ },
13
60
  "scripts": {
14
61
  "build": "tsc",
15
62
  "typecheck": "tsc --noEmit"
@@ -0,0 +1,433 @@
1
+ import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
2
+ import { registerExactEvmScheme } from "@x402/evm/exact/server";
3
+ import type {
4
+ PaymentRequirements,
5
+ PaymentPayload,
6
+ VerifyResponse,
7
+ SettleResponse,
8
+ Network
9
+ } from "@x402/core/types";
10
+
11
+ import type {
12
+ AgentSpendOptions,
13
+ ChargeOptions,
14
+ ChargeRequest,
15
+ ChargeResponse,
16
+ ErrorResponse,
17
+ PaywallOptions,
18
+ PaywallPaymentContext,
19
+ PaywallRequest,
20
+ PaywallResult
21
+ } from "./types.js";
22
+ import { AgentSpendChargeError } from "./error.js";
23
+ import {
24
+ toCardId,
25
+ joinUrl,
26
+ bestEffortIdempotencyKey,
27
+ toStringMetadata,
28
+ resolvePlatformApiBaseUrl,
29
+ resolveAmount,
30
+ extractCardId,
31
+ extractPaymentHeader
32
+ } from "./helpers.js";
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Core client interface
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export interface AgentSpendClient {
39
+ charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
40
+ processPaywall(opts: PaywallOptions, request: PaywallRequest): Promise<PaywallResult>;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Factory
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export function createAgentSpendClient(options: AgentSpendOptions): AgentSpendClient {
48
+ if (!options.serviceApiKey && !options.crypto) {
49
+ throw new AgentSpendChargeError(
50
+ "At least one of serviceApiKey or crypto config must be provided",
51
+ 500
52
+ );
53
+ }
54
+
55
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
56
+ if (!fetchImpl) {
57
+ throw new AgentSpendChargeError("No fetch implementation available", 500);
58
+ }
59
+
60
+ const platformApiBaseUrl = resolvePlatformApiBaseUrl(options.platformApiBaseUrl);
61
+
62
+ // -------------------------------------------------------------------
63
+ // Lazy service_id fetch + cache
64
+ // -------------------------------------------------------------------
65
+ let cachedServiceId: string | null = null;
66
+
67
+ async function getServiceId(): Promise<string | null> {
68
+ if (cachedServiceId) return cachedServiceId;
69
+ if (!options.serviceApiKey) return null;
70
+ try {
71
+ const res = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/service/me"), {
72
+ headers: { authorization: `Bearer ${options.serviceApiKey}` }
73
+ });
74
+ if (res.ok) {
75
+ const data = (await res.json()) as { id?: string };
76
+ cachedServiceId = data.id ?? null;
77
+ }
78
+ } catch { /* graceful fallback */ }
79
+ return cachedServiceId;
80
+ }
81
+
82
+ // -------------------------------------------------------------------
83
+ // x402 singleton setup
84
+ // -------------------------------------------------------------------
85
+ let facilitator: HTTPFacilitatorClient | null = null;
86
+ let resourceServer: x402ResourceServer | null = null;
87
+ const cryptoNetwork: Network = (options.crypto?.network ?? "eip155:8453") as Network;
88
+
89
+ if (options.crypto || options.serviceApiKey) {
90
+ const facilitatorUrl =
91
+ options.crypto?.facilitatorUrl ?? "https://facilitator.openx402.ai";
92
+ facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
93
+ resourceServer = new x402ResourceServer(facilitator);
94
+ registerExactEvmScheme(resourceServer);
95
+ }
96
+
97
+ // -------------------------------------------------------------------
98
+ // charge() — card-only
99
+ // -------------------------------------------------------------------
100
+
101
+ async function charge(cardIdInput: string, opts: ChargeOptions): Promise<ChargeResponse> {
102
+ if (!options.serviceApiKey) {
103
+ throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
104
+ }
105
+
106
+ const cardId = toCardId(cardIdInput);
107
+ if (!cardId) {
108
+ throw new AgentSpendChargeError("card_id must start with card_", 400);
109
+ }
110
+ if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
111
+ throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
112
+ }
113
+
114
+ const payload: ChargeRequest = {
115
+ card_id: cardId,
116
+ amount_cents: opts.amount_cents,
117
+ currency: opts.currency ?? "usd",
118
+ ...(opts.description ? { description: opts.description } : {}),
119
+ ...(opts.metadata ? { metadata: opts.metadata } : {}),
120
+ idempotency_key: opts.idempotency_key ?? bestEffortIdempotencyKey()
121
+ };
122
+
123
+ const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/charge"), {
124
+ method: "POST",
125
+ headers: {
126
+ authorization: `Bearer ${options.serviceApiKey}`,
127
+ "content-type": "application/json"
128
+ },
129
+ body: JSON.stringify(payload)
130
+ });
131
+
132
+ const responseBody = (await response.json().catch(() => ({}))) as Partial<ChargeResponse> &
133
+ Partial<ErrorResponse> &
134
+ Record<string, unknown>;
135
+ if (!response.ok) {
136
+ throw new AgentSpendChargeError(
137
+ typeof responseBody.error === "string" ? responseBody.error : "AgentSpend charge failed",
138
+ response.status,
139
+ responseBody
140
+ );
141
+ }
142
+
143
+ return responseBody as ChargeResponse;
144
+ }
145
+
146
+ // -------------------------------------------------------------------
147
+ // resolvePayToAddress
148
+ // -------------------------------------------------------------------
149
+
150
+ async function resolvePayToAddress(): Promise<string> {
151
+ if (options.crypto?.receiverAddress) {
152
+ return options.crypto.receiverAddress;
153
+ }
154
+
155
+ if (options.serviceApiKey) {
156
+ const response = await fetchImpl(
157
+ joinUrl(platformApiBaseUrl, "/v1/crypto/deposit-address"),
158
+ {
159
+ method: "POST",
160
+ headers: {
161
+ authorization: `Bearer ${options.serviceApiKey}`,
162
+ "content-type": "application/json"
163
+ },
164
+ body: JSON.stringify({ amount_cents: 0, currency: "usd" })
165
+ }
166
+ );
167
+
168
+ if (!response.ok) {
169
+ throw new AgentSpendChargeError("Failed to resolve crypto deposit address", 502);
170
+ }
171
+
172
+ const data = (await response.json()) as { deposit_address?: string };
173
+ if (!data.deposit_address) {
174
+ throw new AgentSpendChargeError("No deposit address returned", 502);
175
+ }
176
+ return data.deposit_address;
177
+ }
178
+
179
+ throw new AgentSpendChargeError("No crypto payTo address available", 500);
180
+ }
181
+
182
+ // -------------------------------------------------------------------
183
+ // build402Result — x402 Payment-Required format
184
+ // -------------------------------------------------------------------
185
+
186
+ async function build402Result(
187
+ requestUrl: string,
188
+ amountCents: number,
189
+ currency: string
190
+ ): Promise<PaywallResult> {
191
+ const serviceId = await getServiceId();
192
+
193
+ try {
194
+ const payTo = await resolvePayToAddress();
195
+
196
+ const paymentRequirements: PaymentRequirements = {
197
+ scheme: "exact",
198
+ network: cryptoNetwork,
199
+ amount: String(amountCents),
200
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
201
+ payTo,
202
+ maxTimeoutSeconds: 300,
203
+ extra: { name: "USD Coin", version: "2" }
204
+ };
205
+
206
+ const paymentRequired = {
207
+ x402Version: 2,
208
+ error: "Payment required",
209
+ resource: {
210
+ url: requestUrl,
211
+ description: `Payment of ${amountCents} cents`,
212
+ mimeType: "application/json"
213
+ },
214
+ accepts: [paymentRequirements]
215
+ };
216
+
217
+ const headerValue = Buffer.from(
218
+ JSON.stringify(paymentRequired)
219
+ ).toString("base64");
220
+
221
+ return {
222
+ outcome: "payment_required",
223
+ statusCode: 402,
224
+ body: {
225
+ error: "Payment required",
226
+ amount_cents: amountCents,
227
+ currency,
228
+ ...(serviceId ? {
229
+ agentspend: {
230
+ service_id: serviceId,
231
+ amount_cents: amountCents,
232
+ }
233
+ } : {})
234
+ },
235
+ headers: { "Payment-Required": headerValue }
236
+ };
237
+ } catch (error) {
238
+ console.error(
239
+ "[agentspend] Failed to resolve crypto payTo address — returning card-only 402:",
240
+ error instanceof Error ? error.message : error
241
+ );
242
+ return {
243
+ outcome: "payment_required",
244
+ statusCode: 402,
245
+ body: {
246
+ error: "Payment required",
247
+ amount_cents: amountCents,
248
+ currency,
249
+ ...(serviceId ? {
250
+ agentspend: {
251
+ service_id: serviceId,
252
+ amount_cents: amountCents,
253
+ }
254
+ } : {})
255
+ },
256
+ headers: {}
257
+ };
258
+ }
259
+ }
260
+
261
+ // -------------------------------------------------------------------
262
+ // handleCardPayment
263
+ // -------------------------------------------------------------------
264
+
265
+ async function handleCardPayment(
266
+ request: PaywallRequest,
267
+ cardId: string,
268
+ amountCents: number,
269
+ currency: string,
270
+ body: unknown,
271
+ opts: PaywallOptions
272
+ ): Promise<PaywallResult> {
273
+ if (!options.serviceApiKey) {
274
+ return { outcome: "error", statusCode: 500, body: { error: "Card payments require serviceApiKey" } };
275
+ }
276
+
277
+ try {
278
+ const chargeResult = await charge(cardId, {
279
+ amount_cents: amountCents,
280
+ currency,
281
+ description: opts.description,
282
+ metadata: opts.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
283
+ idempotency_key:
284
+ request.headers["x-request-id"] ?? request.headers["idempotency-key"] ?? undefined
285
+ });
286
+
287
+ const paymentContext: PaywallPaymentContext = {
288
+ method: "card",
289
+ amount_cents: amountCents,
290
+ currency,
291
+ card_id: cardId,
292
+ remaining_limit_cents: chargeResult.remaining_limit_cents
293
+ };
294
+ return { outcome: "charged", paymentContext };
295
+ } catch (error) {
296
+ if (error instanceof AgentSpendChargeError) {
297
+ if (error.statusCode === 403) {
298
+ return build402Result(request.url, amountCents, currency);
299
+ }
300
+ if (error.statusCode === 402) {
301
+ return { outcome: "error", statusCode: 402, body: { error: "Payment required", details: error.details } };
302
+ }
303
+ return { outcome: "error", statusCode: error.statusCode, body: { error: error.message, details: error.details } };
304
+ }
305
+ return { outcome: "error", statusCode: 500, body: { error: "Unexpected paywall failure" } };
306
+ }
307
+ }
308
+
309
+ // -------------------------------------------------------------------
310
+ // handleCryptoPayment
311
+ // -------------------------------------------------------------------
312
+
313
+ async function handleCryptoPayment(
314
+ paymentHeader: string,
315
+ amountCents: number,
316
+ currency: string
317
+ ): Promise<PaywallResult> {
318
+ if (!resourceServer) {
319
+ return { outcome: "error", statusCode: 500, body: { error: "Crypto payments not configured" } };
320
+ }
321
+
322
+ try {
323
+ let paymentPayload: PaymentPayload;
324
+ try {
325
+ paymentPayload = JSON.parse(
326
+ Buffer.from(paymentHeader, "base64").toString("utf-8")
327
+ ) as PaymentPayload;
328
+ } catch {
329
+ return { outcome: "error", statusCode: 400, body: { error: "Invalid payment payload encoding" } };
330
+ }
331
+
332
+ const acceptedPayTo = (paymentPayload as unknown as { accepted?: { payTo?: string } })
333
+ .accepted?.payTo;
334
+ const payTo: string = acceptedPayTo ?? await resolvePayToAddress();
335
+
336
+ const paymentRequirements: PaymentRequirements = {
337
+ scheme: "exact",
338
+ network: cryptoNetwork,
339
+ amount: String(amountCents),
340
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
341
+ payTo,
342
+ maxTimeoutSeconds: 300,
343
+ extra: { name: "USD Coin", version: "2" }
344
+ };
345
+
346
+ const verifyResult: VerifyResponse = await resourceServer!.verifyPayment(
347
+ paymentPayload,
348
+ paymentRequirements
349
+ );
350
+
351
+ if (!verifyResult.isValid) {
352
+ return {
353
+ outcome: "error",
354
+ statusCode: 402,
355
+ body: { error: "Payment verification failed", details: verifyResult.invalidReason }
356
+ };
357
+ }
358
+
359
+ const settleResult: SettleResponse = await resourceServer!.settlePayment(
360
+ paymentPayload,
361
+ paymentRequirements
362
+ );
363
+
364
+ if (!settleResult.success) {
365
+ return {
366
+ outcome: "error",
367
+ statusCode: 402,
368
+ body: { error: "Payment settlement failed", details: settleResult.errorReason }
369
+ };
370
+ }
371
+
372
+ const paymentContext: PaywallPaymentContext = {
373
+ method: "crypto",
374
+ amount_cents: amountCents,
375
+ currency,
376
+ transaction_hash: settleResult.transaction,
377
+ payer_address: verifyResult.payer ?? undefined,
378
+ network: cryptoNetwork
379
+ };
380
+ return { outcome: "crypto_paid", paymentContext };
381
+ } catch (error) {
382
+ if (error instanceof AgentSpendChargeError) {
383
+ return { outcome: "error", statusCode: error.statusCode, body: { error: error.message, details: error.details } };
384
+ }
385
+ return {
386
+ outcome: "error",
387
+ statusCode: 500,
388
+ body: { error: "Crypto payment processing failed", details: (error as Error).message }
389
+ };
390
+ }
391
+ }
392
+
393
+ // -------------------------------------------------------------------
394
+ // processPaywall() — unified entry point for adapters
395
+ // -------------------------------------------------------------------
396
+
397
+ async function processPaywall(opts: PaywallOptions, request: PaywallRequest): Promise<PaywallResult> {
398
+ const { amount } = opts;
399
+
400
+ if (typeof amount === "number") {
401
+ if (!Number.isInteger(amount) || amount <= 0) {
402
+ throw new AgentSpendChargeError("amount must be a positive integer", 500);
403
+ }
404
+ }
405
+
406
+ const body = request.body;
407
+
408
+ const effectiveAmount = resolveAmount(amount, body);
409
+
410
+ if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
411
+ return { outcome: "error", statusCode: 400, body: { error: "Could not determine payment amount from request" } };
412
+ }
413
+
414
+ const currency = opts.currency ?? "usd";
415
+
416
+ // Check for crypto payment header
417
+ const paymentHeader = extractPaymentHeader(request.headers);
418
+ if (paymentHeader) {
419
+ return handleCryptoPayment(paymentHeader, effectiveAmount, currency);
420
+ }
421
+
422
+ // Check for card payment
423
+ const cardId = extractCardId(request.headers, body);
424
+ if (cardId) {
425
+ return handleCardPayment(request, cardId, effectiveAmount, currency, body, opts);
426
+ }
427
+
428
+ // Neither → 402
429
+ return build402Result(request.url, effectiveAmount, currency);
430
+ }
431
+
432
+ return { charge, processPaywall };
433
+ }
@@ -0,0 +1,10 @@
1
+ export class AgentSpendChargeError extends Error {
2
+ statusCode: number;
3
+ details: unknown;
4
+
5
+ constructor(message: string, statusCode: number, details?: unknown) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ this.details = details;
9
+ }
10
+ }