@nevermined-io/payments 1.4.1 → 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 +121 -93
- package/dist/api/agents-api.d.ts.map +1 -1
- package/dist/api/agents-api.js +8 -7
- package/dist/api/agents-api.js.map +1 -1
- package/dist/api/plans-api.d.ts +5 -1
- package/dist/api/plans-api.d.ts.map +1 -1
- package/dist/api/plans-api.js +14 -9
- package/dist/api/plans-api.js.map +1 -1
- package/dist/api/requests-api.d.ts.map +1 -1
- package/dist/api/requests-api.js +4 -3
- package/dist/api/requests-api.js.map +1 -1
- package/dist/common/helper.d.ts +11 -0
- package/dist/common/helper.d.ts.map +1 -1
- package/dist/common/helper.js +32 -0
- package/dist/common/helper.js.map +1 -1
- package/dist/environments.d.ts +8 -0
- package/dist/environments.d.ts.map +1 -1
- package/dist/environments.js +10 -0
- package/dist/environments.js.map +1 -1
- package/dist/plans.d.ts +24 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +24 -0
- package/dist/plans.js.map +1 -1
- package/dist/x402/delegation-api.d.ts +9 -0
- package/dist/x402/delegation-api.d.ts.map +1 -1
- package/dist/x402/delegation-api.js +4 -0
- package/dist/x402/delegation-api.js.map +1 -1
- package/dist/x402/express/middleware.d.ts.map +1 -1
- package/dist/x402/express/middleware.js +48 -25
- package/dist/x402/express/middleware.js.map +1 -1
- package/dist/x402/facilitator-api.d.ts.map +1 -1
- package/dist/x402/facilitator-api.js +10 -2
- package/dist/x402/facilitator-api.js.map +1 -1
- package/dist/x402/langchain/agent.d.ts +96 -0
- package/dist/x402/langchain/agent.d.ts.map +1 -0
- package/dist/x402/langchain/agent.js +121 -0
- package/dist/x402/langchain/agent.js.map +1 -0
- package/dist/x402/langchain/decorator.d.ts +43 -4
- package/dist/x402/langchain/decorator.d.ts.map +1 -1
- package/dist/x402/langchain/decorator.js +173 -6
- package/dist/x402/langchain/decorator.js.map +1 -1
- package/dist/x402/langchain/index.d.ts +2 -1
- package/dist/x402/langchain/index.d.ts.map +1 -1
- package/dist/x402/langchain/index.js +2 -1
- package/dist/x402/langchain/index.js.map +1 -1
- package/dist/x402/langsmith/index.d.ts +15 -0
- package/dist/x402/langsmith/index.d.ts.map +1 -0
- package/dist/x402/langsmith/index.js +15 -0
- package/dist/x402/langsmith/index.js.map +1 -0
- package/dist/x402/langsmith/spans.d.ts +163 -0
- package/dist/x402/langsmith/spans.d.ts.map +1 -0
- package/dist/x402/langsmith/spans.js +341 -0
- package/dist/x402/langsmith/spans.js.map +1 -0
- package/package.json +16 -2
|
@@ -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"]}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The `credits` option accepts two forms:
|
|
16
16
|
* - **Static number**: `credits: 1` — always charges 1 credit
|
|
17
|
-
* - **Function**: `credits: (ctx) => Math.max(1, ctx.result.length / 100)` — dynamic
|
|
17
|
+
* - **Function**: `credits: (ctx) => Math.max(1, Math.floor(ctx.result.length / 100))` — dynamic
|
|
18
18
|
*
|
|
19
19
|
* When `credits` is a function, it receives `{ args, result }` after tool execution.
|
|
20
20
|
*
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* ```
|
|
44
44
|
*/
|
|
45
45
|
import type { Payments } from '../../payments.js';
|
|
46
|
-
import { type X402PaymentRequired } from '../facilitator-api.js';
|
|
46
|
+
import { type X402PaymentRequired, type SettlePermissionsResult } from '../facilitator-api.js';
|
|
47
47
|
/**
|
|
48
48
|
* Context passed to a dynamic credits function after tool execution.
|
|
49
49
|
*/
|
|
@@ -66,7 +66,18 @@ export interface RequiresPaymentOptions {
|
|
|
66
66
|
payments: Payments;
|
|
67
67
|
/** Single plan ID to accept */
|
|
68
68
|
planId: string;
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Number of credits to charge, or a function for dynamic pricing (default: 1).
|
|
71
|
+
*
|
|
72
|
+
* This value is sent as `maxAmount` to the facilitator. The amount actually
|
|
73
|
+
* redeemed depends on the plan's server-side credit configuration:
|
|
74
|
+
*
|
|
75
|
+
* - **Fixed plans** (`plan.credits.minAmount === plan.credits.maxAmount`)
|
|
76
|
+
* always burn `plan.credits.maxAmount` — this value is then effectively a
|
|
77
|
+
* no-op (see nevermined-io/nvm-monorepo#1568).
|
|
78
|
+
* - **Range plans** clamp this value into
|
|
79
|
+
* `[plan.credits.minAmount, plan.credits.maxAmount]`.
|
|
80
|
+
*/
|
|
70
81
|
credits?: number | CreditsCallable;
|
|
71
82
|
/** Optional agent identifier */
|
|
72
83
|
agentId?: string;
|
|
@@ -88,7 +99,17 @@ export declare class PaymentRequiredError extends Error {
|
|
|
88
99
|
* Payment context stored in `config.configurable.payment_context` after verification.
|
|
89
100
|
*/
|
|
90
101
|
export interface PaymentContext {
|
|
91
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Abbreviated, non-functional reference to the x402 access token — the SAME
|
|
104
|
+
* redacted form surfaced as `nvm.payment_token` (`<first 16>…<last 4>`, or a
|
|
105
|
+
* `…(short)` marker for a too-short token). The **full token is deliberately
|
|
106
|
+
* not persisted here**: this object is written into
|
|
107
|
+
* `config.configurable.payment_context`, and tracing frameworks (e.g.
|
|
108
|
+
* LangChain) can capture `config.configurable` into span metadata, so storing
|
|
109
|
+
* the raw credential would let it ride into any traced run opened during or
|
|
110
|
+
* after the tool body. Settlement uses the token read from
|
|
111
|
+
* `config.configurable.payment_token`, never this field.
|
|
112
|
+
*/
|
|
92
113
|
token: string;
|
|
93
114
|
/** The payment required object */
|
|
94
115
|
paymentRequired: X402PaymentRequired;
|
|
@@ -101,6 +122,24 @@ export interface PaymentContext {
|
|
|
101
122
|
/** Agent request context for observability */
|
|
102
123
|
agentRequest?: unknown;
|
|
103
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Return the most recent settlement receipt produced by {@link requiresPayment}.
|
|
127
|
+
*
|
|
128
|
+
* Use this after invoking a LangChain/LangGraph runnable whose tool is wrapped
|
|
129
|
+
* with {@link requiresPayment} to recover the settlement receipt
|
|
130
|
+
* (`creditsRedeemed`, `remainingBalance`, `transaction`, `network`, `payer`)
|
|
131
|
+
* without threading it back through the runnable config (which LangGraph copies
|
|
132
|
+
* per node, so the in-place write is invisible to the outer scope).
|
|
133
|
+
*
|
|
134
|
+
* Returns `undefined` if no settlement has happened yet in this process, or if
|
|
135
|
+
* the most recent invocation raised before reaching the settle phase.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* This accessor reads from a module-level slot. In multi-tenant processes (e.g.
|
|
139
|
+
* a server handling concurrent settlements), the value reflects whichever
|
|
140
|
+
* invocation settled most recently — there is no per-call isolation.
|
|
141
|
+
*/
|
|
142
|
+
export declare function lastSettlement(): SettlePermissionsResult | undefined;
|
|
104
143
|
/**
|
|
105
144
|
* Wraps a LangChain.js tool implementation with x402 payment verification and settlement.
|
|
106
145
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../../../src/x402/langchain/decorator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAEL,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../../../src/x402/langchain/decorator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAEL,KAAK,mBAAmB,EAExB,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAA;AAY9B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,8BAA8B;IAC9B,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,CAAA;AAE7D;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,QAAQ,EAAE,QAAQ,CAAA;IAClB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,eAAe,CAAA;IAClC,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,yEAAyE;IACzE,eAAe,EAAE,mBAAmB,GAAG,SAAS,CAAA;gBAEpC,OAAO,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB;CAKnE;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,kCAAkC;IAClC,eAAe,EAAE,mBAAmB,CAAA;IACpC,kCAAkC;IAClC,eAAe,EAAE,MAAM,CAAA;IACvB,0CAA0C;IAC1C,QAAQ,EAAE,OAAO,CAAA;IACjB,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAeD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,IAAI,uBAAuB,GAAG,SAAS,CAEpE;AAwCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAC5E,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EACjE,OAAO,EAAE,sBAAsB,GAC9B,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAoNrD"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The `credits` option accepts two forms:
|
|
16
16
|
* - **Static number**: `credits: 1` — always charges 1 credit
|
|
17
|
-
* - **Function**: `credits: (ctx) => Math.max(1, ctx.result.length / 100)` — dynamic
|
|
17
|
+
* - **Function**: `credits: (ctx) => Math.max(1, Math.floor(ctx.result.length / 100))` — dynamic
|
|
18
18
|
*
|
|
19
19
|
* When `credits` is a function, it receives `{ args, result }` after tool execution.
|
|
20
20
|
*
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
* ```
|
|
44
44
|
*/
|
|
45
45
|
import { buildPaymentRequired, } from '../facilitator-api.js';
|
|
46
|
+
import { abbreviateToken, activeRunTree, addMetadata, buildSettleMetadata, buildVerifyMetadata, redactMetadataKeys, settlementSpan, verifySpan, } from '../langsmith/spans.js';
|
|
46
47
|
/**
|
|
47
48
|
* Thrown when payment verification fails or no token is provided.
|
|
48
49
|
*
|
|
@@ -56,6 +57,38 @@ export class PaymentRequiredError extends Error {
|
|
|
56
57
|
this.paymentRequired = paymentRequired;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
// Module-level holder for the most recent settlement receipt. LangGraph copies
|
|
61
|
+
// `RunnableConfig.configurable` per node, so the in-place write to
|
|
62
|
+
// `config.configurable.payment_settlement` is not visible to the buyer's outer
|
|
63
|
+
// scope. A module-level slot is the simplest reliable signal. It is
|
|
64
|
+
// intentionally single-tenant — if the same process runs multiple concurrent
|
|
65
|
+
// settlements, the last writer wins. This race is not limited to multi-tenant
|
|
66
|
+
// servers: a single `createPaidReactAgent` run can also hit it, because a ReAct
|
|
67
|
+
// agent may execute several paid tools in parallel within one LLM turn via
|
|
68
|
+
// `ToolNode`, and this single slot only retains the last writer. For
|
|
69
|
+
// multi-tenant or parallel-tool use cases, surface the receipt via a callback
|
|
70
|
+
// or via observability (see Sprint 1 of the LangChain epic).
|
|
71
|
+
let lastSettlementReceipt;
|
|
72
|
+
/**
|
|
73
|
+
* Return the most recent settlement receipt produced by {@link requiresPayment}.
|
|
74
|
+
*
|
|
75
|
+
* Use this after invoking a LangChain/LangGraph runnable whose tool is wrapped
|
|
76
|
+
* with {@link requiresPayment} to recover the settlement receipt
|
|
77
|
+
* (`creditsRedeemed`, `remainingBalance`, `transaction`, `network`, `payer`)
|
|
78
|
+
* without threading it back through the runnable config (which LangGraph copies
|
|
79
|
+
* per node, so the in-place write is invisible to the outer scope).
|
|
80
|
+
*
|
|
81
|
+
* Returns `undefined` if no settlement has happened yet in this process, or if
|
|
82
|
+
* the most recent invocation raised before reaching the settle phase.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* This accessor reads from a module-level slot. In multi-tenant processes (e.g.
|
|
86
|
+
* a server handling concurrent settlements), the value reflects whichever
|
|
87
|
+
* invocation settled most recently — there is no per-call isolation.
|
|
88
|
+
*/
|
|
89
|
+
export function lastSettlement() {
|
|
90
|
+
return lastSettlementReceipt;
|
|
91
|
+
}
|
|
59
92
|
/**
|
|
60
93
|
* Extract the payment token from a LangChain RunnableConfig.
|
|
61
94
|
*
|
|
@@ -82,6 +115,17 @@ function storeInConfigurable(config, key, value) {
|
|
|
82
115
|
return;
|
|
83
116
|
configurable[key] = value;
|
|
84
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Remove a key from config.configurable if present (no-op otherwise).
|
|
120
|
+
*/
|
|
121
|
+
function removeFromConfigurable(config, key) {
|
|
122
|
+
if (config == null || typeof config !== 'object')
|
|
123
|
+
return;
|
|
124
|
+
const configurable = config.configurable;
|
|
125
|
+
if (configurable == null || typeof configurable !== 'object')
|
|
126
|
+
return;
|
|
127
|
+
delete configurable[key];
|
|
128
|
+
}
|
|
85
129
|
/**
|
|
86
130
|
* Wraps a LangChain.js tool implementation with x402 payment verification and settlement.
|
|
87
131
|
*
|
|
@@ -126,17 +170,82 @@ function storeInConfigurable(config, key, value) {
|
|
|
126
170
|
export function requiresPayment(fn, options) {
|
|
127
171
|
const { payments, planId, credits = 1, agentId, network } = options;
|
|
128
172
|
return async (args, config) => {
|
|
173
|
+
// Reset the module-level slot at the START of every invocation, before
|
|
174
|
+
// verify. Any failure that does not reach the settle-success write (a
|
|
175
|
+
// verify failure / PaymentRequiredError, or a swallowed settle failure)
|
|
176
|
+
// then leaves lastSettlement() returning `undefined` rather than a stale
|
|
177
|
+
// receipt from a previous invocation — matching the JSDoc contract on
|
|
178
|
+
// lastSettlement().
|
|
179
|
+
lastSettlementReceipt = undefined;
|
|
129
180
|
// Build payment required object
|
|
130
181
|
const paymentRequired = buildPaymentRequired(planId, {
|
|
131
182
|
endpoint: fn.name || 'tool',
|
|
132
183
|
agentId,
|
|
133
184
|
network,
|
|
134
185
|
});
|
|
186
|
+
// The scheme/network the verify span advertises come from the resolved
|
|
187
|
+
// X402 scheme so the span metadata matches what the buyer paid against.
|
|
188
|
+
const accepted = paymentRequired.accepts[0];
|
|
189
|
+
const planIds = paymentRequired.accepts
|
|
190
|
+
.map((a) => a.planId)
|
|
191
|
+
.filter((p) => Boolean(p));
|
|
192
|
+
const resolvedScheme = accepted?.scheme;
|
|
193
|
+
const resolvedNetwork = network ?? accepted?.network;
|
|
194
|
+
// LangChain auto-captures every key in config.configurable into the parent
|
|
195
|
+
// tool span's metadata, and child spans inherit it at construction time.
|
|
196
|
+
// Strip the full x402 access token from the parent BEFORE opening the verify
|
|
197
|
+
// span so neither the parent nor the child carries the raw credential — only
|
|
198
|
+
// the abbreviated nvm.payment_token remains for correlation.
|
|
199
|
+
const parentRunTree = await activeRunTree();
|
|
200
|
+
redactMetadataKeys(parentRunTree, 'payment_token');
|
|
201
|
+
const verifyStarted = Date.now();
|
|
202
|
+
// Open the verify span BEFORE the token-presence check so failed probes
|
|
203
|
+
// (no payment_token in config) still produce a clearly-named span with the
|
|
204
|
+
// static nvm.* attrs attached to both the span and the parent tool span.
|
|
205
|
+
const vspan = await verifySpan({
|
|
206
|
+
planIds,
|
|
207
|
+
scheme: resolvedScheme,
|
|
208
|
+
network: resolvedNetwork,
|
|
209
|
+
agentId,
|
|
210
|
+
});
|
|
211
|
+
// Pre-verify metadata is best-effort and static-only.
|
|
212
|
+
const preVerifyMd = buildVerifyMetadata({
|
|
213
|
+
planIds,
|
|
214
|
+
scheme: resolvedScheme,
|
|
215
|
+
network: resolvedNetwork,
|
|
216
|
+
agentId,
|
|
217
|
+
});
|
|
218
|
+
vspan.addMetadata(preVerifyMd);
|
|
219
|
+
addMetadata(parentRunTree, preVerifyMd);
|
|
135
220
|
// Extract token from config.configurable.payment_token
|
|
136
221
|
const token = extractPaymentToken(config);
|
|
137
222
|
if (!token) {
|
|
138
|
-
|
|
223
|
+
await vspan.end(new Error('missing payment_token in config.configurable'));
|
|
224
|
+
throw new PaymentRequiredError('Payment required: missing payment_token in config.configurable', paymentRequired);
|
|
139
225
|
}
|
|
226
|
+
// Drop the raw token from configurable now that we hold it in `token`.
|
|
227
|
+
// LangChain's `ensureConfig` re-promotes every configurable scalar into a
|
|
228
|
+
// runnable's `metadata` on EACH invocation, so any traced runnable the tool
|
|
229
|
+
// body spawns (an LLM call, a sub-chain) sharing this config would otherwise
|
|
230
|
+
// re-leak the full credential into its own span metadata — and a child run
|
|
231
|
+
// opened *during* `fn()` would capture it before the settle-side
|
|
232
|
+
// `redactMetadataKeys` below could run. Removing the key here is the
|
|
233
|
+
// proactive complement to that reactive redaction. This MUST run after
|
|
234
|
+
// extraction (deleting earlier would make `extractPaymentToken` return null)
|
|
235
|
+
// and before `fn()` — guaranteed, since `fn()` is called further down.
|
|
236
|
+
// Settlement uses the local `token`, never configurable, so removal is
|
|
237
|
+
// non-functional. On the `tool()`/LangGraph path LangChain hands the wrapper
|
|
238
|
+
// a per-invocation config copy, so this does not mutate a config the caller
|
|
239
|
+
// reuses across sibling tools.
|
|
240
|
+
removeFromConfigurable(config, 'payment_token');
|
|
241
|
+
// Abbreviate/redact the token ONCE here (mirrors Python's
|
|
242
|
+
// attach_metadata_safely pre-abbreviation) and pass the result into the
|
|
243
|
+
// metadata builders. abbreviateToken is idempotent, so the builders leave
|
|
244
|
+
// it unchanged — this means the short-token warning fires at most once per
|
|
245
|
+
// call (not once per verify AND once per settle), and the raw token never
|
|
246
|
+
// reaches the builders' frame locals (defense against exception enrichers
|
|
247
|
+
// that capture locals).
|
|
248
|
+
const abbreviatedToken = abbreviateToken(token);
|
|
140
249
|
// Resolve pre-execution credits (static only; callable deferred to post-execution)
|
|
141
250
|
const creditsToVerify = typeof credits === 'number' ? credits : 1;
|
|
142
251
|
// Verify permissions
|
|
@@ -149,14 +258,38 @@ export function requiresPayment(fn, options) {
|
|
|
149
258
|
});
|
|
150
259
|
}
|
|
151
260
|
catch (error) {
|
|
261
|
+
await vspan.end(error);
|
|
152
262
|
throw new PaymentRequiredError(`Payment verification failed: ${error instanceof Error ? error.message : String(error)}`, paymentRequired);
|
|
153
263
|
}
|
|
264
|
+
// Augment span metadata with verification results + timing (both span and
|
|
265
|
+
// parent), then close the verify span. Best-effort: a metadata failure must
|
|
266
|
+
// not mask the PaymentRequiredError that may follow.
|
|
267
|
+
const verifyMd = buildVerifyMetadata({
|
|
268
|
+
planIds,
|
|
269
|
+
scheme: resolvedScheme,
|
|
270
|
+
network: resolvedNetwork,
|
|
271
|
+
agentId,
|
|
272
|
+
verification,
|
|
273
|
+
durationMs: Date.now() - verifyStarted,
|
|
274
|
+
token: abbreviatedToken,
|
|
275
|
+
});
|
|
276
|
+
vspan.addMetadata(verifyMd);
|
|
277
|
+
addMetadata(parentRunTree, verifyMd);
|
|
154
278
|
if (!verification.isValid) {
|
|
279
|
+
await vspan.end(new Error(verification.invalidReason || 'verification failed'));
|
|
155
280
|
throw new PaymentRequiredError(`Payment verification failed: ${verification.invalidReason || 'Insufficient credits or invalid token'}`, paymentRequired);
|
|
156
281
|
}
|
|
157
|
-
|
|
282
|
+
await vspan.end();
|
|
283
|
+
// Store payment context. The `token` field carries the ABBREVIATED
|
|
284
|
+
// reference, never the raw credential: `payment_context` is written into
|
|
285
|
+
// `config.configurable`, which LangChain can capture into span metadata, so
|
|
286
|
+
// persisting the full token here would reopen the very leak the parent-tree
|
|
287
|
+
// `redactMetadataKeys('payment_token')` calls close — and a child run opened
|
|
288
|
+
// *during* the tool body would capture it before any post-hoc redaction
|
|
289
|
+
// could run. `abbreviatedToken` is always defined past the `!token` guard
|
|
290
|
+
// above; `?? ''` is a belt-and-suspenders that can never fall back to raw.
|
|
158
291
|
const paymentContext = {
|
|
159
|
-
token,
|
|
292
|
+
token: abbreviatedToken ?? '',
|
|
160
293
|
paymentRequired,
|
|
161
294
|
creditsToSettle: creditsToVerify,
|
|
162
295
|
verified: true,
|
|
@@ -170,18 +303,52 @@ export function requiresPayment(fn, options) {
|
|
|
170
303
|
const finalCredits = typeof credits === 'function'
|
|
171
304
|
? credits({ args: args, result })
|
|
172
305
|
: credits;
|
|
173
|
-
// Settle credits
|
|
306
|
+
// Settle credits, wrapped in an nvm:settlement span (mirrors Python's
|
|
307
|
+
// settlement_span around settle_permissions).
|
|
308
|
+
//
|
|
309
|
+
// Re-fetch the active run tree: `fn()` may have opened nested traced runs, so
|
|
310
|
+
// `activeRunTree()` can now return a different RunTree than the verify-side
|
|
311
|
+
// redaction at the top of this function scrubbed. We already removed
|
|
312
|
+
// `payment_token` from `config.configurable` before `fn()` ran, so newly
|
|
313
|
+
// opened child runs can no longer re-promote the raw credential — this
|
|
314
|
+
// re-redaction is now defense-in-depth, covering any RunTree whose metadata
|
|
315
|
+
// was populated before that removal took effect. Redact BEFORE opening the
|
|
316
|
+
// settlement span so the credential never rides into the settle child span or
|
|
317
|
+
// the (possibly new) parent tree.
|
|
318
|
+
const settleParentRunTree = await activeRunTree();
|
|
319
|
+
redactMetadataKeys(settleParentRunTree, 'payment_token');
|
|
320
|
+
const settleStarted = Date.now();
|
|
321
|
+
const sspan = await settlementSpan({ planIds, agentId });
|
|
174
322
|
try {
|
|
175
323
|
const settlement = await payments.facilitator.settlePermissions({
|
|
176
324
|
paymentRequired,
|
|
177
325
|
x402AccessToken: token,
|
|
178
|
-
|
|
326
|
+
// A dynamic `credits` callable can return a float (e.g. length/100).
|
|
327
|
+
// `BigInt(1.5)` throws RangeError, which the surrounding try/catch
|
|
328
|
+
// swallows — credits are never burned and the caller is never told.
|
|
329
|
+
// Floor to keep the settle on the money path.
|
|
330
|
+
maxAmount: BigInt(Math.floor(finalCredits)),
|
|
179
331
|
agentRequestId: paymentContext.agentRequestId,
|
|
180
332
|
});
|
|
181
333
|
storeInConfigurable(config, 'payment_settlement', settlement);
|
|
334
|
+
// Also publish to the module-level slot so lastSettlement() can recover
|
|
335
|
+
// the receipt — LangGraph copies config.configurable per node, hiding the
|
|
336
|
+
// line above from the buyer's outer scope.
|
|
337
|
+
lastSettlementReceipt = settlement;
|
|
338
|
+
const settleMd = buildSettleMetadata({
|
|
339
|
+
settlement,
|
|
340
|
+
planIds,
|
|
341
|
+
agentId,
|
|
342
|
+
durationMs: Date.now() - settleStarted,
|
|
343
|
+
token: abbreviatedToken,
|
|
344
|
+
});
|
|
345
|
+
sspan.addMetadata(settleMd);
|
|
346
|
+
addMetadata(settleParentRunTree, settleMd);
|
|
347
|
+
await sspan.end();
|
|
182
348
|
}
|
|
183
349
|
catch (settleError) {
|
|
184
350
|
console.error('Payment settlement failed:', settleError);
|
|
351
|
+
await sspan.end(settleError);
|
|
185
352
|
// Still return result even if settlement fails
|
|
186
353
|
}
|
|
187
354
|
return result;
|