@agentspend/sdk 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +21 -20
- package/dist/index.js +239 -31
- package/package.json +7 -14
- package/src/index.ts +565 -0
- package/tsconfig.json +8 -0
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
amount_cents: number;
|
|
4
|
-
currency?: string;
|
|
5
|
-
description?: string;
|
|
6
|
-
metadata?: Record<string, string>;
|
|
7
|
-
idempotency_key?: string;
|
|
8
|
-
}
|
|
9
|
-
export interface ChargeResponse {
|
|
10
|
-
charged: true;
|
|
11
|
-
wallet_id: string;
|
|
12
|
-
amount_cents: number;
|
|
13
|
-
currency: string;
|
|
14
|
-
remaining_limit_cents: number;
|
|
15
|
-
stripe_payment_intent_id: string;
|
|
16
|
-
stripe_charge_id: string;
|
|
17
|
-
charge_attempt_id: string;
|
|
18
|
-
}
|
|
1
|
+
import type { ChargeResponse, PaywallPaymentContext } from "@agentspend/types";
|
|
2
|
+
export type { ChargeRequest, ChargeResponse, ErrorResponse, PaymentMethod, PaywallPaymentContext } from "@agentspend/types";
|
|
19
3
|
export interface AgentSpendOptions {
|
|
20
4
|
/**
|
|
21
5
|
* Base URL for the AgentSpend Platform API.
|
|
@@ -24,8 +8,18 @@ export interface AgentSpendOptions {
|
|
|
24
8
|
* otherwise it falls back to the hosted default.
|
|
25
9
|
*/
|
|
26
10
|
platformApiBaseUrl?: string;
|
|
27
|
-
|
|
11
|
+
/** Service API key. Optional — crypto-only services don't need one. */
|
|
12
|
+
serviceApiKey?: string;
|
|
28
13
|
fetchImpl?: typeof fetch;
|
|
14
|
+
/** Crypto / x402 configuration. */
|
|
15
|
+
crypto?: {
|
|
16
|
+
/** Static payTo address for crypto-only services. */
|
|
17
|
+
receiverAddress?: string;
|
|
18
|
+
/** Chain identifier. Default: "eip155:8453" (Base). */
|
|
19
|
+
network?: string;
|
|
20
|
+
/** x402 facilitator URL. Default: "https://x402.org/facilitator". */
|
|
21
|
+
facilitatorUrl?: string;
|
|
22
|
+
};
|
|
29
23
|
}
|
|
30
24
|
export interface ChargeOptions {
|
|
31
25
|
amount_cents: number;
|
|
@@ -43,17 +37,24 @@ export interface HonoContextLike {
|
|
|
43
37
|
req: {
|
|
44
38
|
header(name: string): string | undefined;
|
|
45
39
|
json(): Promise<unknown>;
|
|
40
|
+
url: string;
|
|
41
|
+
method: string;
|
|
46
42
|
};
|
|
47
43
|
json(body: unknown, status?: number): unknown;
|
|
44
|
+
header(name: string, value: string): void;
|
|
45
|
+
set(key: string, value: unknown): void;
|
|
46
|
+
get(key: string): unknown;
|
|
48
47
|
}
|
|
49
48
|
export interface PaywallOptions {
|
|
50
49
|
currency?: string;
|
|
51
50
|
description?: string;
|
|
52
51
|
metadata?: (body: unknown) => Record<string, unknown>;
|
|
52
|
+
/** Dynamic pricing: derive amount from the parsed request body. */
|
|
53
|
+
amountFromRequest?: (body: unknown) => number;
|
|
53
54
|
}
|
|
55
|
+
export declare function getPaymentContext(c: HonoContextLike): PaywallPaymentContext | null;
|
|
54
56
|
export interface AgentSpend {
|
|
55
57
|
charge(walletId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
56
58
|
paywall(amountCents: number, opts?: PaywallOptions): (c: HonoContextLike, next: () => Promise<void>) => Promise<unknown>;
|
|
57
59
|
}
|
|
58
60
|
export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
|
|
59
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentSpendChargeError = void 0;
|
|
4
|
+
exports.getPaymentContext = getPaymentContext;
|
|
5
|
+
exports.createAgentSpend = createAgentSpend;
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// x402 imports – server-side only (HTTP calls to facilitator, no crypto deps)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const server_1 = require("@x402/core/server");
|
|
10
|
+
class AgentSpendChargeError extends Error {
|
|
2
11
|
statusCode;
|
|
3
12
|
details;
|
|
4
13
|
constructor(message, statusCode, details) {
|
|
@@ -7,13 +16,48 @@ export class AgentSpendChargeError extends Error {
|
|
|
7
16
|
this.details = details;
|
|
8
17
|
}
|
|
9
18
|
}
|
|
10
|
-
|
|
19
|
+
exports.AgentSpendChargeError = AgentSpendChargeError;
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Payment context helper
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const PAYMENT_CONTEXT_KEY = "payment";
|
|
24
|
+
function getPaymentContext(c) {
|
|
25
|
+
const ctx = c.get(PAYMENT_CONTEXT_KEY);
|
|
26
|
+
return ctx ?? null;
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Factory
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
function createAgentSpend(options) {
|
|
32
|
+
// Validate: at least one of serviceApiKey or crypto must be provided
|
|
33
|
+
if (!options.serviceApiKey && !options.crypto) {
|
|
34
|
+
throw new AgentSpendChargeError("At least one of serviceApiKey or crypto config must be provided", 500);
|
|
35
|
+
}
|
|
11
36
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
12
37
|
if (!fetchImpl) {
|
|
13
38
|
throw new AgentSpendChargeError("No fetch implementation available", 500);
|
|
14
39
|
}
|
|
15
40
|
const platformApiBaseUrl = resolvePlatformApiBaseUrl(options.platformApiBaseUrl);
|
|
41
|
+
// -------------------------------------------------------------------
|
|
42
|
+
// x402 singleton setup (Decision 9)
|
|
43
|
+
// Server-side: facilitator handles verify + settle over HTTP.
|
|
44
|
+
// No client-side EVM scheme needed — we delegate to the facilitator.
|
|
45
|
+
// -------------------------------------------------------------------
|
|
46
|
+
let facilitator = null;
|
|
47
|
+
let resourceServer = null;
|
|
48
|
+
const cryptoNetwork = (options.crypto?.network ?? "eip155:8453");
|
|
49
|
+
if (options.crypto || options.serviceApiKey) {
|
|
50
|
+
const facilitatorUrl = options.crypto?.facilitatorUrl ?? "https://x402.org/facilitator";
|
|
51
|
+
facilitator = new server_1.HTTPFacilitatorClient({ url: facilitatorUrl });
|
|
52
|
+
resourceServer = new server_1.x402ResourceServer(facilitator);
|
|
53
|
+
}
|
|
54
|
+
// -------------------------------------------------------------------
|
|
55
|
+
// charge() — card-only, unchanged
|
|
56
|
+
// -------------------------------------------------------------------
|
|
16
57
|
async function charge(walletIdInput, opts) {
|
|
58
|
+
if (!options.serviceApiKey) {
|
|
59
|
+
throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
|
|
60
|
+
}
|
|
17
61
|
const walletId = toWalletId(walletIdInput);
|
|
18
62
|
if (!walletId) {
|
|
19
63
|
throw new AgentSpendChargeError("wallet_id must start with wal_", 400);
|
|
@@ -43,50 +87,216 @@ export function createAgentSpend(options) {
|
|
|
43
87
|
}
|
|
44
88
|
return responseBody;
|
|
45
89
|
}
|
|
90
|
+
// -------------------------------------------------------------------
|
|
91
|
+
// paywall() — unified card + crypto middleware
|
|
92
|
+
// -------------------------------------------------------------------
|
|
46
93
|
function paywall(amountCents, opts) {
|
|
47
|
-
|
|
48
|
-
|
|
94
|
+
// Allow amountCents === 0 when amountFromRequest is provided (dynamic pricing)
|
|
95
|
+
if (!opts?.amountFromRequest) {
|
|
96
|
+
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
|
97
|
+
throw new AgentSpendChargeError("amountCents must be a positive integer", 500);
|
|
98
|
+
}
|
|
49
99
|
}
|
|
50
100
|
return async function paywallMiddleware(c, next) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
// Step 1: Parse body once (Decision 11)
|
|
102
|
+
const body = await c.req.json().catch(() => ({}));
|
|
103
|
+
// Step 2: Determine effective amount
|
|
104
|
+
let effectiveAmount = amountCents;
|
|
105
|
+
if (opts?.amountFromRequest) {
|
|
106
|
+
effectiveAmount = opts.amountFromRequest(body);
|
|
107
|
+
if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
|
|
108
|
+
return c.json({ error: "Could not determine payment amount from request" }, 400);
|
|
59
109
|
}
|
|
60
110
|
}
|
|
111
|
+
const currency = opts?.currency ?? "usd";
|
|
112
|
+
// Step 3: Check for x-payment header → crypto payment
|
|
113
|
+
const paymentHeader = c.req.header("x-payment");
|
|
114
|
+
if (paymentHeader) {
|
|
115
|
+
return handleCryptoPayment(c, next, paymentHeader, effectiveAmount, currency, body, opts);
|
|
116
|
+
}
|
|
117
|
+
// Step 4: Check for x-wallet-id header or body.wallet_id → card payment
|
|
118
|
+
const walletIdFromHeader = c.req.header("x-wallet-id");
|
|
119
|
+
let walletId = walletIdFromHeader ? toWalletId(walletIdFromHeader) : null;
|
|
61
120
|
if (!walletId) {
|
|
62
|
-
|
|
121
|
+
const bodyWalletId = typeof body?.wallet_id === "string"
|
|
122
|
+
? body.wallet_id
|
|
123
|
+
: null;
|
|
124
|
+
walletId = toWalletId(bodyWalletId);
|
|
63
125
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
amount_cents: amountCents,
|
|
67
|
-
currency: opts?.currency ?? "usd",
|
|
68
|
-
description: opts?.description,
|
|
69
|
-
metadata: opts?.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
|
|
70
|
-
idempotency_key: c.req.header("x-request-id") ?? c.req.header("idempotency-key") ?? undefined
|
|
71
|
-
});
|
|
126
|
+
if (walletId) {
|
|
127
|
+
return handleCardPayment(c, next, walletId, effectiveAmount, currency, body, opts);
|
|
72
128
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
129
|
+
// Step 5: Neither → return 402 with Payment-Required header (Decision 8)
|
|
130
|
+
return return402Response(c, effectiveAmount, currency);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// -------------------------------------------------------------------
|
|
134
|
+
// handleCardPayment — existing charge() flow
|
|
135
|
+
// -------------------------------------------------------------------
|
|
136
|
+
async function handleCardPayment(c, next, walletId, amountCents, currency, body, opts) {
|
|
137
|
+
if (!options.serviceApiKey) {
|
|
138
|
+
return c.json({ error: "Card payments require serviceApiKey" }, 500);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const chargeResult = await charge(walletId, {
|
|
142
|
+
amount_cents: amountCents,
|
|
143
|
+
currency,
|
|
144
|
+
description: opts?.description,
|
|
145
|
+
metadata: opts?.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
|
|
146
|
+
idempotency_key: c.req.header("x-request-id") ?? c.req.header("idempotency-key") ?? undefined
|
|
147
|
+
});
|
|
148
|
+
const paymentContext = {
|
|
149
|
+
method: "card",
|
|
150
|
+
amount_cents: amountCents,
|
|
151
|
+
currency,
|
|
152
|
+
wallet_id: walletId,
|
|
153
|
+
remaining_limit_cents: chargeResult.remaining_limit_cents
|
|
154
|
+
};
|
|
155
|
+
c.set(PAYMENT_CONTEXT_KEY, paymentContext);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof AgentSpendChargeError) {
|
|
159
|
+
if (error.statusCode === 402) {
|
|
160
|
+
return c.json({ error: "Payment required", details: error.details }, 402);
|
|
79
161
|
}
|
|
80
|
-
return c.json({ error:
|
|
162
|
+
return c.json({ error: error.message, details: error.details }, error.statusCode);
|
|
81
163
|
}
|
|
164
|
+
return c.json({ error: "Unexpected paywall failure" }, 500);
|
|
165
|
+
}
|
|
166
|
+
await next();
|
|
167
|
+
}
|
|
168
|
+
// -------------------------------------------------------------------
|
|
169
|
+
// handleCryptoPayment — x402 verify + settle via facilitator
|
|
170
|
+
// -------------------------------------------------------------------
|
|
171
|
+
async function handleCryptoPayment(c, next, paymentHeader, amountCents, currency, _body, _opts) {
|
|
172
|
+
if (!facilitator) {
|
|
173
|
+
return c.json({ error: "Crypto payments not configured" }, 500);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Decode the x-payment header (base64 JSON payment payload)
|
|
177
|
+
let paymentPayload;
|
|
178
|
+
try {
|
|
179
|
+
paymentPayload = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8"));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return c.json({ error: "Invalid payment payload encoding" }, 400);
|
|
183
|
+
}
|
|
184
|
+
// Resolve the payTo address for verification context
|
|
185
|
+
const payTo = await resolvePayToAddress();
|
|
186
|
+
// Build the payment requirements that the payment should satisfy
|
|
187
|
+
const paymentRequirements = {
|
|
188
|
+
scheme: "exact",
|
|
189
|
+
network: cryptoNetwork,
|
|
190
|
+
amount: String(amountCents),
|
|
191
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
192
|
+
payTo,
|
|
193
|
+
maxTimeoutSeconds: 300,
|
|
194
|
+
extra: {}
|
|
195
|
+
};
|
|
196
|
+
// Verify payment via facilitator
|
|
197
|
+
const verifyResult = await facilitator.verify(paymentPayload, paymentRequirements);
|
|
198
|
+
if (!verifyResult.isValid) {
|
|
199
|
+
return c.json({ error: "Payment verification failed", details: verifyResult.invalidReason }, 402);
|
|
200
|
+
}
|
|
201
|
+
// Settle payment via facilitator
|
|
202
|
+
const settleResult = await facilitator.settle(paymentPayload, paymentRequirements);
|
|
203
|
+
if (!settleResult.success) {
|
|
204
|
+
return c.json({ error: "Payment settlement failed", details: settleResult.errorReason }, 402);
|
|
205
|
+
}
|
|
206
|
+
const paymentContext = {
|
|
207
|
+
method: "crypto",
|
|
208
|
+
amount_cents: amountCents,
|
|
209
|
+
currency,
|
|
210
|
+
transaction_hash: settleResult.transaction,
|
|
211
|
+
payer_address: verifyResult.payer ?? undefined,
|
|
212
|
+
network: cryptoNetwork
|
|
213
|
+
};
|
|
214
|
+
c.set(PAYMENT_CONTEXT_KEY, paymentContext);
|
|
82
215
|
await next();
|
|
83
|
-
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof AgentSpendChargeError) {
|
|
219
|
+
return c.json({ error: error.message, details: error.details }, error.statusCode);
|
|
220
|
+
}
|
|
221
|
+
return c.json({ error: "Crypto payment processing failed", details: error.message }, 500);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// -------------------------------------------------------------------
|
|
225
|
+
// return402Response — x402 Payment-Required format (Decision 8)
|
|
226
|
+
// -------------------------------------------------------------------
|
|
227
|
+
async function return402Response(c, amountCents, currency) {
|
|
228
|
+
try {
|
|
229
|
+
const payTo = await resolvePayToAddress();
|
|
230
|
+
const paymentRequirements = {
|
|
231
|
+
scheme: "exact",
|
|
232
|
+
network: cryptoNetwork,
|
|
233
|
+
amount: String(amountCents),
|
|
234
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
235
|
+
payTo,
|
|
236
|
+
maxTimeoutSeconds: 300,
|
|
237
|
+
extra: {}
|
|
238
|
+
};
|
|
239
|
+
// Build x402 v2 PaymentRequired response
|
|
240
|
+
const paymentRequired = {
|
|
241
|
+
x402Version: 2,
|
|
242
|
+
error: "Payment required",
|
|
243
|
+
resource: {
|
|
244
|
+
url: c.req.url,
|
|
245
|
+
description: `Payment of ${amountCents} cents`,
|
|
246
|
+
mimeType: "application/json"
|
|
247
|
+
},
|
|
248
|
+
accepts: [paymentRequirements]
|
|
249
|
+
};
|
|
250
|
+
// Set Payment-Required header (base64 encoded)
|
|
251
|
+
const headerValue = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
252
|
+
c.header("Payment-Required", headerValue);
|
|
253
|
+
return c.json({ error: "Payment required", amount_cents: amountCents, currency }, 402);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// If we can't resolve a payTo address, return a plain 402
|
|
257
|
+
return c.json({ error: "Payment required", amount_cents: amountCents, currency }, 402);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// -------------------------------------------------------------------
|
|
261
|
+
// resolvePayToAddress — static wallet or Stripe Machine Payments
|
|
262
|
+
// -------------------------------------------------------------------
|
|
263
|
+
async function resolvePayToAddress() {
|
|
264
|
+
// Static address for crypto-only services
|
|
265
|
+
if (options.crypto?.receiverAddress) {
|
|
266
|
+
return options.crypto.receiverAddress;
|
|
267
|
+
}
|
|
268
|
+
// Stripe Connect service → get deposit address from platform
|
|
269
|
+
if (options.serviceApiKey) {
|
|
270
|
+
const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/crypto/deposit-address"), {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: {
|
|
273
|
+
authorization: `Bearer ${options.serviceApiKey}`,
|
|
274
|
+
"content-type": "application/json"
|
|
275
|
+
},
|
|
276
|
+
body: JSON.stringify({ amount_cents: 0, currency: "usd" })
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
throw new AgentSpendChargeError("Failed to resolve crypto deposit address", 502);
|
|
280
|
+
}
|
|
281
|
+
const data = (await response.json());
|
|
282
|
+
if (!data.deposit_address) {
|
|
283
|
+
throw new AgentSpendChargeError("No deposit address returned", 502);
|
|
284
|
+
}
|
|
285
|
+
return data.deposit_address;
|
|
286
|
+
}
|
|
287
|
+
throw new AgentSpendChargeError("No crypto payTo address available", 500);
|
|
84
288
|
}
|
|
289
|
+
// -------------------------------------------------------------------
|
|
290
|
+
// Return public interface
|
|
291
|
+
// -------------------------------------------------------------------
|
|
85
292
|
return {
|
|
86
293
|
charge,
|
|
87
294
|
paywall
|
|
88
295
|
};
|
|
89
296
|
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Helpers (unchanged from original)
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
90
300
|
function toWalletId(input) {
|
|
91
301
|
if (typeof input !== "string") {
|
|
92
302
|
return null;
|
|
@@ -132,11 +342,9 @@ function resolvePlatformApiBaseUrl(explicitBaseUrl) {
|
|
|
132
342
|
if (explicitBaseUrl && explicitBaseUrl.trim().length > 0) {
|
|
133
343
|
return explicitBaseUrl.trim();
|
|
134
344
|
}
|
|
135
|
-
// Prefer environment config when running on Node/Bun. Guarded for non-Node runtimes.
|
|
136
345
|
const envValue = typeof process !== "undefined" && process.env ? process.env.AGENTSPEND_API_URL : undefined;
|
|
137
346
|
if (typeof envValue === "string" && envValue.trim().length > 0) {
|
|
138
347
|
return envValue.trim();
|
|
139
348
|
}
|
|
140
349
|
return DEFAULT_PLATFORM_API_BASE_URL;
|
|
141
350
|
}
|
|
142
|
-
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentspend/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.2.0",
|
|
5
4
|
"main": "dist/index.js",
|
|
6
5
|
"types": "dist/index.d.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
|
-
"default": "./dist/index.js"
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"dist"
|
|
15
|
-
],
|
|
16
6
|
"publishConfig": {
|
|
17
7
|
"access": "public"
|
|
18
8
|
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@agentspend/types": "^0.2.0",
|
|
11
|
+
"@x402/core": "^2.3.1"
|
|
12
|
+
},
|
|
19
13
|
"scripts": {
|
|
20
|
-
"build": "tsc
|
|
21
|
-
"
|
|
22
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
23
16
|
}
|
|
24
17
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChargeRequest,
|
|
3
|
+
ChargeResponse,
|
|
4
|
+
ErrorResponse,
|
|
5
|
+
PaymentMethod,
|
|
6
|
+
PaywallPaymentContext
|
|
7
|
+
} from "@agentspend/types";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
ChargeRequest,
|
|
11
|
+
ChargeResponse,
|
|
12
|
+
ErrorResponse,
|
|
13
|
+
PaymentMethod,
|
|
14
|
+
PaywallPaymentContext
|
|
15
|
+
} from "@agentspend/types";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// x402 imports – server-side only (HTTP calls to facilitator, no crypto deps)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
|
|
21
|
+
import type {
|
|
22
|
+
PaymentRequirements,
|
|
23
|
+
PaymentPayload,
|
|
24
|
+
VerifyResponse,
|
|
25
|
+
SettleResponse,
|
|
26
|
+
Network
|
|
27
|
+
} from "@x402/core/types";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Options
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface AgentSpendOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Base URL for the AgentSpend Platform API.
|
|
36
|
+
*
|
|
37
|
+
* If omitted, the SDK will use `process.env.AGENTSPEND_API_URL` when available,
|
|
38
|
+
* otherwise it falls back to the hosted default.
|
|
39
|
+
*/
|
|
40
|
+
platformApiBaseUrl?: string;
|
|
41
|
+
/** Service API key. Optional — crypto-only services don't need one. */
|
|
42
|
+
serviceApiKey?: string;
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
/** Crypto / x402 configuration. */
|
|
45
|
+
crypto?: {
|
|
46
|
+
/** Static payTo address for crypto-only services. */
|
|
47
|
+
receiverAddress?: string;
|
|
48
|
+
/** Chain identifier. Default: "eip155:8453" (Base). */
|
|
49
|
+
network?: string;
|
|
50
|
+
/** x402 facilitator URL. Default: "https://x402.org/facilitator". */
|
|
51
|
+
facilitatorUrl?: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ChargeOptions {
|
|
56
|
+
amount_cents: number;
|
|
57
|
+
currency?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
metadata?: Record<string, string>;
|
|
60
|
+
idempotency_key?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class AgentSpendChargeError extends Error {
|
|
64
|
+
statusCode: number;
|
|
65
|
+
details: unknown;
|
|
66
|
+
|
|
67
|
+
constructor(message: string, statusCode: number, details?: unknown) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.statusCode = statusCode;
|
|
70
|
+
this.details = details;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Context abstraction (Hono-compatible)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export interface HonoContextLike {
|
|
79
|
+
req: {
|
|
80
|
+
header(name: string): string | undefined;
|
|
81
|
+
json(): Promise<unknown>;
|
|
82
|
+
url: string;
|
|
83
|
+
method: string;
|
|
84
|
+
};
|
|
85
|
+
json(body: unknown, status?: number): unknown;
|
|
86
|
+
header(name: string, value: string): void;
|
|
87
|
+
set(key: string, value: unknown): void;
|
|
88
|
+
get(key: string): unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Paywall options
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface PaywallOptions {
|
|
96
|
+
currency?: string;
|
|
97
|
+
description?: string;
|
|
98
|
+
metadata?: (body: unknown) => Record<string, unknown>;
|
|
99
|
+
/** Dynamic pricing: derive amount from the parsed request body. */
|
|
100
|
+
amountFromRequest?: (body: unknown) => number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Payment context helper
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
const PAYMENT_CONTEXT_KEY = "payment";
|
|
108
|
+
|
|
109
|
+
export function getPaymentContext(c: HonoContextLike): PaywallPaymentContext | null {
|
|
110
|
+
const ctx = c.get(PAYMENT_CONTEXT_KEY);
|
|
111
|
+
return (ctx as PaywallPaymentContext) ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Public interface
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export interface AgentSpend {
|
|
119
|
+
charge(walletId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
120
|
+
paywall(
|
|
121
|
+
amountCents: number,
|
|
122
|
+
opts?: PaywallOptions
|
|
123
|
+
): (c: HonoContextLike, next: () => Promise<void>) => Promise<unknown>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Factory
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
131
|
+
// Validate: at least one of serviceApiKey or crypto must be provided
|
|
132
|
+
if (!options.serviceApiKey && !options.crypto) {
|
|
133
|
+
throw new AgentSpendChargeError(
|
|
134
|
+
"At least one of serviceApiKey or crypto config must be provided",
|
|
135
|
+
500
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
140
|
+
if (!fetchImpl) {
|
|
141
|
+
throw new AgentSpendChargeError("No fetch implementation available", 500);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const platformApiBaseUrl = resolvePlatformApiBaseUrl(options.platformApiBaseUrl);
|
|
145
|
+
|
|
146
|
+
// -------------------------------------------------------------------
|
|
147
|
+
// x402 singleton setup (Decision 9)
|
|
148
|
+
// Server-side: facilitator handles verify + settle over HTTP.
|
|
149
|
+
// No client-side EVM scheme needed — we delegate to the facilitator.
|
|
150
|
+
// -------------------------------------------------------------------
|
|
151
|
+
let facilitator: HTTPFacilitatorClient | null = null;
|
|
152
|
+
let resourceServer: x402ResourceServer | null = null;
|
|
153
|
+
const cryptoNetwork: Network = (options.crypto?.network ?? "eip155:8453") as Network;
|
|
154
|
+
|
|
155
|
+
if (options.crypto || options.serviceApiKey) {
|
|
156
|
+
const facilitatorUrl =
|
|
157
|
+
options.crypto?.facilitatorUrl ?? "https://x402.org/facilitator";
|
|
158
|
+
facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
|
|
159
|
+
resourceServer = new x402ResourceServer(facilitator);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// -------------------------------------------------------------------
|
|
163
|
+
// charge() — card-only, unchanged
|
|
164
|
+
// -------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
async function charge(walletIdInput: string, opts: ChargeOptions): Promise<ChargeResponse> {
|
|
167
|
+
if (!options.serviceApiKey) {
|
|
168
|
+
throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const walletId = toWalletId(walletIdInput);
|
|
172
|
+
if (!walletId) {
|
|
173
|
+
throw new AgentSpendChargeError("wallet_id must start with wal_", 400);
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
|
|
176
|
+
throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const payload: ChargeRequest = {
|
|
180
|
+
wallet_id: walletId,
|
|
181
|
+
amount_cents: opts.amount_cents,
|
|
182
|
+
currency: opts.currency ?? "usd",
|
|
183
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
184
|
+
...(opts.metadata ? { metadata: opts.metadata } : {}),
|
|
185
|
+
idempotency_key: opts.idempotency_key ?? bestEffortIdempotencyKey()
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/charge"), {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: {
|
|
191
|
+
authorization: `Bearer ${options.serviceApiKey}`,
|
|
192
|
+
"content-type": "application/json"
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(payload)
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const responseBody = (await response.json().catch(() => ({}))) as Partial<ChargeResponse> &
|
|
198
|
+
Partial<ErrorResponse> &
|
|
199
|
+
Record<string, unknown>;
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new AgentSpendChargeError(
|
|
202
|
+
typeof responseBody.error === "string" ? responseBody.error : "AgentSpend charge failed",
|
|
203
|
+
response.status,
|
|
204
|
+
responseBody
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return responseBody as ChargeResponse;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -------------------------------------------------------------------
|
|
212
|
+
// paywall() — unified card + crypto middleware
|
|
213
|
+
// -------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function paywall(amountCents: number, opts?: PaywallOptions) {
|
|
216
|
+
// Allow amountCents === 0 when amountFromRequest is provided (dynamic pricing)
|
|
217
|
+
if (!opts?.amountFromRequest) {
|
|
218
|
+
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
|
219
|
+
throw new AgentSpendChargeError("amountCents must be a positive integer", 500);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return async function paywallMiddleware(
|
|
224
|
+
c: HonoContextLike,
|
|
225
|
+
next: () => Promise<void>
|
|
226
|
+
): Promise<unknown> {
|
|
227
|
+
// Step 1: Parse body once (Decision 11)
|
|
228
|
+
const body: unknown = await c.req.json().catch(() => ({}));
|
|
229
|
+
|
|
230
|
+
// Step 2: Determine effective amount
|
|
231
|
+
let effectiveAmount = amountCents;
|
|
232
|
+
if (opts?.amountFromRequest) {
|
|
233
|
+
effectiveAmount = opts.amountFromRequest(body);
|
|
234
|
+
if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
|
|
235
|
+
return c.json({ error: "Could not determine payment amount from request" }, 400);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const currency = opts?.currency ?? "usd";
|
|
240
|
+
|
|
241
|
+
// Step 3: Check for x-payment header → crypto payment
|
|
242
|
+
const paymentHeader = c.req.header("x-payment");
|
|
243
|
+
if (paymentHeader) {
|
|
244
|
+
return handleCryptoPayment(c, next, paymentHeader, effectiveAmount, currency, body, opts);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Step 4: Check for x-wallet-id header or body.wallet_id → card payment
|
|
248
|
+
const walletIdFromHeader = c.req.header("x-wallet-id");
|
|
249
|
+
let walletId = walletIdFromHeader ? toWalletId(walletIdFromHeader) : null;
|
|
250
|
+
if (!walletId) {
|
|
251
|
+
const bodyWalletId =
|
|
252
|
+
typeof (body as { wallet_id?: unknown })?.wallet_id === "string"
|
|
253
|
+
? (body as { wallet_id: string }).wallet_id
|
|
254
|
+
: null;
|
|
255
|
+
walletId = toWalletId(bodyWalletId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (walletId) {
|
|
259
|
+
return handleCardPayment(c, next, walletId, effectiveAmount, currency, body, opts);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Step 5: Neither → return 402 with Payment-Required header (Decision 8)
|
|
263
|
+
return return402Response(c, effectiveAmount, currency);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// -------------------------------------------------------------------
|
|
268
|
+
// handleCardPayment — existing charge() flow
|
|
269
|
+
// -------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
async function handleCardPayment(
|
|
272
|
+
c: HonoContextLike,
|
|
273
|
+
next: () => Promise<void>,
|
|
274
|
+
walletId: string,
|
|
275
|
+
amountCents: number,
|
|
276
|
+
currency: string,
|
|
277
|
+
body: unknown,
|
|
278
|
+
opts?: PaywallOptions
|
|
279
|
+
): Promise<unknown> {
|
|
280
|
+
if (!options.serviceApiKey) {
|
|
281
|
+
return c.json({ error: "Card payments require serviceApiKey" }, 500);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const chargeResult = await charge(walletId, {
|
|
286
|
+
amount_cents: amountCents,
|
|
287
|
+
currency,
|
|
288
|
+
description: opts?.description,
|
|
289
|
+
metadata: opts?.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
|
|
290
|
+
idempotency_key:
|
|
291
|
+
c.req.header("x-request-id") ?? c.req.header("idempotency-key") ?? undefined
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const paymentContext: PaywallPaymentContext = {
|
|
295
|
+
method: "card",
|
|
296
|
+
amount_cents: amountCents,
|
|
297
|
+
currency,
|
|
298
|
+
wallet_id: walletId,
|
|
299
|
+
remaining_limit_cents: chargeResult.remaining_limit_cents
|
|
300
|
+
};
|
|
301
|
+
c.set(PAYMENT_CONTEXT_KEY, paymentContext);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error instanceof AgentSpendChargeError) {
|
|
304
|
+
if (error.statusCode === 402) {
|
|
305
|
+
return c.json({ error: "Payment required", details: error.details }, 402);
|
|
306
|
+
}
|
|
307
|
+
return c.json({ error: error.message, details: error.details }, error.statusCode);
|
|
308
|
+
}
|
|
309
|
+
return c.json({ error: "Unexpected paywall failure" }, 500);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await next();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// -------------------------------------------------------------------
|
|
316
|
+
// handleCryptoPayment — x402 verify + settle via facilitator
|
|
317
|
+
// -------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
async function handleCryptoPayment(
|
|
320
|
+
c: HonoContextLike,
|
|
321
|
+
next: () => Promise<void>,
|
|
322
|
+
paymentHeader: string,
|
|
323
|
+
amountCents: number,
|
|
324
|
+
currency: string,
|
|
325
|
+
_body: unknown,
|
|
326
|
+
_opts?: PaywallOptions
|
|
327
|
+
): Promise<unknown> {
|
|
328
|
+
if (!facilitator) {
|
|
329
|
+
return c.json({ error: "Crypto payments not configured" }, 500);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
// Decode the x-payment header (base64 JSON payment payload)
|
|
334
|
+
let paymentPayload: PaymentPayload;
|
|
335
|
+
try {
|
|
336
|
+
paymentPayload = JSON.parse(
|
|
337
|
+
Buffer.from(paymentHeader, "base64").toString("utf-8")
|
|
338
|
+
) as PaymentPayload;
|
|
339
|
+
} catch {
|
|
340
|
+
return c.json({ error: "Invalid payment payload encoding" }, 400);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Resolve the payTo address for verification context
|
|
344
|
+
const payTo = await resolvePayToAddress();
|
|
345
|
+
|
|
346
|
+
// Build the payment requirements that the payment should satisfy
|
|
347
|
+
const paymentRequirements: PaymentRequirements = {
|
|
348
|
+
scheme: "exact",
|
|
349
|
+
network: cryptoNetwork,
|
|
350
|
+
amount: String(amountCents),
|
|
351
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
352
|
+
payTo,
|
|
353
|
+
maxTimeoutSeconds: 300,
|
|
354
|
+
extra: {}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Verify payment via facilitator
|
|
358
|
+
const verifyResult: VerifyResponse = await facilitator.verify(
|
|
359
|
+
paymentPayload,
|
|
360
|
+
paymentRequirements
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (!verifyResult.isValid) {
|
|
364
|
+
return c.json(
|
|
365
|
+
{ error: "Payment verification failed", details: verifyResult.invalidReason },
|
|
366
|
+
402
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Settle payment via facilitator
|
|
371
|
+
const settleResult: SettleResponse = await facilitator.settle(
|
|
372
|
+
paymentPayload,
|
|
373
|
+
paymentRequirements
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (!settleResult.success) {
|
|
377
|
+
return c.json(
|
|
378
|
+
{ error: "Payment settlement failed", details: settleResult.errorReason },
|
|
379
|
+
402
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const paymentContext: PaywallPaymentContext = {
|
|
384
|
+
method: "crypto",
|
|
385
|
+
amount_cents: amountCents,
|
|
386
|
+
currency,
|
|
387
|
+
transaction_hash: settleResult.transaction,
|
|
388
|
+
payer_address: verifyResult.payer ?? undefined,
|
|
389
|
+
network: cryptoNetwork
|
|
390
|
+
};
|
|
391
|
+
c.set(PAYMENT_CONTEXT_KEY, paymentContext);
|
|
392
|
+
|
|
393
|
+
await next();
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error instanceof AgentSpendChargeError) {
|
|
396
|
+
return c.json({ error: error.message, details: error.details }, error.statusCode);
|
|
397
|
+
}
|
|
398
|
+
return c.json(
|
|
399
|
+
{ error: "Crypto payment processing failed", details: (error as Error).message },
|
|
400
|
+
500
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// -------------------------------------------------------------------
|
|
406
|
+
// return402Response — x402 Payment-Required format (Decision 8)
|
|
407
|
+
// -------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
async function return402Response(
|
|
410
|
+
c: HonoContextLike,
|
|
411
|
+
amountCents: number,
|
|
412
|
+
currency: string
|
|
413
|
+
): Promise<unknown> {
|
|
414
|
+
try {
|
|
415
|
+
const payTo = await resolvePayToAddress();
|
|
416
|
+
|
|
417
|
+
const paymentRequirements: PaymentRequirements = {
|
|
418
|
+
scheme: "exact",
|
|
419
|
+
network: cryptoNetwork,
|
|
420
|
+
amount: String(amountCents),
|
|
421
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
422
|
+
payTo,
|
|
423
|
+
maxTimeoutSeconds: 300,
|
|
424
|
+
extra: {}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Build x402 v2 PaymentRequired response
|
|
428
|
+
const paymentRequired = {
|
|
429
|
+
x402Version: 2,
|
|
430
|
+
error: "Payment required",
|
|
431
|
+
resource: {
|
|
432
|
+
url: c.req.url,
|
|
433
|
+
description: `Payment of ${amountCents} cents`,
|
|
434
|
+
mimeType: "application/json"
|
|
435
|
+
},
|
|
436
|
+
accepts: [paymentRequirements]
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Set Payment-Required header (base64 encoded)
|
|
440
|
+
const headerValue = Buffer.from(
|
|
441
|
+
JSON.stringify(paymentRequired)
|
|
442
|
+
).toString("base64");
|
|
443
|
+
c.header("Payment-Required", headerValue);
|
|
444
|
+
|
|
445
|
+
return c.json({ error: "Payment required", amount_cents: amountCents, currency }, 402);
|
|
446
|
+
} catch {
|
|
447
|
+
// If we can't resolve a payTo address, return a plain 402
|
|
448
|
+
return c.json(
|
|
449
|
+
{ error: "Payment required", amount_cents: amountCents, currency },
|
|
450
|
+
402
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// -------------------------------------------------------------------
|
|
456
|
+
// resolvePayToAddress — static wallet or Stripe Machine Payments
|
|
457
|
+
// -------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
async function resolvePayToAddress(): Promise<string> {
|
|
460
|
+
// Static address for crypto-only services
|
|
461
|
+
if (options.crypto?.receiverAddress) {
|
|
462
|
+
return options.crypto.receiverAddress;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Stripe Connect service → get deposit address from platform
|
|
466
|
+
if (options.serviceApiKey) {
|
|
467
|
+
const response = await fetchImpl(
|
|
468
|
+
joinUrl(platformApiBaseUrl, "/v1/crypto/deposit-address"),
|
|
469
|
+
{
|
|
470
|
+
method: "POST",
|
|
471
|
+
headers: {
|
|
472
|
+
authorization: `Bearer ${options.serviceApiKey}`,
|
|
473
|
+
"content-type": "application/json"
|
|
474
|
+
},
|
|
475
|
+
body: JSON.stringify({ amount_cents: 0, currency: "usd" })
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
throw new AgentSpendChargeError("Failed to resolve crypto deposit address", 502);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const data = (await response.json()) as { deposit_address?: string };
|
|
484
|
+
if (!data.deposit_address) {
|
|
485
|
+
throw new AgentSpendChargeError("No deposit address returned", 502);
|
|
486
|
+
}
|
|
487
|
+
return data.deposit_address;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
throw new AgentSpendChargeError("No crypto payTo address available", 500);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// -------------------------------------------------------------------
|
|
494
|
+
// Return public interface
|
|
495
|
+
// -------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
charge,
|
|
499
|
+
paywall
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Helpers (unchanged from original)
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
function toWalletId(input: unknown): string | null {
|
|
508
|
+
if (typeof input !== "string") {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
const trimmed = input.trim();
|
|
512
|
+
if (!trimmed.startsWith("wal_")) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
return trimmed;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function joinUrl(base: string, path: string): string {
|
|
519
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
520
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
521
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function bestEffortIdempotencyKey(): string {
|
|
525
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
526
|
+
if (uuid) {
|
|
527
|
+
return uuid;
|
|
528
|
+
}
|
|
529
|
+
return `auto_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function toStringMetadata(input: unknown): Record<string, string> {
|
|
533
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result: Record<string, string> = {};
|
|
538
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
539
|
+
if (typeof value === "string") {
|
|
540
|
+
result[key] = value;
|
|
541
|
+
} else if (typeof value === "number" && Number.isFinite(value)) {
|
|
542
|
+
result[key] = String(value);
|
|
543
|
+
} else if (typeof value === "boolean") {
|
|
544
|
+
result[key] = value ? "true" : "false";
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const DEFAULT_PLATFORM_API_BASE_URL = "https://api.agentspend.co";
|
|
551
|
+
|
|
552
|
+
function resolvePlatformApiBaseUrl(explicitBaseUrl: string | undefined): string {
|
|
553
|
+
if (explicitBaseUrl && explicitBaseUrl.trim().length > 0) {
|
|
554
|
+
return explicitBaseUrl.trim();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const envValue =
|
|
558
|
+
typeof process !== "undefined" && process.env ? process.env.AGENTSPEND_API_URL : undefined;
|
|
559
|
+
|
|
560
|
+
if (typeof envValue === "string" && envValue.trim().length > 0) {
|
|
561
|
+
return envValue.trim();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return DEFAULT_PLATFORM_API_BASE_URL;
|
|
565
|
+
}
|
package/tsconfig.json
ADDED
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,wBAAwB,EAAE,MAAM,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAMD,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;gBAEL,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAKnE;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE;QACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACzC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;KAC1B,CAAC;IACF,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC/C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACvE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1H;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAgGvE"}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|