Add Nevermined x402 payments to your LangChain and LangGraph agents
Start here: need to register a service and create a plan first? Follow the
5-minute setup.
Runnable tutorial
langchain-paid-agent-py — a deliberately minimal LangChain + LangGraph agent
with a single tool gated by @requires_payment. Clone, fill in .env, run
poetry run buyer to see the full discovery → token-acquisition → settlement
flow in five numbered steps. The cleanest starting point if you’d rather read
working code than docs.
In LangChain.js, requiresPayment() is a higher-order function that wraps the tool implementation:
import 'dotenv/config'import { tool } from '@langchain/core/tools'import { z } from 'zod'import { Payments } from '@nevermined-io/payments'import { requiresPayment } from '@nevermined-io/payments/langchain'const payments = Payments.getInstance({ nvmApiKey: process.env.NVM_API_KEY!, environment: process.env.NVM_ENVIRONMENT || 'sandbox',})const PLAN_ID = process.env.NVM_PLAN_ID!// Protect a tool with payment — 1 credit per callconst searchData = tool( requiresPayment( (args) => `Results for '${args.query}': ...`, { payments, planId: PLAN_ID, credits: 1 } ), { name: 'search_data', description: 'Search for data on a given topic. Costs 1 credit.', schema: z.object({ query: z.string() }), })
The payment token is read from config.configurable.payment_token.
Pass it when invoking the tool or agent.
In Python, @requires_payment is a decorator applied before @tool:
import osfrom dotenv import load_dotenvfrom langchain_core.runnables import RunnableConfigfrom langchain_core.tools import toolfrom payments_py import Payments, PaymentOptionsfrom payments_py.x402.langchain import requires_paymentload_dotenv()payments = Payments.get_instance( PaymentOptions( nvm_api_key=os.environ["NVM_API_KEY"], environment=os.environ.get("NVM_ENVIRONMENT", "sandbox"), ))PLAN_ID = os.environ["NVM_PLAN_ID"]# Protect a tool with payment — 1 credit per call@tool@requires_payment(payments=payments, plan_id=PLAN_ID, credits=1)def search_data(query: str, config: RunnableConfig) -> str: """Search for data on a given topic. Costs 1 credit.""" return f"Results for '{query}': ..."
The tool function must accept a config: RunnableConfig parameter.
The decorator uses it to read the payment token from
config["configurable"]["payment_token"].
import { HumanMessage } from '@langchain/core/messages'import { ChatOpenAI } from '@langchain/openai'const llm = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 })const tools = [searchData, summarizeData, researchTopic]const llmWithTools = llm.bindTools(tools)const toolMap = new Map(tools.map((t) => [t.name, t]))const messages = [new HumanMessage('Search for AI trends')]const aiMessage = await llmWithTools.invoke(messages)messages.push(aiMessage)for (const toolCall of aiMessage.tool_calls || []) { const result = await toolMap.get(toolCall.name)!.invoke( toolCall.args, { configurable: { payment_token: accessToken } } )}
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)tools = [search_data, summarize_data, research_topic]llm_with_tools = llm.bind_tools(tools)tool_map = {t.name: t for t in tools}messages = [HumanMessage(content="Search for AI trends")]ai_message = llm_with_tools.invoke(messages)messages.append(ai_message)for tool_call in ai_message.tool_calls: result = tool_map[tool_call["name"]].invoke( tool_call["args"], config={"configurable": {"payment_token": access_token}}, )
The same payment-protected tools work with LangGraph’s create_react_agent. The simple case — buyer already holds a token — just threads it through configurable:
TypeScript
Python
import { createReactAgent } from '@langchain/langgraph/prebuilt'const agent = createReactAgent({ llm: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools: [searchData, summarizeData, researchTopic], prompt: 'You are a helpful research assistant.',})const result = await agent.invoke( { messages: [{ role: 'human', content: 'Research AI agents and summarize' }] }, { configurable: { payment_token: accessToken } })
from langgraph.prebuilt import create_react_agentgraph = create_react_agent( ChatOpenAI(model="gpt-4o-mini"), [search_data, summarize_data, research_topic], prompt="You are a helpful research assistant.",)result = graph.invoke( {"messages": [("human", "Research AI agents and summarize")]}, config={"configurable": {"payment_token": access_token}},)
If the buyer doesn’t know the plan id / scheme / provider up front, the x402 way is to invoke the agent without a token, let the protected tool raise PaymentRequiredError, and read the requirements off the exception. By default LangGraph’s ToolNode would catch that exception and stringify it into a ToolMessage for the LLM — losing the X402PaymentRequired payload. Both SDKs ship a paid-agent helper for exactly this — createPaidReactAgent (TypeScript) / create_paid_react_agent (Python) — that builds the underlying ToolNode with handleToolErrors: false / handle_tool_errors=False, so the exception propagates to agent.invoke()’s caller with the payload intact.
TypeScript
Python
import { tool } from '@langchain/core/tools'import { ChatOpenAI } from '@langchain/openai'import { z } from 'zod'import { PaymentRequiredError, createPaidReactAgent, requiresPayment,} from '@nevermined-io/payments/langchain'const getMarketInsight = tool( requiresPayment( (args) => `Market insight for '${args.topic}': ...`, { payments, planId: PLAN_ID, credits: 1 }, ), { name: 'get_market_insight', description: 'Return a market insight. Costs 1 credit.', schema: z.object({ topic: z.string() }), },)const agent = await createPaidReactAgent( new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }), [getMarketInsight], { prompt: 'You are a market data assistant.' },)// 1. Discover what the agent's tool charges by invoking without a token.let accepttry { await agent.invoke( { messages: [{ role: 'human', content: QUERY }] }, { configurable: {} }, )} catch (err) { if (!(err instanceof PaymentRequiredError)) throw err accept = err.paymentRequired!.accepts[0] // accept.scheme → "nvm:erc4337" or "nvm:card-delegation" // accept.network → CAIP-2 chain or provider name (stripe, braintree, …) // accept.planId → which plan to acquire a token against}// 2. Pick a payment method matching the discovered network.const methods = await payments.delegation.listPaymentMethods()const pm = methods.find((m) => m.provider === accept.network)!// 3. Acquire the token against the discovered plan.const { accessToken } = await payments.x402.getX402AccessToken( accept.planId, undefined, { scheme: accept.scheme, delegationConfig: { providerPaymentMethodId: pm.id, spendingLimitCents: 10000, durationSecs: 3600, currency: 'usd', }, },)// 4. Retry with the token.const result = await agent.invoke( { messages: [{ role: 'human', content: QUERY }] }, { configurable: { payment_token: accessToken } },)
from payments_py.x402.langchain import ( PaymentRequiredError, create_paid_react_agent, requires_payment,)from payments_py.x402.types import DelegationConfig, X402TokenOptions@tool@requires_payment(payments=payments, plan_id=PLAN_ID, credits=1)def get_market_insight(topic: str, config: RunnableConfig = None) -> str: """Return a market insight. Costs 1 credit.""" return f"Market insight for '{topic}': ..."agent = create_paid_react_agent( ChatOpenAI(model="gpt-4o-mini", temperature=0), [get_market_insight], prompt="You are a market data assistant.",)# 1. Discover what the agent's tool charges by invoking without a token.try: agent.invoke({"messages": [("human", QUERY)]}, config={"configurable": {}})except PaymentRequiredError as err: accept = err.payment_required.accepts[0] # accept.scheme → "nvm:erc4337" or "nvm:card-delegation" # accept.network → CAIP-2 chain or provider name (stripe, braintree, …) # accept.plan_id → which plan to acquire a token against# 2. Pick a payment method matching the discovered network.pm = next(m for m in payments.delegation.list_payment_methods() if m.provider == accept.network)# 3. Acquire the token against the discovered plan.token = payments.x402.get_x402_access_token( accept.plan_id, token_options=X402TokenOptions( scheme=accept.scheme, delegation_config=DelegationConfig( provider_payment_method_id=pm.id, spending_limit_cents=10000, duration_secs=3600, currency="usd", ), ),)["accessToken"]# 4. Retry with the token.result = agent.invoke( {"messages": [("human", QUERY)]}, config={"configurable": {"payment_token": token}},)
For card-delegation plans the SDK needs a delegation config so it can
auto-create the Stripe / Braintree delegation that backs the payment
signature. Crypto plans (nvm:erc4337) need the same — see the x402 Protocol
module reference (TypeScript ·
Python) for the full token-options
surface.
createPaidReactAgent is async in TypeScript — it awaits the lazy
@langchain/langgraph import so the dependency stays optional. Install
LangGraph yourself (pnpm add @langchain/langgraph) to use it.
After a successful agent call, the buyer can read the settlement receipt — credits redeemed, remaining balance, transaction hash, network, payer — via lastSettlement() (TypeScript) / last_settlement() (Python). LangGraph copies configurable per node, so the in-place payment_settlement write is invisible to the outer scope; the accessor reads it from a module-level slot instead.
lastSettlement() / last_settlement() reads from a module-level slot. In
multi-tenant processes (e.g. a server handling concurrent settlements), the
value reflects whichever invocation settled most recently — there is no
per-call isolation. For multi-tenant scenarios, surface settlement via a
callback or observability layer instead.
Once payment protection is wired up, you can have every paid tool call surface as structured spans in LangSmith — no code changes required, just two env vars and an optional dependency. Both SDKs emit the identical nvm:verify / nvm:settlement span shape (the cross-SDK observability-spans-v1 contract), so a single trace can be correlated across a TypeScript buyer and a Python seller — e.g. filter on nvm.tx_hash.Install the optional dependency:
TypeScript
Python
pnpm add langsmith
langsmith is an optional peer dependency — the requiresPayment wrapper emits the spans automatically when it is installed and tracing is enabled.
pip install "payments-py[langchain,langsmith]"
Enable tracing:
LANGSMITH_TRACING=trueLANGSMITH_API_KEY=lsv2_pt_your-keyLANGSMITH_PROJECT=nvm-langchain # optional# Only needed if your LangSmith account is NOT in GCP US:# LANGSMITH_ENDPOINT=https://eu.api.smith.langchain.com
That’s it. Running the same agent invocation now produces a trace tree with two dedicated Nevermined child spans nested under the tool:
LangGraph└── tools └── get_market_insight ├── nvm:verify 0.28s ← around the facilitator verify call └── nvm:settlement 1.88s ← around the facilitator settle call
Each Nevermined span carries nvm.* metadata for audit + reconciliation:
The same nvm.* metadata is also attached to the parent tool span so cmd-F searches in the LangSmith UI land on either level.Failed discovery probes are first-class too. When the buyer’s first agent.invoke() runs without a payment_token (the discovery-first flow), the nvm:verify span still opens, carries the static nvm.plan_ids / nvm.scheme / nvm.network, and is marked failed by the raised PaymentRequiredError. That gives you “which plan was the probe against?” filterability instead of an opaque LangChain crash.
The payment_token the buyer passes in configurable.payment_token would normally be captured into the parent tool span’s metadata by LangChain and inherited by every child span. The full token grants access to the protected tool until it expires. Both SDKs proactively strip it from the parent span’s metadata before opening any nvm:* child, so the full credential never reaches a Nevermined-emitted attribute. The abbreviated nvm.payment_token (first 16 chars + … + last 4 chars) remains available for correlation.A too-short token (≤ 20 chars — almost always a misconfiguration, since real x402 access tokens are JWTs) is redacted, not exported: at most the first 4 chars are surfaced followed by a …(short) marker, and a token of 4 chars or fewer reveals nothing (just …(short)). A warning is logged, so a wrong value passed as the token never lands in a trace.Other channels (custom callbacks, an explicit metadata write that includes the token, tool signatures that contain the token) are not covered — strip them yourself or set export LANGSMITH_HIDE_INPUTS=true for blanket coverage.
If a span failure ever occurs during metadata building or attachment, observability is silently dropped — the payment flow itself is never interrupted. Settlement receipts persist (via lastSettlement() in TypeScript / last_settlement() in Python) regardless of whether the span emit succeeded.For the full module reference, see the Python LangChain module reference. The TypeScript helper surface (verifySpan, settlementSpan, abbreviateToken, addMetadata, redactMetadataKeys, activeRunTree, plus buildVerifyMetadata / buildSettleMetadata for manual use outside the requiresPayment decorator) is exported from @nevermined-io/payments/langsmith — JSDoc on each helper documents the contract until the next update-docs.yml sync mirrors a dedicated TS api-reference page.
The credits argument is sent to the facilitator as max_amount. The amount
actually redeemed depends on the plan’s server-side credit config:
fixed plans (where plan.credits.minAmount == plan.credits.maxAmount)
always burn plan.credits.maxAmount and ignore the supplied value (per
nvm-monorepo#1568);
range plans clamp the value into [minAmount, maxAmount]. If you want
predictable per-call cost, configure the plan as fixed.
For serving the agent over HTTP, use payment middleware on your framework. Payment is handled at the HTTP layer — tools are plain functions with no decorators or payment config.
# Explicit card-delegation scheme for fiat payments@tool@requires_payment( payments=payments, plan_id=PLAN_ID, credits=1, scheme="nvm:card-delegation",)def my_fiat_tool(query: str, config: RunnableConfig) -> str: ...
The decorator/wrapper automatically detects the payment scheme from plan metadata.
Plans with fiat pricing (isCrypto: false) use nvm:card-delegation (Stripe).
No code changes are needed on the agent side.