@nevermined-io/payments 1.5.0 → 1.6.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/README.md CHANGED
@@ -19,16 +19,15 @@ The Nevermined Payments Library is a TypeScript SDK that allows AI Builders and
19
19
 
20
20
  The Payments Library enables:
21
21
 
22
- * Easy registration and discovery of AI agents and the payment plans required to access them. All agents registered in Nevermined expose their metadata in a generic way, making them searchable and discoverable for specific purposes.
23
- * Flexible definition of pricing options and how AI agents can be queried. This is achieved through payment plans (based on time or credits) and consumption costs (fixed per request or dynamic). All of this can be defined by the AI builder or agent during the registration process.
24
- * Subscribers (humans or other agents) to purchase credits that grant access to AI agent services. Payments can be made in crypto or fiat via Stripe integration. The protocol registers the payment and credits distribution settlement on-chain.
25
- * Agents or users with access credits to query other AI agents. Nevermined authorizes only users with sufficient balance and keeps track of their credit usage.
26
-
22
+ - Easy registration and discovery of AI agents and the payment plans required to access them. All agents registered in Nevermined expose their metadata in a generic way, making them searchable and discoverable for specific purposes.
23
+ - Flexible definition of pricing options and how AI agents can be queried. This is achieved through payment plans (based on time or credits) and consumption costs (fixed per request or dynamic). All of this can be defined by the AI builder or agent during the registration process.
24
+ - Subscribers (humans or other agents) to purchase credits that grant access to AI agent services. Payments can be made in crypto or fiat via Stripe integration. The protocol registers the payment and credits distribution settlement on-chain.
25
+ - Agents or users with access credits to query other AI agents. Nevermined authorizes only users with sufficient balance and keeps track of their credit usage.
27
26
 
28
27
  The library is designed for use in browser environments or as part of AI Agents:
29
28
 
30
- * In a browser, the library provides a simple way to connect to the Nevermined protocol, allowing users to query AI Agents or publish their own.
31
- * As part of an AI Agent, the library allows the agent to query other agents programmatically. Additionally, agents can use the library to expose their own services and make them available to other agents or humans.
29
+ - In a browser, the library provides a simple way to connect to the Nevermined protocol, allowing users to query AI Agents or publish their own.
30
+ - As part of an AI Agent, the library allows the agent to query other agents programmatically. Additionally, agents can use the library to expose their own services and make them available to other agents or humans.
32
31
 
33
32
  ## Quickstart
34
33
 
@@ -41,6 +40,7 @@ npm install @nevermined-io/payments@^1.1
41
40
  ```
42
41
 
43
42
  > Pin the major version (or rely on a committed lockfile) so SDK upgrades are explicit. Always commit `package-lock.json` / `pnpm-lock.yaml`.
43
+
44
44
  ## A2A Integration (Agents‑to‑Agents)
45
45
 
46
46
  Nevermined Payments integrates with the A2A protocol to authorize and charge per request between agents:
@@ -78,13 +78,13 @@ Add a payment extension under `capabilities.extensions` carrying Nevermined meta
78
78
  ```
79
79
 
80
80
  Important notes:
81
+
81
82
  - The `url` must match exactly the URL registered in Nevermined for the agent/plan.
82
83
  - The final streaming event must include `metadata.creditsUsed` with the consumed cost.
83
84
 
84
-
85
85
  ## Requirements
86
86
 
87
- To use the Nevermined Payments Library, you need to get your Nevermined API key. You can get yours freely from the [Nevermined App](https://nevermined.app).
87
+ To use the Nevermined Payments Library, you need to get your Nevermined API key. You can get yours freely from the [Nevermined App](https://nevermined.app).
88
88
 
89
89
  ### Environments
90
90
 
@@ -92,6 +92,15 @@ To use the Nevermined Payments Library, you need to get your Nevermined API key.
92
92
 
93
93
  Pick the environment that matches where your agent and plans are registered. The agent card `url` must belong to that environment.
94
94
 
95
+ ### Optional peer dependencies
96
+
97
+ The core SDK has no required runtime peers. Some integrations are gated behind optional peer dependencies you install only if you use them:
98
+
99
+ - **LangChain tool protection** (`@nevermined-io/payments/langchain`) — requires `@langchain/core` and `@langchain/langgraph >=1.0.0 <2`.
100
+ - **LangSmith observability** (`@nevermined-io/payments/langsmith`, and the spans the `requiresPayment` wrapper emits when `LANGSMITH_TRACING=true`) — requires **`langsmith >=0.7`**. The spans rely on `getCurrentRunTree` (exported from `langsmith/singletons/traceable`) and the merging `RunTree.metadata` setter, both of which are only reliably present from `0.7` onward. Install it yourself: `pnpm add langsmith`.
101
+
102
+ These are declared as optional peers, so npm/pnpm will not pull them in automatically and the SDK no-ops cleanly when they are absent.
103
+
95
104
  ### Initialize the Payments library in the Browser
96
105
 
97
106
  This is a browser only method. Here we have an example using react.
@@ -124,7 +133,7 @@ export default function Home() {
124
133
  ### Initialize the Payments library in an AI Agent
125
134
 
126
135
  ```typescript
127
- import { Payments } from "@nevermined-io/payments";
136
+ import { Payments } from '@nevermined-io/payments'
128
137
 
129
138
  const payments = Payments.getInstance({
130
139
  nvmApiKey,
@@ -138,19 +147,29 @@ Once the app is initialized we can create a payment plan:
138
147
 
139
148
  ```typescript
140
149
  const planMetadata: PlanMetadata = {
141
- name: 'E2E test Payments Plan',
142
- }
150
+ name: 'E2E test Payments Plan',
151
+ }
143
152
  const priceConfig = payments.plans.getERC20PriceConfig(20n, ERC20_ADDRESS, builderAddress)
144
153
  const creditsConfig = payments.plans.getFixedCreditsConfig(100n)
145
- const { planId } = await payments.plans.registerCreditsPlan(planMetadata, priceConfig, creditsConfig)
154
+ const { planId } = await payments.plans.registerCreditsPlan(
155
+ planMetadata,
156
+ priceConfig,
157
+ creditsConfig,
158
+ )
146
159
  ```
147
160
 
148
161
  Or register a plan limited by time:
149
162
 
150
163
  ```typescript
151
164
  const priceConfig = payments.plans.getERC20PriceConfig(50n, ERC20_ADDRESS, builderAddress)
152
- const expirablePlanConfig = payments.plans.getExpirableDurationConfig(payments.plans.ONE_DAY_DURATION) // 1 day
153
- const response = await payments.plans.registerTimePlan(planMetadata, priceConfig, expirablePlanConfig)
165
+ const expirablePlanConfig = payments.plans.getExpirableDurationConfig(
166
+ payments.plans.ONE_DAY_DURATION,
167
+ ) // 1 day
168
+ const response = await payments.plans.registerTimePlan(
169
+ planMetadata,
170
+ priceConfig,
171
+ expirablePlanConfig,
172
+ )
154
173
  ```
155
174
 
156
175
  You can also create trial plans (free plans that can only be purchased once):
@@ -166,7 +185,7 @@ const trialCreditsConfig = payments.plans.getFixedCreditsConfig(10n)
166
185
  const trialPlan = await payments.plans.registerCreditsTrialPlan(
167
186
  trialPlanMetadata,
168
187
  freePriceConfig,
169
- trialCreditsConfig
188
+ trialCreditsConfig,
170
189
  )
171
190
 
172
191
  // Time-based trial plan
@@ -174,7 +193,7 @@ const timeTrialConfig = payments.plans.getExpirableDurationConfig(ONE_DAY_DURATI
174
193
  const timeTrialPlan = await payments.plans.registerTimeTrialPlan(
175
194
  trialPlanMetadata,
176
195
  freePriceConfig,
177
- timeTrialConfig
196
+ timeTrialConfig,
178
197
  )
179
198
  ```
180
199
 
@@ -192,12 +211,13 @@ const agentMetadata = {
192
211
  // The API that the agent will expose
193
212
  const agentApi = {
194
213
  endpoints: [
195
- { 'POST': `https://example.com/api/v1/agents/:agentId/tasks` },
196
- { 'GET': `https://example.com/api/v1/agents/:agentId/tasks/invoke` }
197
- ]}
214
+ { POST: `https://example.com/api/v1/agents/:agentId/tasks` },
215
+ { GET: `https://example.com/api/v1/agents/:agentId/tasks/invoke` },
216
+ ],
217
+ }
198
218
 
199
219
  // This is the list of payment plans that the agent will accept
200
- const paymentPlans = [ creditsPlanId, expirablePlanId ]
220
+ const paymentPlans = [creditsPlanId, expirablePlanId]
201
221
  const result = await payments.agents.registerAgent(agentMetadata, agentApi, paymentPlans)
202
222
  ```
203
223
 
@@ -212,9 +232,7 @@ const agentMetadata = {
212
232
  }
213
233
 
214
234
  const agentApi = {
215
- endpoints: [
216
- { 'POST': 'https://example.com/api/v1/agents/:agentId/tasks' }
217
- ]
235
+ endpoints: [{ POST: 'https://example.com/api/v1/agents/:agentId/tasks' }],
218
236
  }
219
237
 
220
238
  const planMetadata = { name: 'Basic Plan' }
@@ -228,11 +246,12 @@ const { agentId, planId, txHash } = await payments.agents.registerAgentAndPlan(
228
246
  planMetadata,
229
247
  priceConfig,
230
248
  creditsConfig,
231
- 'time' // Optionally set access limit to 'time' or 'credits'
249
+ 'time', // Optionally set access limit to 'time' or 'credits'
232
250
  )
233
251
  ```
234
252
 
235
253
  The `accessLimit` parameter is optional. If not specified, it's automatically inferred:
254
+
236
255
  - `'credits'` if `creditsConfig.durationSecs === 0n` (non-expirable)
237
256
  - `'time'` if `creditsConfig.durationSecs > 0n` (expirable)
238
257
 
@@ -261,7 +280,7 @@ const agentHTTPOptions = {
261
280
  headers: {
262
281
  Accept: 'application/json',
263
282
  'Content-Type': 'application/json',
264
- 'payment-signature': accessToken
283
+ 'payment-signature': accessToken,
265
284
  },
266
285
  }
267
286
  const response = await fetch(new URL(agentURL), agentHTTPOptions)
@@ -281,7 +300,7 @@ MCP servers expose handlers (tools/resources/prompts) with their logical URLs, s
281
300
 
282
301
  ### Why Nevermined Payments?
283
302
 
284
- While MCP defines *what* an agent can do, it doesn't specify *who* can access it or *how* to charge for it. **Nevermined Payments** adds:
303
+ While MCP defines _what_ an agent can do, it doesn't specify _who_ can access it or _how_ to charge for it. **Nevermined Payments** adds:
285
304
 
286
305
  - **Authentication**: Validates user tokens via `Authorization` header
287
306
  - **Credit System**: Checks and deducts credits per request
@@ -292,77 +311,77 @@ While MCP defines *what* an agent can do, it doesn't specify *who* can access it
292
311
  #### 1. Initialize Nevermined Payments
293
312
 
294
313
  ```typescript
295
- import { Payments, EnvironmentName } from "@nevermined-io/payments";
314
+ import { Payments, EnvironmentName } from '@nevermined-io/payments'
296
315
 
297
316
  const payments = Payments.getInstance({
298
317
  nvmApiKey: process.env.NVM_API_KEY!,
299
318
  environment: process.env.NVM_ENVIRONMENT! as EnvironmentName,
300
- });
319
+ })
301
320
  ```
302
321
 
303
322
  #### 2. Register Protected Tools
304
323
 
305
324
  ```typescript
306
- import { z } from "zod";
325
+ import { z } from 'zod'
307
326
 
308
327
  const schema = z.object({
309
- city: z.string().describe("City name"),
310
- }) as any;
328
+ city: z.string().describe('City name'),
329
+ }) as any
311
330
 
312
331
  payments.mcp.registerTool(
313
- "weather.today",
332
+ 'weather.today',
314
333
  {
315
334
  title: "Today's Weather",
316
- description: "Get weather for a city",
335
+ description: 'Get weather for a city',
317
336
  inputSchema: schema,
318
337
  },
319
338
  async (args) => {
320
- const { city } = args as { city: string };
339
+ const { city } = args as { city: string }
321
340
  return {
322
- content: [{ type: "text", text: `Weather for ${city}: Sunny, 25C` }],
323
- };
341
+ content: [{ type: 'text', text: `Weather for ${city}: Sunny, 25C` }],
342
+ }
324
343
  },
325
- { credits: 1n }
326
- );
344
+ { credits: 1n },
345
+ )
327
346
  ```
328
347
 
329
348
  #### 3. Register Protected Resources
330
349
 
331
350
  ```typescript
332
351
  payments.mcp.registerResource(
333
- "Weather Data",
334
- "weather://today",
352
+ 'Weather Data',
353
+ 'weather://today',
335
354
  {
336
355
  title: "Today's Weather",
337
- description: "JSON weather data",
338
- mimeType: "application/json",
356
+ description: 'JSON weather data',
357
+ mimeType: 'application/json',
339
358
  },
340
359
  async (uri) => {
341
360
  return {
342
- contents: [{ uri: uri.href, text: "{...}", mimeType: "application/json" }],
343
- };
361
+ contents: [{ uri: uri.href, text: '{...}', mimeType: 'application/json' }],
362
+ }
344
363
  },
345
- { credits: 5n }
346
- );
364
+ { credits: 5n },
365
+ )
347
366
  ```
348
367
 
349
368
  #### 4. Register Protected Prompts
350
369
 
351
370
  ```typescript
352
371
  payments.mcp.registerPrompt(
353
- "weather.ensureCity",
372
+ 'weather.ensureCity',
354
373
  {
355
- title: "Ensure city",
356
- description: "Guide to call weather.today",
374
+ title: 'Ensure city',
375
+ description: 'Guide to call weather.today',
357
376
  argsSchema: schema,
358
377
  },
359
378
  (args) => {
360
379
  return {
361
- messages: [{ role: "user", content: { type: "text", text: "..." } }],
362
- };
380
+ messages: [{ role: 'user', content: { type: 'text', text: '...' } }],
381
+ }
363
382
  },
364
- { credits: (ctx) => ctx.result.length > 100 ? 2n : 1n } // Dynamic credits
365
- );
383
+ { credits: (ctx) => (ctx.result.length > 100 ? 2n : 1n) }, // Dynamic credits
384
+ )
366
385
  ```
367
386
 
368
387
  #### 5. Start the Server
@@ -371,9 +390,9 @@ payments.mcp.registerPrompt(
371
390
  const { info, stop } = await payments.mcp.start({
372
391
  port: 3002,
373
392
  agentId: process.env.NVM_AGENT_ID!,
374
- serverName: "weather-mcp",
375
- version: "0.1.0",
376
- });
393
+ serverName: 'weather-mcp',
394
+ version: '0.1.0',
395
+ })
377
396
  ```
378
397
 
379
398
  ### Credit Configuration
@@ -382,22 +401,32 @@ All registration functions support fixed or dynamic credits:
382
401
 
383
402
  ```typescript
384
403
  // Fixed credits
385
- { credits: 1n }
386
- { credits: 5n }
404
+ {
405
+ credits: 1n
406
+ }
407
+ {
408
+ credits: 5n
409
+ }
387
410
 
388
411
  // Dynamic credits based on input
389
- { credits: (ctx) => ctx.args.premium ? 5n : 1n }
412
+ {
413
+ credits: (ctx) => (ctx.args.premium ? 5n : 1n)
414
+ }
390
415
 
391
416
  // Dynamic credits based on result
392
- { credits: (ctx) => ctx.result.length > 1000 ? 3n : 1n }
417
+ {
418
+ credits: (ctx) => (ctx.result.length > 1000 ? 3n : 1n)
419
+ }
393
420
 
394
421
  // Tiered pricing
395
- { credits: (ctx) => {
396
- const size = JSON.stringify(ctx.result).length;
397
- if (size > 1000) return 5n;
398
- if (size > 500) return 3n;
399
- return 1n;
400
- }}
422
+ {
423
+ credits: (ctx) => {
424
+ const size = JSON.stringify(ctx.result).length
425
+ if (size > 1000) return 5n
426
+ if (size > 500) return 3n
427
+ return 1n
428
+ }
429
+ }
401
430
  ```
402
431
 
403
432
  ### Client Usage
@@ -405,55 +434,54 @@ All registration functions support fixed or dynamic credits:
405
434
  #### Get Access Token
406
435
 
407
436
  ```typescript
408
- import { Payments } from "@nevermined-io/payments";
437
+ import { Payments } from '@nevermined-io/payments'
409
438
 
410
439
  const payments = Payments.getInstance({
411
440
  nvmApiKey: process.env.NVM_API_KEY!,
412
- environment: "sandbox",
413
- });
441
+ environment: 'sandbox',
442
+ })
414
443
 
415
444
  const { accessToken } = await payments.x402.getX402AccessToken(
416
445
  process.env.NVM_PLAN_ID!,
417
- process.env.NVM_AGENT_ID!
418
- );
446
+ process.env.NVM_AGENT_ID!,
447
+ )
419
448
  ```
420
449
 
421
450
  #### Call Protected Tools
422
451
 
423
452
  ```typescript
424
- import { Client } from "@modelcontextprotocol/sdk/client";
425
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp";
453
+ import { Client } from '@modelcontextprotocol/sdk/client'
454
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'
426
455
 
427
- const transport = new StreamableHTTPClientTransport(
428
- new URL("http://localhost:3002/mcp"),
429
- { requestInit: { headers: { Authorization: `Bearer ${accessToken}` } } }
430
- );
456
+ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3002/mcp'), {
457
+ requestInit: { headers: { Authorization: `Bearer ${accessToken}` } },
458
+ })
431
459
 
432
- const client = new Client({ name: "my-client" });
433
- await client.connect(transport);
460
+ const client = new Client({ name: 'my-client' })
461
+ await client.connect(transport)
434
462
 
435
463
  const result = await client.callTool({
436
- name: "weather.today",
437
- arguments: { city: "London" },
438
- });
464
+ name: 'weather.today',
465
+ arguments: { city: 'London' },
466
+ })
439
467
  ```
440
468
 
441
469
  ### Endpoints
442
470
 
443
- | Endpoint | Description |
444
- |----------|-------------|
445
- | `POST /mcp` | MCP JSON-RPC requests |
446
- | `GET /mcp` | SSE stream for notifications |
447
- | `DELETE /mcp` | Session termination |
448
- | `GET /health` | Health check |
449
- | `GET /` | Server info |
471
+ | Endpoint | Description |
472
+ | ------------- | ---------------------------- |
473
+ | `POST /mcp` | MCP JSON-RPC requests |
474
+ | `GET /mcp` | SSE stream for notifications |
475
+ | `DELETE /mcp` | Session termination |
476
+ | `GET /health` | Health check |
477
+ | `GET /` | Server info |
450
478
 
451
479
  ### Error Codes
452
480
 
453
- | Code | Description |
454
- |------|-------------|
481
+ | Code | Description |
482
+ | -------- | ---------------------------------------------------------------- |
455
483
  | `-32003` | Authorization required / Payment required / Insufficient credits |
456
- | `-32002` | Server error |
484
+ | `-32002` | Server error |
457
485
 
458
486
  ### Notes
459
487
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * LangGraph ReAct agent helper that surfaces `PaymentRequiredError` intact.
3
+ *
4
+ * By default `createReactAgent` from `@langchain/langgraph/prebuilt` constructs a
5
+ * `ToolNode` with `handleToolErrors: true` — tool exceptions are caught and
6
+ * rendered into a `ToolMessage` for the LLM. That is convenient for
7
+ * prompt-engineered recovery, but it stringifies the exception and loses the
8
+ * `X402PaymentRequired` payload attached to {@link PaymentRequiredError}.
9
+ * Without that payload the caller cannot run the x402 discovery flow (probe →
10
+ * read scheme/network/plan id → acquire token → retry).
11
+ *
12
+ * {@link createPaidReactAgent} builds the same agent but with a `ToolNode`
13
+ * configured to **re-raise** exceptions (`handleToolErrors: false`), so
14
+ * `PaymentRequiredError` propagates all the way back to `agent.invoke()`'s
15
+ * caller with `.paymentRequired` populated.
16
+ *
17
+ * `@langchain/langgraph` is imported **lazily** inside the function so the
18
+ * existing `peerDependencies` story is unchanged — users who only use the
19
+ * `requiresPayment` wrapper do not need LangGraph installed. Install it
20
+ * yourself (`pnpm add @langchain/langgraph`) to use this helper.
21
+ *
22
+ * Unlike Python's synchronous `create_paid_react_agent`, this helper is
23
+ * **`async`** — that is a deliberate consequence of the lazy `import()` that
24
+ * keeps `@langchain/langgraph` an optional peer, not a parity break. `await` it.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { tool } from '@langchain/core/tools'
29
+ * import { ChatOpenAI } from '@langchain/openai'
30
+ * import { z } from 'zod'
31
+ * import {
32
+ * PaymentRequiredError,
33
+ * createPaidReactAgent,
34
+ * requiresPayment,
35
+ * lastSettlement,
36
+ * } from '@nevermined-io/payments/langchain'
37
+ *
38
+ * const getMarketInsight = tool(
39
+ * requiresPayment(
40
+ * (args) => `Market insight for ${args.topic} ...`,
41
+ * { payments, planId: PLAN_ID, credits: 1 },
42
+ * ),
43
+ * { name: 'get_market_insight', description: 'Paid market insight', schema: z.object({ topic: z.string() }) },
44
+ * )
45
+ *
46
+ * const agent = await createPaidReactAgent(
47
+ * new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }),
48
+ * [getMarketInsight],
49
+ * { prompt: '...' },
50
+ * )
51
+ *
52
+ * // Discovery: invoke without a token to learn what to pay for.
53
+ * try {
54
+ * await agent.invoke({ messages: [...] }, { configurable: {} })
55
+ * } catch (err) {
56
+ * if (err instanceof PaymentRequiredError) {
57
+ * const accept = err.paymentRequired?.accepts[0]
58
+ * // ... acquire token using accept.planId / accept.scheme / accept.network ...
59
+ * }
60
+ * }
61
+ *
62
+ * const result = await agent.invoke(
63
+ * { messages: [...] },
64
+ * { configurable: { payment_token: token } },
65
+ * )
66
+ * const receipt = lastSettlement()
67
+ * ```
68
+ */
69
+ /**
70
+ * Options forwarded to the underlying `createReactAgent` call.
71
+ *
72
+ * Everything except `tools` (which this helper builds from the `tools`
73
+ * argument as a `ToolNode` with `handleToolErrors: false`) and `llm` (which
74
+ * this helper maps from the `model` argument) is passed through verbatim —
75
+ * `prompt`, `stateSchema`, `checkpointer`, `responseFormat`, etc.
76
+ */
77
+ export type CreatePaidReactAgentOptions = Record<string, unknown>;
78
+ /**
79
+ * Build a LangGraph ReAct agent that lets `PaymentRequiredError` propagate.
80
+ *
81
+ * Wraps `createReactAgent` from `@langchain/langgraph/prebuilt` with a
82
+ * `ToolNode(tools, { handleToolErrors: false })`. The signature mirrors the
83
+ * Python `create_paid_react_agent(model, tools, **kwargs)` helper: `model` is
84
+ * mapped to the JS `llm` parameter and any extra `options` are forwarded.
85
+ *
86
+ * @param model - The chat model (mapped to `createReactAgent`'s `llm` argument).
87
+ * @param tools - The LangChain tools, typically functions wrapped with
88
+ * `requiresPayment` and registered via `tool(...)`.
89
+ * @param options - Forwarded verbatim to `createReactAgent` (`prompt`,
90
+ * `stateSchema`, `checkpointer`, …).
91
+ * @returns The compiled ReAct agent graph, ready to be invoked with
92
+ * `agent.invoke(...)`.
93
+ * @throws If `@langchain/langgraph` is not installed.
94
+ */
95
+ export declare function createPaidReactAgent(model: unknown, tools: readonly unknown[], options?: CreatePaidReactAgentOptions): Promise<unknown>;
96
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/x402/langchain/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,2BAA2B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEjE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,OAAO,CAAC,CAyClB"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * LangGraph ReAct agent helper that surfaces `PaymentRequiredError` intact.
3
+ *
4
+ * By default `createReactAgent` from `@langchain/langgraph/prebuilt` constructs a
5
+ * `ToolNode` with `handleToolErrors: true` — tool exceptions are caught and
6
+ * rendered into a `ToolMessage` for the LLM. That is convenient for
7
+ * prompt-engineered recovery, but it stringifies the exception and loses the
8
+ * `X402PaymentRequired` payload attached to {@link PaymentRequiredError}.
9
+ * Without that payload the caller cannot run the x402 discovery flow (probe →
10
+ * read scheme/network/plan id → acquire token → retry).
11
+ *
12
+ * {@link createPaidReactAgent} builds the same agent but with a `ToolNode`
13
+ * configured to **re-raise** exceptions (`handleToolErrors: false`), so
14
+ * `PaymentRequiredError` propagates all the way back to `agent.invoke()`'s
15
+ * caller with `.paymentRequired` populated.
16
+ *
17
+ * `@langchain/langgraph` is imported **lazily** inside the function so the
18
+ * existing `peerDependencies` story is unchanged — users who only use the
19
+ * `requiresPayment` wrapper do not need LangGraph installed. Install it
20
+ * yourself (`pnpm add @langchain/langgraph`) to use this helper.
21
+ *
22
+ * Unlike Python's synchronous `create_paid_react_agent`, this helper is
23
+ * **`async`** — that is a deliberate consequence of the lazy `import()` that
24
+ * keeps `@langchain/langgraph` an optional peer, not a parity break. `await` it.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { tool } from '@langchain/core/tools'
29
+ * import { ChatOpenAI } from '@langchain/openai'
30
+ * import { z } from 'zod'
31
+ * import {
32
+ * PaymentRequiredError,
33
+ * createPaidReactAgent,
34
+ * requiresPayment,
35
+ * lastSettlement,
36
+ * } from '@nevermined-io/payments/langchain'
37
+ *
38
+ * const getMarketInsight = tool(
39
+ * requiresPayment(
40
+ * (args) => `Market insight for ${args.topic} ...`,
41
+ * { payments, planId: PLAN_ID, credits: 1 },
42
+ * ),
43
+ * { name: 'get_market_insight', description: 'Paid market insight', schema: z.object({ topic: z.string() }) },
44
+ * )
45
+ *
46
+ * const agent = await createPaidReactAgent(
47
+ * new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }),
48
+ * [getMarketInsight],
49
+ * { prompt: '...' },
50
+ * )
51
+ *
52
+ * // Discovery: invoke without a token to learn what to pay for.
53
+ * try {
54
+ * await agent.invoke({ messages: [...] }, { configurable: {} })
55
+ * } catch (err) {
56
+ * if (err instanceof PaymentRequiredError) {
57
+ * const accept = err.paymentRequired?.accepts[0]
58
+ * // ... acquire token using accept.planId / accept.scheme / accept.network ...
59
+ * }
60
+ * }
61
+ *
62
+ * const result = await agent.invoke(
63
+ * { messages: [...] },
64
+ * { configurable: { payment_token: token } },
65
+ * )
66
+ * const receipt = lastSettlement()
67
+ * ```
68
+ */
69
+ /**
70
+ * Build a LangGraph ReAct agent that lets `PaymentRequiredError` propagate.
71
+ *
72
+ * Wraps `createReactAgent` from `@langchain/langgraph/prebuilt` with a
73
+ * `ToolNode(tools, { handleToolErrors: false })`. The signature mirrors the
74
+ * Python `create_paid_react_agent(model, tools, **kwargs)` helper: `model` is
75
+ * mapped to the JS `llm` parameter and any extra `options` are forwarded.
76
+ *
77
+ * @param model - The chat model (mapped to `createReactAgent`'s `llm` argument).
78
+ * @param tools - The LangChain tools, typically functions wrapped with
79
+ * `requiresPayment` and registered via `tool(...)`.
80
+ * @param options - Forwarded verbatim to `createReactAgent` (`prompt`,
81
+ * `stateSchema`, `checkpointer`, …).
82
+ * @returns The compiled ReAct agent graph, ready to be invoked with
83
+ * `agent.invoke(...)`.
84
+ * @throws If `@langchain/langgraph` is not installed.
85
+ */
86
+ export async function createPaidReactAgent(model, tools, options = {}) {
87
+ // `llm` and `tools` are owned by this helper — `tools` carries the
88
+ // handleToolErrors:false ToolNode that makes PaymentRequiredError propagate,
89
+ // and overriding either would silently defeat the whole point of the helper
90
+ // (a caller-supplied `tools` re-enables the default handleToolErrors:true and
91
+ // the X402 payload is stringified away). Reject them up front, mirroring how
92
+ // Python's positional `create_paid_react_agent(model, tools, **kwargs)` raises
93
+ // a TypeError if `llm`/`tools` are passed again via kwargs.
94
+ if ('llm' in options || 'tools' in options) {
95
+ throw new Error('createPaidReactAgent: `llm` and `tools` are set from the `model` and ' +
96
+ '`tools` arguments and must not be passed in `options` (they would ' +
97
+ 'override the handleToolErrors:false ToolNode and break x402 discovery).');
98
+ }
99
+ let prebuilt;
100
+ try {
101
+ prebuilt = await import('@langchain/langgraph/prebuilt');
102
+ }
103
+ catch (err) {
104
+ throw new Error('createPaidReactAgent requires @langchain/langgraph. ' +
105
+ `Install it with \`pnpm add @langchain/langgraph\`. (${err instanceof Error ? err.message : String(err)})`);
106
+ }
107
+ const { ToolNode, createReactAgent } = prebuilt;
108
+ // handleToolErrors: false re-raises tool exceptions instead of stringifying
109
+ // them into a ToolMessage, so PaymentRequiredError reaches agent.invoke()'s
110
+ // caller with its X402PaymentRequired payload intact.
111
+ const toolNode = new ToolNode(tools, { handleToolErrors: false });
112
+ // `createReactAgent` is the current prebuilt entry point in
113
+ // @langchain/langgraph@1.2.0. It is marked @deprecated in favour of
114
+ // `createAgent`, but that replacement lives in the separate `langchain`
115
+ // package (out of scope for this SDK's optional langgraph peer), so the
116
+ // prebuilt `createReactAgent` is the deliberate, only in-package choice here.
117
+ // Spread `...options` FIRST so the protected `llm`/`tools` keys (set last)
118
+ // always win, even though the guard above already forbids them in `options`.
119
+ return createReactAgent({ ...options, llm: model, tools: toolNode });
120
+ }
121
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/x402/langchain/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAYH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAc,EACd,KAAyB,EACzB,UAAuC,EAAE;IAEzC,mEAAmE;IACnE,6EAA6E;IAC7E,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,+EAA+E;IAC/E,4DAA4D;IAC5D,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,oEAAoE;YACpE,yEAAyE,CAC5E,CAAA;IACH,CAAC;IAED,IAAI,QAAwD,CAAA;IAC5D,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAA;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,sDAAsD;YACpD,uDACE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,GAAG,CACN,CAAA;IACH,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,QAAQ,CAAA;IAE/C,4EAA4E;IAC5E,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,KAAc,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1E,4DAA4D;IAC5D,oEAAoE;IACpE,wEAAwE;IACxE,wEAAwE;IACxE,8EAA8E;IAC9E,2EAA2E;IAC3E,6EAA6E;IAC7E,OAAO,gBAAgB,CAAC,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,KAAc,EAAE,KAAK,EAAE,QAAQ,EAAW,CAAC,CAAA;AACxF,CAAC","sourcesContent":["/**\n * LangGraph ReAct agent helper that surfaces `PaymentRequiredError` intact.\n *\n * By default `createReactAgent` from `@langchain/langgraph/prebuilt` constructs a\n * `ToolNode` with `handleToolErrors: true` — tool exceptions are caught and\n * rendered into a `ToolMessage` for the LLM. That is convenient for\n * prompt-engineered recovery, but it stringifies the exception and loses the\n * `X402PaymentRequired` payload attached to {@link PaymentRequiredError}.\n * Without that payload the caller cannot run the x402 discovery flow (probe →\n * read scheme/network/plan id → acquire token → retry).\n *\n * {@link createPaidReactAgent} builds the same agent but with a `ToolNode`\n * configured to **re-raise** exceptions (`handleToolErrors: false`), so\n * `PaymentRequiredError` propagates all the way back to `agent.invoke()`'s\n * caller with `.paymentRequired` populated.\n *\n * `@langchain/langgraph` is imported **lazily** inside the function so the\n * existing `peerDependencies` story is unchanged — users who only use the\n * `requiresPayment` wrapper do not need LangGraph installed. Install it\n * yourself (`pnpm add @langchain/langgraph`) to use this helper.\n *\n * Unlike Python's synchronous `create_paid_react_agent`, this helper is\n * **`async`** — that is a deliberate consequence of the lazy `import()` that\n * keeps `@langchain/langgraph` an optional peer, not a parity break. `await` it.\n *\n * @example\n * ```typescript\n * import { tool } from '@langchain/core/tools'\n * import { ChatOpenAI } from '@langchain/openai'\n * import { z } from 'zod'\n * import {\n * PaymentRequiredError,\n * createPaidReactAgent,\n * requiresPayment,\n * lastSettlement,\n * } from '@nevermined-io/payments/langchain'\n *\n * const getMarketInsight = tool(\n * requiresPayment(\n * (args) => `Market insight for ${args.topic} ...`,\n * { payments, planId: PLAN_ID, credits: 1 },\n * ),\n * { name: 'get_market_insight', description: 'Paid market insight', schema: z.object({ topic: z.string() }) },\n * )\n *\n * const agent = await createPaidReactAgent(\n * new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }),\n * [getMarketInsight],\n * { prompt: '...' },\n * )\n *\n * // Discovery: invoke without a token to learn what to pay for.\n * try {\n * await agent.invoke({ messages: [...] }, { configurable: {} })\n * } catch (err) {\n * if (err instanceof PaymentRequiredError) {\n * const accept = err.paymentRequired?.accepts[0]\n * // ... acquire token using accept.planId / accept.scheme / accept.network ...\n * }\n * }\n *\n * const result = await agent.invoke(\n * { messages: [...] },\n * { configurable: { payment_token: token } },\n * )\n * const receipt = lastSettlement()\n * ```\n */\n\n/**\n * Options forwarded to the underlying `createReactAgent` call.\n *\n * Everything except `tools` (which this helper builds from the `tools`\n * argument as a `ToolNode` with `handleToolErrors: false`) and `llm` (which\n * this helper maps from the `model` argument) is passed through verbatim —\n * `prompt`, `stateSchema`, `checkpointer`, `responseFormat`, etc.\n */\nexport type CreatePaidReactAgentOptions = Record<string, unknown>\n\n/**\n * Build a LangGraph ReAct agent that lets `PaymentRequiredError` propagate.\n *\n * Wraps `createReactAgent` from `@langchain/langgraph/prebuilt` with a\n * `ToolNode(tools, { handleToolErrors: false })`. The signature mirrors the\n * Python `create_paid_react_agent(model, tools, **kwargs)` helper: `model` is\n * mapped to the JS `llm` parameter and any extra `options` are forwarded.\n *\n * @param model - The chat model (mapped to `createReactAgent`'s `llm` argument).\n * @param tools - The LangChain tools, typically functions wrapped with\n * `requiresPayment` and registered via `tool(...)`.\n * @param options - Forwarded verbatim to `createReactAgent` (`prompt`,\n * `stateSchema`, `checkpointer`, …).\n * @returns The compiled ReAct agent graph, ready to be invoked with\n * `agent.invoke(...)`.\n * @throws If `@langchain/langgraph` is not installed.\n */\nexport async function createPaidReactAgent(\n model: unknown,\n tools: readonly unknown[],\n options: CreatePaidReactAgentOptions = {},\n): Promise<unknown> {\n // `llm` and `tools` are owned by this helper — `tools` carries the\n // handleToolErrors:false ToolNode that makes PaymentRequiredError propagate,\n // and overriding either would silently defeat the whole point of the helper\n // (a caller-supplied `tools` re-enables the default handleToolErrors:true and\n // the X402 payload is stringified away). Reject them up front, mirroring how\n // Python's positional `create_paid_react_agent(model, tools, **kwargs)` raises\n // a TypeError if `llm`/`tools` are passed again via kwargs.\n if ('llm' in options || 'tools' in options) {\n throw new Error(\n 'createPaidReactAgent: `llm` and `tools` are set from the `model` and ' +\n '`tools` arguments and must not be passed in `options` (they would ' +\n 'override the handleToolErrors:false ToolNode and break x402 discovery).',\n )\n }\n\n let prebuilt: typeof import('@langchain/langgraph/prebuilt')\n try {\n prebuilt = await import('@langchain/langgraph/prebuilt')\n } catch (err) {\n throw new Error(\n 'createPaidReactAgent requires @langchain/langgraph. ' +\n `Install it with \\`pnpm add @langchain/langgraph\\`. (${\n err instanceof Error ? err.message : String(err)\n })`,\n )\n }\n const { ToolNode, createReactAgent } = prebuilt\n\n // handleToolErrors: false re-raises tool exceptions instead of stringifying\n // them into a ToolMessage, so PaymentRequiredError reaches agent.invoke()'s\n // caller with its X402PaymentRequired payload intact.\n const toolNode = new ToolNode(tools as never, { handleToolErrors: false })\n // `createReactAgent` is the current prebuilt entry point in\n // @langchain/langgraph@1.2.0. It is marked @deprecated in favour of\n // `createAgent`, but that replacement lives in the separate `langchain`\n // package (out of scope for this SDK's optional langgraph peer), so the\n // prebuilt `createReactAgent` is the deliberate, only in-package choice here.\n // Spread `...options` FIRST so the protected `llm`/`tools` keys (set last)\n // always win, even though the guard above already forbids them in `options`.\n return createReactAgent({ ...options, llm: model as never, tools: toolNode } as never)\n}\n"]}