@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,6 @@
1
+ import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallRequest, PaywallResult } from "./types.js";
2
+ export interface AgentSpendClient {
3
+ charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
4
+ processPaywall(opts: PaywallOptions, request: PaywallRequest): Promise<PaywallResult>;
5
+ }
6
+ export declare function createAgentSpendClient(options: AgentSpendOptions): AgentSpendClient;
@@ -0,0 +1,308 @@
1
+ import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
2
+ import { registerExactEvmScheme } from "@x402/evm/exact/server";
3
+ import { AgentSpendChargeError } from "./error.js";
4
+ import { toCardId, joinUrl, bestEffortIdempotencyKey, toStringMetadata, resolvePlatformApiBaseUrl, resolveAmount, extractCardId, extractPaymentHeader } from "./helpers.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Factory
7
+ // ---------------------------------------------------------------------------
8
+ export function createAgentSpendClient(options) {
9
+ if (!options.serviceApiKey && !options.crypto) {
10
+ throw new AgentSpendChargeError("At least one of serviceApiKey or crypto config must be provided", 500);
11
+ }
12
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
13
+ if (!fetchImpl) {
14
+ throw new AgentSpendChargeError("No fetch implementation available", 500);
15
+ }
16
+ const platformApiBaseUrl = resolvePlatformApiBaseUrl(options.platformApiBaseUrl);
17
+ // -------------------------------------------------------------------
18
+ // Lazy service_id fetch + cache
19
+ // -------------------------------------------------------------------
20
+ let cachedServiceId = null;
21
+ async function getServiceId() {
22
+ if (cachedServiceId)
23
+ return cachedServiceId;
24
+ if (!options.serviceApiKey)
25
+ return null;
26
+ try {
27
+ const res = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/service/me"), {
28
+ headers: { authorization: `Bearer ${options.serviceApiKey}` }
29
+ });
30
+ if (res.ok) {
31
+ const data = (await res.json());
32
+ cachedServiceId = data.id ?? null;
33
+ }
34
+ }
35
+ catch { /* graceful fallback */ }
36
+ return cachedServiceId;
37
+ }
38
+ // -------------------------------------------------------------------
39
+ // x402 singleton setup
40
+ // -------------------------------------------------------------------
41
+ let facilitator = null;
42
+ let resourceServer = null;
43
+ const cryptoNetwork = (options.crypto?.network ?? "eip155:8453");
44
+ if (options.crypto || options.serviceApiKey) {
45
+ const facilitatorUrl = options.crypto?.facilitatorUrl ?? "https://facilitator.openx402.ai";
46
+ facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
47
+ resourceServer = new x402ResourceServer(facilitator);
48
+ registerExactEvmScheme(resourceServer);
49
+ }
50
+ // -------------------------------------------------------------------
51
+ // charge() — card-only
52
+ // -------------------------------------------------------------------
53
+ async function charge(cardIdInput, opts) {
54
+ if (!options.serviceApiKey) {
55
+ throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
56
+ }
57
+ const cardId = toCardId(cardIdInput);
58
+ if (!cardId) {
59
+ throw new AgentSpendChargeError("card_id must start with card_", 400);
60
+ }
61
+ if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
62
+ throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
63
+ }
64
+ const payload = {
65
+ card_id: cardId,
66
+ amount_cents: opts.amount_cents,
67
+ currency: opts.currency ?? "usd",
68
+ ...(opts.description ? { description: opts.description } : {}),
69
+ ...(opts.metadata ? { metadata: opts.metadata } : {}),
70
+ idempotency_key: opts.idempotency_key ?? bestEffortIdempotencyKey()
71
+ };
72
+ const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/charge"), {
73
+ method: "POST",
74
+ headers: {
75
+ authorization: `Bearer ${options.serviceApiKey}`,
76
+ "content-type": "application/json"
77
+ },
78
+ body: JSON.stringify(payload)
79
+ });
80
+ const responseBody = (await response.json().catch(() => ({})));
81
+ if (!response.ok) {
82
+ throw new AgentSpendChargeError(typeof responseBody.error === "string" ? responseBody.error : "AgentSpend charge failed", response.status, responseBody);
83
+ }
84
+ return responseBody;
85
+ }
86
+ // -------------------------------------------------------------------
87
+ // resolvePayToAddress
88
+ // -------------------------------------------------------------------
89
+ async function resolvePayToAddress() {
90
+ if (options.crypto?.receiverAddress) {
91
+ return options.crypto.receiverAddress;
92
+ }
93
+ if (options.serviceApiKey) {
94
+ const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/crypto/deposit-address"), {
95
+ method: "POST",
96
+ headers: {
97
+ authorization: `Bearer ${options.serviceApiKey}`,
98
+ "content-type": "application/json"
99
+ },
100
+ body: JSON.stringify({ amount_cents: 0, currency: "usd" })
101
+ });
102
+ if (!response.ok) {
103
+ throw new AgentSpendChargeError("Failed to resolve crypto deposit address", 502);
104
+ }
105
+ const data = (await response.json());
106
+ if (!data.deposit_address) {
107
+ throw new AgentSpendChargeError("No deposit address returned", 502);
108
+ }
109
+ return data.deposit_address;
110
+ }
111
+ throw new AgentSpendChargeError("No crypto payTo address available", 500);
112
+ }
113
+ // -------------------------------------------------------------------
114
+ // build402Result — x402 Payment-Required format
115
+ // -------------------------------------------------------------------
116
+ async function build402Result(requestUrl, amountCents, currency) {
117
+ const serviceId = await getServiceId();
118
+ try {
119
+ const payTo = await resolvePayToAddress();
120
+ const paymentRequirements = {
121
+ scheme: "exact",
122
+ network: cryptoNetwork,
123
+ amount: String(amountCents),
124
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
125
+ payTo,
126
+ maxTimeoutSeconds: 300,
127
+ extra: { name: "USD Coin", version: "2" }
128
+ };
129
+ const paymentRequired = {
130
+ x402Version: 2,
131
+ error: "Payment required",
132
+ resource: {
133
+ url: requestUrl,
134
+ description: `Payment of ${amountCents} cents`,
135
+ mimeType: "application/json"
136
+ },
137
+ accepts: [paymentRequirements]
138
+ };
139
+ const headerValue = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
140
+ return {
141
+ outcome: "payment_required",
142
+ statusCode: 402,
143
+ body: {
144
+ error: "Payment required",
145
+ amount_cents: amountCents,
146
+ currency,
147
+ ...(serviceId ? {
148
+ agentspend: {
149
+ service_id: serviceId,
150
+ amount_cents: amountCents,
151
+ }
152
+ } : {})
153
+ },
154
+ headers: { "Payment-Required": headerValue }
155
+ };
156
+ }
157
+ catch (error) {
158
+ console.error("[agentspend] Failed to resolve crypto payTo address — returning card-only 402:", error instanceof Error ? error.message : error);
159
+ return {
160
+ outcome: "payment_required",
161
+ statusCode: 402,
162
+ body: {
163
+ error: "Payment required",
164
+ amount_cents: amountCents,
165
+ currency,
166
+ ...(serviceId ? {
167
+ agentspend: {
168
+ service_id: serviceId,
169
+ amount_cents: amountCents,
170
+ }
171
+ } : {})
172
+ },
173
+ headers: {}
174
+ };
175
+ }
176
+ }
177
+ // -------------------------------------------------------------------
178
+ // handleCardPayment
179
+ // -------------------------------------------------------------------
180
+ async function handleCardPayment(request, cardId, amountCents, currency, body, opts) {
181
+ if (!options.serviceApiKey) {
182
+ return { outcome: "error", statusCode: 500, body: { error: "Card payments require serviceApiKey" } };
183
+ }
184
+ try {
185
+ const chargeResult = await charge(cardId, {
186
+ amount_cents: amountCents,
187
+ currency,
188
+ description: opts.description,
189
+ metadata: opts.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
190
+ idempotency_key: request.headers["x-request-id"] ?? request.headers["idempotency-key"] ?? undefined
191
+ });
192
+ const paymentContext = {
193
+ method: "card",
194
+ amount_cents: amountCents,
195
+ currency,
196
+ card_id: cardId,
197
+ remaining_limit_cents: chargeResult.remaining_limit_cents
198
+ };
199
+ return { outcome: "charged", paymentContext };
200
+ }
201
+ catch (error) {
202
+ if (error instanceof AgentSpendChargeError) {
203
+ if (error.statusCode === 403) {
204
+ return build402Result(request.url, amountCents, currency);
205
+ }
206
+ if (error.statusCode === 402) {
207
+ return { outcome: "error", statusCode: 402, body: { error: "Payment required", details: error.details } };
208
+ }
209
+ return { outcome: "error", statusCode: error.statusCode, body: { error: error.message, details: error.details } };
210
+ }
211
+ return { outcome: "error", statusCode: 500, body: { error: "Unexpected paywall failure" } };
212
+ }
213
+ }
214
+ // -------------------------------------------------------------------
215
+ // handleCryptoPayment
216
+ // -------------------------------------------------------------------
217
+ async function handleCryptoPayment(paymentHeader, amountCents, currency) {
218
+ if (!resourceServer) {
219
+ return { outcome: "error", statusCode: 500, body: { error: "Crypto payments not configured" } };
220
+ }
221
+ try {
222
+ let paymentPayload;
223
+ try {
224
+ paymentPayload = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8"));
225
+ }
226
+ catch {
227
+ return { outcome: "error", statusCode: 400, body: { error: "Invalid payment payload encoding" } };
228
+ }
229
+ const acceptedPayTo = paymentPayload
230
+ .accepted?.payTo;
231
+ const payTo = acceptedPayTo ?? await resolvePayToAddress();
232
+ const paymentRequirements = {
233
+ scheme: "exact",
234
+ network: cryptoNetwork,
235
+ amount: String(amountCents),
236
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
237
+ payTo,
238
+ maxTimeoutSeconds: 300,
239
+ extra: { name: "USD Coin", version: "2" }
240
+ };
241
+ const verifyResult = await resourceServer.verifyPayment(paymentPayload, paymentRequirements);
242
+ if (!verifyResult.isValid) {
243
+ return {
244
+ outcome: "error",
245
+ statusCode: 402,
246
+ body: { error: "Payment verification failed", details: verifyResult.invalidReason }
247
+ };
248
+ }
249
+ const settleResult = await resourceServer.settlePayment(paymentPayload, paymentRequirements);
250
+ if (!settleResult.success) {
251
+ return {
252
+ outcome: "error",
253
+ statusCode: 402,
254
+ body: { error: "Payment settlement failed", details: settleResult.errorReason }
255
+ };
256
+ }
257
+ const paymentContext = {
258
+ method: "crypto",
259
+ amount_cents: amountCents,
260
+ currency,
261
+ transaction_hash: settleResult.transaction,
262
+ payer_address: verifyResult.payer ?? undefined,
263
+ network: cryptoNetwork
264
+ };
265
+ return { outcome: "crypto_paid", paymentContext };
266
+ }
267
+ catch (error) {
268
+ if (error instanceof AgentSpendChargeError) {
269
+ return { outcome: "error", statusCode: error.statusCode, body: { error: error.message, details: error.details } };
270
+ }
271
+ return {
272
+ outcome: "error",
273
+ statusCode: 500,
274
+ body: { error: "Crypto payment processing failed", details: error.message }
275
+ };
276
+ }
277
+ }
278
+ // -------------------------------------------------------------------
279
+ // processPaywall() — unified entry point for adapters
280
+ // -------------------------------------------------------------------
281
+ async function processPaywall(opts, request) {
282
+ const { amount } = opts;
283
+ if (typeof amount === "number") {
284
+ if (!Number.isInteger(amount) || amount <= 0) {
285
+ throw new AgentSpendChargeError("amount must be a positive integer", 500);
286
+ }
287
+ }
288
+ const body = request.body;
289
+ const effectiveAmount = resolveAmount(amount, body);
290
+ if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
291
+ return { outcome: "error", statusCode: 400, body: { error: "Could not determine payment amount from request" } };
292
+ }
293
+ const currency = opts.currency ?? "usd";
294
+ // Check for crypto payment header
295
+ const paymentHeader = extractPaymentHeader(request.headers);
296
+ if (paymentHeader) {
297
+ return handleCryptoPayment(paymentHeader, effectiveAmount, currency);
298
+ }
299
+ // Check for card payment
300
+ const cardId = extractCardId(request.headers, body);
301
+ if (cardId) {
302
+ return handleCardPayment(request, cardId, effectiveAmount, currency, body, opts);
303
+ }
304
+ // Neither → 402
305
+ return build402Result(request.url, effectiveAmount, currency);
306
+ }
307
+ return { charge, processPaywall };
308
+ }
@@ -0,0 +1,5 @@
1
+ export declare class AgentSpendChargeError extends Error {
2
+ statusCode: number;
3
+ details: unknown;
4
+ constructor(message: string, statusCode: number, details?: unknown);
5
+ }
@@ -0,0 +1,9 @@
1
+ export class AgentSpendChargeError extends Error {
2
+ statusCode;
3
+ details;
4
+ constructor(message, statusCode, details) {
5
+ super(message);
6
+ this.statusCode = statusCode;
7
+ this.details = details;
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ export declare function toCardId(input: unknown): string | null;
2
+ export declare function joinUrl(base: string, path: string): string;
3
+ export declare function bestEffortIdempotencyKey(): string;
4
+ export declare function toStringMetadata(input: unknown): Record<string, string>;
5
+ export declare function resolvePlatformApiBaseUrl(explicitBaseUrl: string | undefined): string;
6
+ export declare function resolveAmount(amount: number | string | ((body: unknown) => number), body: unknown): number;
7
+ export declare function extractCardId(headers: Record<string, string | undefined>, body: unknown): string | null;
8
+ export declare function extractPaymentHeader(headers: Record<string, string | undefined>): string | undefined;
@@ -0,0 +1,77 @@
1
+ export function toCardId(input) {
2
+ if (typeof input !== "string") {
3
+ return null;
4
+ }
5
+ const trimmed = input.trim();
6
+ if (!trimmed.startsWith("card_")) {
7
+ return null;
8
+ }
9
+ return trimmed;
10
+ }
11
+ export function joinUrl(base, path) {
12
+ const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
13
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
14
+ return `${normalizedBase}${normalizedPath}`;
15
+ }
16
+ export function bestEffortIdempotencyKey() {
17
+ const uuid = globalThis.crypto?.randomUUID?.();
18
+ if (uuid) {
19
+ return uuid;
20
+ }
21
+ return `auto_${Date.now()}_${Math.random().toString(16).slice(2)}`;
22
+ }
23
+ export function toStringMetadata(input) {
24
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
25
+ return {};
26
+ }
27
+ const result = {};
28
+ for (const [key, value] of Object.entries(input)) {
29
+ if (typeof value === "string") {
30
+ result[key] = value;
31
+ }
32
+ else if (typeof value === "number" && Number.isFinite(value)) {
33
+ result[key] = String(value);
34
+ }
35
+ else if (typeof value === "boolean") {
36
+ result[key] = value ? "true" : "false";
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ const DEFAULT_PLATFORM_API_BASE_URL = "https://api.agentspend.co";
42
+ export function resolvePlatformApiBaseUrl(explicitBaseUrl) {
43
+ if (explicitBaseUrl && explicitBaseUrl.trim().length > 0) {
44
+ return explicitBaseUrl.trim();
45
+ }
46
+ const envValue = typeof process !== "undefined" && process.env ? process.env.AGENTSPEND_API_URL : undefined;
47
+ if (typeof envValue === "string" && envValue.trim().length > 0) {
48
+ return envValue.trim();
49
+ }
50
+ return DEFAULT_PLATFORM_API_BASE_URL;
51
+ }
52
+ export function resolveAmount(amount, body) {
53
+ if (typeof amount === "number") {
54
+ return amount;
55
+ }
56
+ else if (typeof amount === "string") {
57
+ const raw = body?.[amount];
58
+ return typeof raw === "number" ? raw : 0;
59
+ }
60
+ else {
61
+ return amount(body);
62
+ }
63
+ }
64
+ export function extractCardId(headers, body) {
65
+ const cardIdFromHeader = headers["x-card-id"];
66
+ let cardId = cardIdFromHeader ? toCardId(cardIdFromHeader) : null;
67
+ if (!cardId) {
68
+ const bodyCardId = typeof body?.card_id === "string"
69
+ ? body.card_id
70
+ : null;
71
+ cardId = toCardId(bodyCardId);
72
+ }
73
+ return cardId;
74
+ }
75
+ export function extractPaymentHeader(headers) {
76
+ return headers["payment-signature"] ?? headers["x-payment"] ?? undefined;
77
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export * from "./error.js";
3
+ export * from "./helpers.js";
4
+ export { createAgentSpendClient } from "./client.js";
5
+ export type { AgentSpendClient } from "./client.js";
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./error.js";
3
+ export * from "./helpers.js";
4
+ export { createAgentSpendClient } from "./client.js";
@@ -58,23 +58,6 @@ export interface ChargeOptions {
58
58
  metadata?: Record<string, string>;
59
59
  idempotency_key?: string;
60
60
  }
61
- export declare class AgentSpendChargeError extends Error {
62
- statusCode: number;
63
- details: unknown;
64
- constructor(message: string, statusCode: number, details?: unknown);
65
- }
66
- export interface HonoContextLike {
67
- req: {
68
- header(name: string): string | undefined;
69
- json(): Promise<unknown>;
70
- url: string;
71
- method: string;
72
- };
73
- json(body: unknown, status?: number): Response;
74
- header(name: string, value: string): void;
75
- set(key: string, value: unknown): void;
76
- get(key: string): unknown;
77
- }
78
61
  export interface PaywallOptions {
79
62
  /**
80
63
  * Amount in cents.
@@ -87,9 +70,25 @@ export interface PaywallOptions {
87
70
  description?: string;
88
71
  metadata?: (body: unknown) => Record<string, unknown>;
89
72
  }
90
- export declare function getPaymentContext(c: HonoContextLike): PaywallPaymentContext | null;
91
- export interface AgentSpend {
92
- charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
93
- paywall(opts: PaywallOptions): (c: HonoContextLike, next: () => Promise<void>) => Promise<Response | void>;
73
+ export interface PaywallRequest {
74
+ url: string;
75
+ method: string;
76
+ headers: Record<string, string | undefined>;
77
+ body: unknown;
94
78
  }
95
- export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
79
+ export type PaywallResult = {
80
+ outcome: "charged";
81
+ paymentContext: PaywallPaymentContext;
82
+ } | {
83
+ outcome: "crypto_paid";
84
+ paymentContext: PaywallPaymentContext;
85
+ } | {
86
+ outcome: "payment_required";
87
+ statusCode: 402;
88
+ body: Record<string, unknown>;
89
+ headers: Record<string, string>;
90
+ } | {
91
+ outcome: "error";
92
+ statusCode: number;
93
+ body: Record<string, unknown>;
94
+ };
@@ -0,0 +1,4 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Types (inlined from @agentspend/types to avoid cross-repo publish)
3
+ // ---------------------------------------------------------------------------
4
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallPaymentContext } from "../core/types.js";
2
+ export * from "../core/index.js";
3
+ interface ExpressRequest {
4
+ body: unknown;
5
+ url: string;
6
+ method: string;
7
+ get(name: string): string | undefined;
8
+ header(name: string): string | undefined;
9
+ paymentContext?: PaywallPaymentContext;
10
+ }
11
+ interface ExpressResponse {
12
+ status(code: number): ExpressResponse;
13
+ set(name: string, value: string): ExpressResponse;
14
+ json(body: unknown): void;
15
+ }
16
+ type NextFunction = (err?: unknown) => void;
17
+ export declare function getPaymentContext(req: ExpressRequest): PaywallPaymentContext | null;
18
+ export interface AgentSpend {
19
+ charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
20
+ paywall(opts: PaywallOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => void;
21
+ }
22
+ export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
@@ -0,0 +1,50 @@
1
+ import { createAgentSpendClient } from "../core/client.js";
2
+ export * from "../core/index.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Payment context helper
5
+ // ---------------------------------------------------------------------------
6
+ export function getPaymentContext(req) {
7
+ return req.paymentContext ?? null;
8
+ }
9
+ // ---------------------------------------------------------------------------
10
+ // Factory
11
+ // ---------------------------------------------------------------------------
12
+ export function createAgentSpend(options) {
13
+ const client = createAgentSpendClient(options);
14
+ function paywall(opts) {
15
+ return function paywallMiddleware(req, res, next) {
16
+ const headerGet = (name) => req.get?.(name) ?? req.header?.(name);
17
+ const request = {
18
+ url: req.url,
19
+ method: req.method,
20
+ headers: {
21
+ "x-card-id": headerGet("x-card-id"),
22
+ "payment-signature": headerGet("payment-signature"),
23
+ "x-payment": headerGet("x-payment"),
24
+ "x-request-id": headerGet("x-request-id"),
25
+ "idempotency-key": headerGet("idempotency-key"),
26
+ },
27
+ body: req.body
28
+ };
29
+ client.processPaywall(opts, request).then((result) => {
30
+ switch (result.outcome) {
31
+ case "charged":
32
+ case "crypto_paid":
33
+ req.paymentContext = result.paymentContext;
34
+ next();
35
+ return;
36
+ case "payment_required":
37
+ for (const [key, value] of Object.entries(result.headers)) {
38
+ res.set(key, value);
39
+ }
40
+ res.status(result.statusCode).json(result.body);
41
+ return;
42
+ case "error":
43
+ res.status(result.statusCode).json(result.body);
44
+ return;
45
+ }
46
+ }).catch((err) => next(err));
47
+ };
48
+ }
49
+ return { charge: client.charge, paywall };
50
+ }
@@ -0,0 +1,27 @@
1
+ import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallPaymentContext } from "../core/types.js";
2
+ export * from "../core/index.js";
3
+ interface FastifyRequest {
4
+ body: unknown;
5
+ url: string;
6
+ method: string;
7
+ headers: Record<string, string | string[] | undefined>;
8
+ paymentContext?: PaywallPaymentContext;
9
+ }
10
+ interface FastifyReply {
11
+ code(statusCode: number): FastifyReply;
12
+ header(name: string, value: string): FastifyReply;
13
+ send(body: unknown): FastifyReply;
14
+ }
15
+ interface FastifyInstance {
16
+ decorate(name: string, value: unknown): void;
17
+ decorateRequest(name: string, value: unknown): void;
18
+ addHook(name: string, handler: (req: FastifyRequest, reply: FastifyReply) => Promise<void>): void;
19
+ }
20
+ type DoneCallback = (err?: Error) => void;
21
+ export declare function getPaymentContext(req: FastifyRequest): PaywallPaymentContext | null;
22
+ export interface AgentSpend {
23
+ charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
24
+ paywall(opts: PaywallOptions): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
25
+ plugin(opts: PaywallOptions): (fastify: FastifyInstance, _options: unknown, done: DoneCallback) => void;
26
+ }
27
+ export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
@@ -0,0 +1,58 @@
1
+ import { createAgentSpendClient } from "../core/client.js";
2
+ export * from "../core/index.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Payment context helper
5
+ // ---------------------------------------------------------------------------
6
+ export function getPaymentContext(req) {
7
+ return req.paymentContext ?? null;
8
+ }
9
+ // ---------------------------------------------------------------------------
10
+ // Factory
11
+ // ---------------------------------------------------------------------------
12
+ export function createAgentSpend(options) {
13
+ const client = createAgentSpendClient(options);
14
+ function paywall(opts) {
15
+ return async function preHandler(req, reply) {
16
+ const headerGet = (name) => {
17
+ const val = req.headers[name];
18
+ return Array.isArray(val) ? val[0] : val;
19
+ };
20
+ const request = {
21
+ url: req.url,
22
+ method: req.method,
23
+ headers: {
24
+ "x-card-id": headerGet("x-card-id"),
25
+ "payment-signature": headerGet("payment-signature"),
26
+ "x-payment": headerGet("x-payment"),
27
+ "x-request-id": headerGet("x-request-id"),
28
+ "idempotency-key": headerGet("idempotency-key"),
29
+ },
30
+ body: req.body
31
+ };
32
+ const result = await client.processPaywall(opts, request);
33
+ switch (result.outcome) {
34
+ case "charged":
35
+ case "crypto_paid":
36
+ req.paymentContext = result.paymentContext;
37
+ return;
38
+ case "payment_required":
39
+ for (const [key, value] of Object.entries(result.headers)) {
40
+ reply.header(key, value);
41
+ }
42
+ reply.code(result.statusCode).send(result.body);
43
+ return;
44
+ case "error":
45
+ reply.code(result.statusCode).send(result.body);
46
+ return;
47
+ }
48
+ };
49
+ }
50
+ function plugin(opts) {
51
+ return function agentSpendPlugin(fastify, _options, done) {
52
+ fastify.decorateRequest("paymentContext", null);
53
+ fastify.addHook("preHandler", paywall(opts));
54
+ done();
55
+ };
56
+ }
57
+ return { charge: client.charge, paywall, plugin };
58
+ }