@agentspend/sdk 0.2.0 → 0.3.2
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/README.md +86 -0
- package/dist/index.d.ts +42 -7
- package/dist/index.js +45 -34
- package/package.json +1 -2
- package/src/index.ts +99 -64
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @agentspend/sdk
|
|
2
|
+
|
|
3
|
+
SDK for services to accept AI agent payments — cards (Stripe) and crypto (x402/USDC on Base).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agentspend/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Paywall middleware (Hono)
|
|
12
|
+
|
|
13
|
+
Add a paywall to any endpoint. Accepts both card and crypto payments automatically.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createAgentSpend, getPaymentContext } from "@agentspend/sdk";
|
|
17
|
+
|
|
18
|
+
const spend = createAgentSpend({
|
|
19
|
+
serviceApiKey: process.env.AGENTSPEND_SERVICE_API_KEY,
|
|
20
|
+
crypto: {
|
|
21
|
+
receiverAddress: "0x...", // your USDC address on Base
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.post("/api/generate", spend.paywall({ amount: 100 }), async (c) => {
|
|
26
|
+
const payment = getPaymentContext(c);
|
|
27
|
+
// payment.method === "card" | "crypto"
|
|
28
|
+
// payment.amount_cents === 100
|
|
29
|
+
return c.json({ result: "..." });
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Dynamic pricing
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// Read amount from request body field
|
|
37
|
+
spend.paywall({ amount: "amount_cents" });
|
|
38
|
+
|
|
39
|
+
// Custom pricing function
|
|
40
|
+
spend.paywall({ amount: (body) => calculatePrice(body) });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Direct charge (card only)
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const result = await spend.charge("card_abc123", {
|
|
47
|
+
amount_cents: 500,
|
|
48
|
+
description: "API call",
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How agents pay
|
|
53
|
+
|
|
54
|
+
**Card:** Agent sends `x-card-id: card_xxx` header or `card_id` in the request body.
|
|
55
|
+
|
|
56
|
+
**Crypto:** Agent sends `x-payment` header with a signed x402 payment payload.
|
|
57
|
+
|
|
58
|
+
If neither is provided, the service returns `402 Payment Required` with x402 payment requirements.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const spend = createAgentSpend({
|
|
64
|
+
// Stripe card payments (get key from service onboarding)
|
|
65
|
+
serviceApiKey: "sk_...",
|
|
66
|
+
|
|
67
|
+
// Crypto payments (optional)
|
|
68
|
+
crypto: {
|
|
69
|
+
receiverAddress: "0x...", // static payTo address
|
|
70
|
+
network: "eip155:8453", // default: Base
|
|
71
|
+
facilitatorUrl: "https://...", // default: x402.org
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Override platform API URL (optional)
|
|
75
|
+
platformApiBaseUrl: "https://api.agentspend.co",
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
At least one of `serviceApiKey` or `crypto` must be provided.
|
|
80
|
+
|
|
81
|
+
## Environment variables
|
|
82
|
+
|
|
83
|
+
| Variable | Description |
|
|
84
|
+
|----------|-------------|
|
|
85
|
+
| `AGENTSPEND_API_URL` | Platform API base URL (default: `https://api.agentspend.co`) |
|
|
86
|
+
| `AGENTSPEND_SERVICE_API_KEY` | Service API key (from service onboarding) |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export interface ChargeRequest {
|
|
2
|
+
card_id: string;
|
|
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
|
+
card_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
|
+
}
|
|
19
|
+
export interface ErrorResponse {
|
|
20
|
+
error: string;
|
|
21
|
+
}
|
|
22
|
+
export type PaymentMethod = "card" | "crypto";
|
|
23
|
+
export interface PaywallPaymentContext {
|
|
24
|
+
method: PaymentMethod;
|
|
25
|
+
amount_cents: number;
|
|
26
|
+
currency: string;
|
|
27
|
+
card_id?: string;
|
|
28
|
+
remaining_limit_cents?: number;
|
|
29
|
+
transaction_hash?: string;
|
|
30
|
+
payer_address?: string;
|
|
31
|
+
network?: string;
|
|
32
|
+
}
|
|
3
33
|
export interface AgentSpendOptions {
|
|
4
34
|
/**
|
|
5
35
|
* Base URL for the AgentSpend Platform API.
|
|
@@ -40,21 +70,26 @@ export interface HonoContextLike {
|
|
|
40
70
|
url: string;
|
|
41
71
|
method: string;
|
|
42
72
|
};
|
|
43
|
-
json(body: unknown, status?: number):
|
|
73
|
+
json(body: unknown, status?: number): Response;
|
|
44
74
|
header(name: string, value: string): void;
|
|
45
75
|
set(key: string, value: unknown): void;
|
|
46
76
|
get(key: string): unknown;
|
|
47
77
|
}
|
|
48
78
|
export interface PaywallOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Amount in cents.
|
|
81
|
+
* - number: fixed price (e.g. 500 = $5.00)
|
|
82
|
+
* - string: body field name to read amount from (e.g. "amount_cents")
|
|
83
|
+
* - function: custom dynamic pricing (body: unknown) => number
|
|
84
|
+
*/
|
|
85
|
+
amount: number | string | ((body: unknown) => number);
|
|
49
86
|
currency?: string;
|
|
50
87
|
description?: string;
|
|
51
88
|
metadata?: (body: unknown) => Record<string, unknown>;
|
|
52
|
-
/** Dynamic pricing: derive amount from the parsed request body. */
|
|
53
|
-
amountFromRequest?: (body: unknown) => number;
|
|
54
89
|
}
|
|
55
90
|
export declare function getPaymentContext(c: HonoContextLike): PaywallPaymentContext | null;
|
|
56
91
|
export interface AgentSpend {
|
|
57
|
-
charge(
|
|
58
|
-
paywall(
|
|
92
|
+
charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
93
|
+
paywall(opts: PaywallOptions): (c: HonoContextLike, next: () => Promise<void>) => Promise<Response | void>;
|
|
59
94
|
}
|
|
60
95
|
export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Types (inlined from @agentspend/types to avoid cross-repo publish)
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.AgentSpendChargeError = void 0;
|
|
4
7
|
exports.getPaymentContext = getPaymentContext;
|
|
@@ -54,19 +57,19 @@ function createAgentSpend(options) {
|
|
|
54
57
|
// -------------------------------------------------------------------
|
|
55
58
|
// charge() — card-only, unchanged
|
|
56
59
|
// -------------------------------------------------------------------
|
|
57
|
-
async function charge(
|
|
60
|
+
async function charge(cardIdInput, opts) {
|
|
58
61
|
if (!options.serviceApiKey) {
|
|
59
62
|
throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
|
|
60
63
|
}
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
throw new AgentSpendChargeError("
|
|
64
|
+
const cardId = toCardId(cardIdInput);
|
|
65
|
+
if (!cardId) {
|
|
66
|
+
throw new AgentSpendChargeError("card_id must start with card_", 400);
|
|
64
67
|
}
|
|
65
68
|
if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
|
|
66
69
|
throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
|
|
67
70
|
}
|
|
68
71
|
const payload = {
|
|
69
|
-
|
|
72
|
+
card_id: cardId,
|
|
70
73
|
amount_cents: opts.amount_cents,
|
|
71
74
|
currency: opts.currency ?? "usd",
|
|
72
75
|
...(opts.description ? { description: opts.description } : {}),
|
|
@@ -90,41 +93,49 @@ function createAgentSpend(options) {
|
|
|
90
93
|
// -------------------------------------------------------------------
|
|
91
94
|
// paywall() — unified card + crypto middleware
|
|
92
95
|
// -------------------------------------------------------------------
|
|
93
|
-
function paywall(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
function paywall(opts) {
|
|
97
|
+
const { amount } = opts;
|
|
98
|
+
// Validate fixed-price amount at creation time
|
|
99
|
+
if (typeof amount === "number") {
|
|
100
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
101
|
+
throw new AgentSpendChargeError("amount must be a positive integer", 500);
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
return async function paywallMiddleware(c, next) {
|
|
101
105
|
// Step 1: Parse body once (Decision 11)
|
|
102
106
|
const body = await c.req.json().catch(() => ({}));
|
|
103
107
|
// Step 2: Determine effective amount
|
|
104
|
-
let effectiveAmount
|
|
105
|
-
if (
|
|
106
|
-
effectiveAmount =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
let effectiveAmount;
|
|
109
|
+
if (typeof amount === "number") {
|
|
110
|
+
effectiveAmount = amount;
|
|
111
|
+
}
|
|
112
|
+
else if (typeof amount === "string") {
|
|
113
|
+
const raw = body?.[amount];
|
|
114
|
+
effectiveAmount = typeof raw === "number" ? raw : 0;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
effectiveAmount = amount(body);
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
|
|
120
|
+
return c.json({ error: "Could not determine payment amount from request" }, 400);
|
|
110
121
|
}
|
|
111
|
-
const currency = opts
|
|
122
|
+
const currency = opts.currency ?? "usd";
|
|
112
123
|
// Step 3: Check for x-payment header → crypto payment
|
|
113
124
|
const paymentHeader = c.req.header("x-payment");
|
|
114
125
|
if (paymentHeader) {
|
|
115
126
|
return handleCryptoPayment(c, next, paymentHeader, effectiveAmount, currency, body, opts);
|
|
116
127
|
}
|
|
117
|
-
// Step 4: Check for x-
|
|
118
|
-
const
|
|
119
|
-
let
|
|
120
|
-
if (!
|
|
121
|
-
const
|
|
122
|
-
? body.
|
|
128
|
+
// Step 4: Check for x-card-id header or body.card_id → card payment
|
|
129
|
+
const cardIdFromHeader = c.req.header("x-card-id");
|
|
130
|
+
let cardId = cardIdFromHeader ? toCardId(cardIdFromHeader) : null;
|
|
131
|
+
if (!cardId) {
|
|
132
|
+
const bodyCardId = typeof body?.card_id === "string"
|
|
133
|
+
? body.card_id
|
|
123
134
|
: null;
|
|
124
|
-
|
|
135
|
+
cardId = toCardId(bodyCardId);
|
|
125
136
|
}
|
|
126
|
-
if (
|
|
127
|
-
return handleCardPayment(c, next,
|
|
137
|
+
if (cardId) {
|
|
138
|
+
return handleCardPayment(c, next, cardId, effectiveAmount, currency, body, opts);
|
|
128
139
|
}
|
|
129
140
|
// Step 5: Neither → return 402 with Payment-Required header (Decision 8)
|
|
130
141
|
return return402Response(c, effectiveAmount, currency);
|
|
@@ -133,23 +144,23 @@ function createAgentSpend(options) {
|
|
|
133
144
|
// -------------------------------------------------------------------
|
|
134
145
|
// handleCardPayment — existing charge() flow
|
|
135
146
|
// -------------------------------------------------------------------
|
|
136
|
-
async function handleCardPayment(c, next,
|
|
147
|
+
async function handleCardPayment(c, next, cardId, amountCents, currency, body, opts) {
|
|
137
148
|
if (!options.serviceApiKey) {
|
|
138
149
|
return c.json({ error: "Card payments require serviceApiKey" }, 500);
|
|
139
150
|
}
|
|
140
151
|
try {
|
|
141
|
-
const chargeResult = await charge(
|
|
152
|
+
const chargeResult = await charge(cardId, {
|
|
142
153
|
amount_cents: amountCents,
|
|
143
154
|
currency,
|
|
144
|
-
description: opts
|
|
145
|
-
metadata: opts
|
|
155
|
+
description: opts.description,
|
|
156
|
+
metadata: opts.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
|
|
146
157
|
idempotency_key: c.req.header("x-request-id") ?? c.req.header("idempotency-key") ?? undefined
|
|
147
158
|
});
|
|
148
159
|
const paymentContext = {
|
|
149
160
|
method: "card",
|
|
150
161
|
amount_cents: amountCents,
|
|
151
162
|
currency,
|
|
152
|
-
|
|
163
|
+
card_id: cardId,
|
|
153
164
|
remaining_limit_cents: chargeResult.remaining_limit_cents
|
|
154
165
|
};
|
|
155
166
|
c.set(PAYMENT_CONTEXT_KEY, paymentContext);
|
|
@@ -258,7 +269,7 @@ function createAgentSpend(options) {
|
|
|
258
269
|
}
|
|
259
270
|
}
|
|
260
271
|
// -------------------------------------------------------------------
|
|
261
|
-
// resolvePayToAddress — static
|
|
272
|
+
// resolvePayToAddress — static address or Stripe Machine Payments
|
|
262
273
|
// -------------------------------------------------------------------
|
|
263
274
|
async function resolvePayToAddress() {
|
|
264
275
|
// Static address for crypto-only services
|
|
@@ -297,12 +308,12 @@ function createAgentSpend(options) {
|
|
|
297
308
|
// ---------------------------------------------------------------------------
|
|
298
309
|
// Helpers (unchanged from original)
|
|
299
310
|
// ---------------------------------------------------------------------------
|
|
300
|
-
function
|
|
311
|
+
function toCardId(input) {
|
|
301
312
|
if (typeof input !== "string") {
|
|
302
313
|
return null;
|
|
303
314
|
}
|
|
304
315
|
const trimmed = input.trim();
|
|
305
|
-
if (!trimmed.startsWith("
|
|
316
|
+
if (!trimmed.startsWith("card_")) {
|
|
306
317
|
return null;
|
|
307
318
|
}
|
|
308
319
|
return trimmed;
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentspend/sdk",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@agentspend/types": "^0.2.0",
|
|
11
10
|
"@x402/core": "^2.3.1"
|
|
12
11
|
},
|
|
13
12
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Types (inlined from @agentspend/types to avoid cross-repo publish)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface ChargeRequest {
|
|
6
|
+
card_id: string;
|
|
7
|
+
amount_cents: number;
|
|
8
|
+
currency?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
metadata?: Record<string, string>;
|
|
11
|
+
idempotency_key?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChargeResponse {
|
|
15
|
+
charged: true;
|
|
16
|
+
card_id: string;
|
|
17
|
+
amount_cents: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
remaining_limit_cents: number;
|
|
20
|
+
stripe_payment_intent_id: string;
|
|
21
|
+
stripe_charge_id: string;
|
|
22
|
+
charge_attempt_id: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ErrorResponse {
|
|
26
|
+
error: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PaymentMethod = "card" | "crypto";
|
|
30
|
+
|
|
31
|
+
export interface PaywallPaymentContext {
|
|
32
|
+
method: PaymentMethod;
|
|
33
|
+
amount_cents: number;
|
|
34
|
+
currency: string;
|
|
35
|
+
card_id?: string;
|
|
36
|
+
remaining_limit_cents?: number;
|
|
37
|
+
transaction_hash?: string;
|
|
38
|
+
payer_address?: string;
|
|
39
|
+
network?: string;
|
|
40
|
+
}
|
|
16
41
|
|
|
17
42
|
// ---------------------------------------------------------------------------
|
|
18
43
|
// x402 imports – server-side only (HTTP calls to facilitator, no crypto deps)
|
|
@@ -82,7 +107,7 @@ export interface HonoContextLike {
|
|
|
82
107
|
url: string;
|
|
83
108
|
method: string;
|
|
84
109
|
};
|
|
85
|
-
json(body: unknown, status?: number):
|
|
110
|
+
json(body: unknown, status?: number): Response;
|
|
86
111
|
header(name: string, value: string): void;
|
|
87
112
|
set(key: string, value: unknown): void;
|
|
88
113
|
get(key: string): unknown;
|
|
@@ -93,11 +118,16 @@ export interface HonoContextLike {
|
|
|
93
118
|
// ---------------------------------------------------------------------------
|
|
94
119
|
|
|
95
120
|
export interface PaywallOptions {
|
|
121
|
+
/**
|
|
122
|
+
* Amount in cents.
|
|
123
|
+
* - number: fixed price (e.g. 500 = $5.00)
|
|
124
|
+
* - string: body field name to read amount from (e.g. "amount_cents")
|
|
125
|
+
* - function: custom dynamic pricing (body: unknown) => number
|
|
126
|
+
*/
|
|
127
|
+
amount: number | string | ((body: unknown) => number);
|
|
96
128
|
currency?: string;
|
|
97
129
|
description?: string;
|
|
98
130
|
metadata?: (body: unknown) => Record<string, unknown>;
|
|
99
|
-
/** Dynamic pricing: derive amount from the parsed request body. */
|
|
100
|
-
amountFromRequest?: (body: unknown) => number;
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
// ---------------------------------------------------------------------------
|
|
@@ -116,11 +146,8 @@ export function getPaymentContext(c: HonoContextLike): PaywallPaymentContext | n
|
|
|
116
146
|
// ---------------------------------------------------------------------------
|
|
117
147
|
|
|
118
148
|
export interface AgentSpend {
|
|
119
|
-
charge(
|
|
120
|
-
paywall(
|
|
121
|
-
amountCents: number,
|
|
122
|
-
opts?: PaywallOptions
|
|
123
|
-
): (c: HonoContextLike, next: () => Promise<void>) => Promise<unknown>;
|
|
149
|
+
charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
150
|
+
paywall(opts: PaywallOptions): (c: HonoContextLike, next: () => Promise<void>) => Promise<Response | void>;
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
// ---------------------------------------------------------------------------
|
|
@@ -163,21 +190,21 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
163
190
|
// charge() — card-only, unchanged
|
|
164
191
|
// -------------------------------------------------------------------
|
|
165
192
|
|
|
166
|
-
async function charge(
|
|
193
|
+
async function charge(cardIdInput: string, opts: ChargeOptions): Promise<ChargeResponse> {
|
|
167
194
|
if (!options.serviceApiKey) {
|
|
168
195
|
throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
|
|
169
196
|
}
|
|
170
197
|
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
173
|
-
throw new AgentSpendChargeError("
|
|
198
|
+
const cardId = toCardId(cardIdInput);
|
|
199
|
+
if (!cardId) {
|
|
200
|
+
throw new AgentSpendChargeError("card_id must start with card_", 400);
|
|
174
201
|
}
|
|
175
202
|
if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
|
|
176
203
|
throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
|
|
177
204
|
}
|
|
178
205
|
|
|
179
206
|
const payload: ChargeRequest = {
|
|
180
|
-
|
|
207
|
+
card_id: cardId,
|
|
181
208
|
amount_cents: opts.amount_cents,
|
|
182
209
|
currency: opts.currency ?? "usd",
|
|
183
210
|
...(opts.description ? { description: opts.description } : {}),
|
|
@@ -212,31 +239,39 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
212
239
|
// paywall() — unified card + crypto middleware
|
|
213
240
|
// -------------------------------------------------------------------
|
|
214
241
|
|
|
215
|
-
function paywall(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
function paywall(opts: PaywallOptions) {
|
|
243
|
+
const { amount } = opts;
|
|
244
|
+
|
|
245
|
+
// Validate fixed-price amount at creation time
|
|
246
|
+
if (typeof amount === "number") {
|
|
247
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
248
|
+
throw new AgentSpendChargeError("amount must be a positive integer", 500);
|
|
220
249
|
}
|
|
221
250
|
}
|
|
222
251
|
|
|
223
252
|
return async function paywallMiddleware(
|
|
224
253
|
c: HonoContextLike,
|
|
225
254
|
next: () => Promise<void>
|
|
226
|
-
): Promise<
|
|
255
|
+
): Promise<Response | void> {
|
|
227
256
|
// Step 1: Parse body once (Decision 11)
|
|
228
257
|
const body: unknown = await c.req.json().catch(() => ({}));
|
|
229
258
|
|
|
230
259
|
// Step 2: Determine effective amount
|
|
231
|
-
let effectiveAmount
|
|
232
|
-
if (
|
|
233
|
-
effectiveAmount =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
let effectiveAmount: number;
|
|
261
|
+
if (typeof amount === "number") {
|
|
262
|
+
effectiveAmount = amount;
|
|
263
|
+
} else if (typeof amount === "string") {
|
|
264
|
+
const raw = (body as Record<string, unknown>)?.[amount];
|
|
265
|
+
effectiveAmount = typeof raw === "number" ? raw : 0;
|
|
266
|
+
} else {
|
|
267
|
+
effectiveAmount = amount(body);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
|
|
271
|
+
return c.json({ error: "Could not determine payment amount from request" }, 400);
|
|
237
272
|
}
|
|
238
273
|
|
|
239
|
-
const currency = opts
|
|
274
|
+
const currency = opts.currency ?? "usd";
|
|
240
275
|
|
|
241
276
|
// Step 3: Check for x-payment header → crypto payment
|
|
242
277
|
const paymentHeader = c.req.header("x-payment");
|
|
@@ -244,19 +279,19 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
244
279
|
return handleCryptoPayment(c, next, paymentHeader, effectiveAmount, currency, body, opts);
|
|
245
280
|
}
|
|
246
281
|
|
|
247
|
-
// Step 4: Check for x-
|
|
248
|
-
const
|
|
249
|
-
let
|
|
250
|
-
if (!
|
|
251
|
-
const
|
|
252
|
-
typeof (body as {
|
|
253
|
-
? (body as {
|
|
282
|
+
// Step 4: Check for x-card-id header or body.card_id → card payment
|
|
283
|
+
const cardIdFromHeader = c.req.header("x-card-id");
|
|
284
|
+
let cardId = cardIdFromHeader ? toCardId(cardIdFromHeader) : null;
|
|
285
|
+
if (!cardId) {
|
|
286
|
+
const bodyCardId =
|
|
287
|
+
typeof (body as { card_id?: unknown })?.card_id === "string"
|
|
288
|
+
? (body as { card_id: string }).card_id
|
|
254
289
|
: null;
|
|
255
|
-
|
|
290
|
+
cardId = toCardId(bodyCardId);
|
|
256
291
|
}
|
|
257
292
|
|
|
258
|
-
if (
|
|
259
|
-
return handleCardPayment(c, next,
|
|
293
|
+
if (cardId) {
|
|
294
|
+
return handleCardPayment(c, next, cardId, effectiveAmount, currency, body, opts);
|
|
260
295
|
}
|
|
261
296
|
|
|
262
297
|
// Step 5: Neither → return 402 with Payment-Required header (Decision 8)
|
|
@@ -271,22 +306,22 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
271
306
|
async function handleCardPayment(
|
|
272
307
|
c: HonoContextLike,
|
|
273
308
|
next: () => Promise<void>,
|
|
274
|
-
|
|
309
|
+
cardId: string,
|
|
275
310
|
amountCents: number,
|
|
276
311
|
currency: string,
|
|
277
312
|
body: unknown,
|
|
278
|
-
opts
|
|
279
|
-
): Promise<
|
|
313
|
+
opts: PaywallOptions
|
|
314
|
+
): Promise<Response | void> {
|
|
280
315
|
if (!options.serviceApiKey) {
|
|
281
316
|
return c.json({ error: "Card payments require serviceApiKey" }, 500);
|
|
282
317
|
}
|
|
283
318
|
|
|
284
319
|
try {
|
|
285
|
-
const chargeResult = await charge(
|
|
320
|
+
const chargeResult = await charge(cardId, {
|
|
286
321
|
amount_cents: amountCents,
|
|
287
322
|
currency,
|
|
288
|
-
description: opts
|
|
289
|
-
metadata: opts
|
|
323
|
+
description: opts.description,
|
|
324
|
+
metadata: opts.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
|
|
290
325
|
idempotency_key:
|
|
291
326
|
c.req.header("x-request-id") ?? c.req.header("idempotency-key") ?? undefined
|
|
292
327
|
});
|
|
@@ -295,7 +330,7 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
295
330
|
method: "card",
|
|
296
331
|
amount_cents: amountCents,
|
|
297
332
|
currency,
|
|
298
|
-
|
|
333
|
+
card_id: cardId,
|
|
299
334
|
remaining_limit_cents: chargeResult.remaining_limit_cents
|
|
300
335
|
};
|
|
301
336
|
c.set(PAYMENT_CONTEXT_KEY, paymentContext);
|
|
@@ -323,8 +358,8 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
323
358
|
amountCents: number,
|
|
324
359
|
currency: string,
|
|
325
360
|
_body: unknown,
|
|
326
|
-
_opts
|
|
327
|
-
): Promise<
|
|
361
|
+
_opts: PaywallOptions
|
|
362
|
+
): Promise<Response | void> {
|
|
328
363
|
if (!facilitator) {
|
|
329
364
|
return c.json({ error: "Crypto payments not configured" }, 500);
|
|
330
365
|
}
|
|
@@ -410,7 +445,7 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
410
445
|
c: HonoContextLike,
|
|
411
446
|
amountCents: number,
|
|
412
447
|
currency: string
|
|
413
|
-
): Promise<
|
|
448
|
+
): Promise<Response> {
|
|
414
449
|
try {
|
|
415
450
|
const payTo = await resolvePayToAddress();
|
|
416
451
|
|
|
@@ -453,7 +488,7 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
453
488
|
}
|
|
454
489
|
|
|
455
490
|
// -------------------------------------------------------------------
|
|
456
|
-
// resolvePayToAddress — static
|
|
491
|
+
// resolvePayToAddress — static address or Stripe Machine Payments
|
|
457
492
|
// -------------------------------------------------------------------
|
|
458
493
|
|
|
459
494
|
async function resolvePayToAddress(): Promise<string> {
|
|
@@ -504,12 +539,12 @@ export function createAgentSpend(options: AgentSpendOptions): AgentSpend {
|
|
|
504
539
|
// Helpers (unchanged from original)
|
|
505
540
|
// ---------------------------------------------------------------------------
|
|
506
541
|
|
|
507
|
-
function
|
|
542
|
+
function toCardId(input: unknown): string | null {
|
|
508
543
|
if (typeof input !== "string") {
|
|
509
544
|
return null;
|
|
510
545
|
}
|
|
511
546
|
const trimmed = input.trim();
|
|
512
|
-
if (!trimmed.startsWith("
|
|
547
|
+
if (!trimmed.startsWith("card_")) {
|
|
513
548
|
return null;
|
|
514
549
|
}
|
|
515
550
|
return trimmed;
|