This guide covers the LangChain and LangGraph integration shipped in the [langchain] optional extra of payments-py. The module payments_py.x402.langchain provides the decorator, helpers, and exceptions for monetizing LangChain tools with the x402 protocol.
For the conceptual walk-through (two integration approaches, the discovery-first flow, dynamic credits patterns), see the LangChain integration guide. For a runnable end-to-end demo, see the langchain-paid-agent-py tutorial.
Looking to gate a LangSmith Deployment entry point (/threads/{id}/runs/wait etc.) rather than individual tools? See LangSmith Deployment Middleware. The two integrations are complementary: the decorator covered here protects tools the agent calls; the LangSmith Deployment middleware protects the agent’s HTTP entry point.
Installation
pip install payments-py[langchain] langgraph langchain-openai
The [langchain] extra installs langchain-core. langgraph and langchain-openai are optional — needed only if you build a LangGraph agent.
Exports
| Symbol | Purpose |
|---|
requires_payment | Decorator that gates a @tool on an x402 access token |
PaymentRequiredError | Exception raised when the token is missing or verification fails |
last_settlement | Read the most recent settlement receipt from outside the agent’s call stack |
create_paid_react_agent | LangGraph ReAct agent builder that lets PaymentRequiredError propagate intact |
All four are exported from payments_py.x402.langchain:
from payments_py.x402.langchain import (
requires_payment,
PaymentRequiredError,
last_settlement,
create_paid_react_agent,
)
requires_payment
Decorator that protects a LangChain @tool with x402 payment verification and settlement. Pulls the access token from RunnableConfig.configurable["payment_token"], verifies it, runs the tool body, then settles credits.
Signature
def requires_payment(
payments: Payments,
plan_id: str | None = None,
plan_ids: list[str] | None = None,
credits: int | Callable[[dict], int] = 1,
agent_id: str | None = None,
network: str | None = None,
scheme: str | None = None,
) -> Callable
| Parameter | Description |
|---|
payments | A Payments instance with payments.facilitator configured. |
plan_id / plan_ids | The plan(s) the tool charges against. Pass one of the two; plan_id is a convenience for the single-plan case. |
credits | How many credits to charge. Sent to the facilitator as max_amount. See credits semantics for fixed-vs-range plans. |
agent_id | Optional agent identifier propagated to the receipt. |
network | Optional CAIP-2 network ID. Auto-resolved from the plan when omitted. |
scheme | Optional x402 scheme (nvm:erc4337 or nvm:card-delegation). Auto-resolved when omitted. |
Decorator order
@tool outside, @requires_payment inside:
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool
from payments_py import Payments, PaymentOptions
from payments_py.x402.langchain import requires_payment
payments = Payments.get_instance(
PaymentOptions(nvm_api_key="nvm:builder-key", environment="sandbox")
)
@tool
@requires_payment(payments=payments, plan_id="plan-123", credits=1)
def get_market_insight(topic: str, config: RunnableConfig = None) -> str:
"""Return a short market insight. Costs 1 credit per call."""
return f"Market insight for '{topic}': demand is up 12% QoQ."
The protected function must accept a config: RunnableConfig parameter — that is how the decorator reads the payment token at call time.
Dynamic credits
credits accepts three forms:
- Static int —
credits=1 (fixed cost per call).
- Lambda —
credits=lambda ctx: max(1, len(ctx["result"]) // 100).
- Named function —
credits=my_fn where my_fn(ctx) -> int.
When callable, ctx is {"args": <tool kwargs>, "result": <tool return>}. Dynamic credits resolve after execution so the result is available.
Credits semantics
The credits argument is sent to the facilitator as max_amount. The actual amount redeemed depends on the plan’s server-side credit configuration:
- Fixed plans (
plan.credits.minAmount == plan.credits.maxAmount) always burn plan.credits.maxAmount. The decorator’s credits=N is then effectively a no-op (per nevermined-io/nvm-monorepo#1568).
- Range plans clamp the supplied value into
[plan.credits.minAmount, plan.credits.maxAmount].
If you want predictable per-call cost, configure the plan as fixed; the decorator value is then a client-side declaration.
PaymentRequiredError
Raised by @requires_payment when the token is missing from config["configurable"]["payment_token"], or when the facilitator rejects the token (expired, invalid signature, insufficient balance, etc.). Carries the full X402PaymentRequired payload so callers can run the x402 discovery flow.
from payments_py.x402.langchain import PaymentRequiredError
try:
agent.invoke({"messages": [("human", "...")]}, config={"configurable": {}})
except PaymentRequiredError as err:
accepts = err.payment_required.accepts[0]
# accepts.scheme → "nvm:erc4337" or "nvm:card-delegation"
# accepts.network → CAIP-2 chain or provider name (stripe, braintree, visa)
# accepts.plan_id → which plan to acquire a token against
Attributes
| Attribute | Type | Description |
|---|
payment_required | X402PaymentRequired | None | The full x402 v2 payment-required payload. accepts[0] carries scheme / network / plan_id / agent_id. |
For the discovery → acquire → retry flow, see the LangChain integration guide.
last_settlement
Returns the most recent SettleResponse produced by @requires_payment in this process. Use this after invoking a LangGraph runnable to recover the settlement receipt — credits_redeemed, remaining_balance, transaction, network, payer — without threading it back through RunnableConfig.configurable (which LangGraph copies per node, so the SDK’s in-place write is not visible to the outer caller).
Signature
def last_settlement() -> SettleResponse | None
Returns None if no settlement has happened yet in this process, or if the most recent invocation raised before reaching the settle phase.
Example
from payments_py.x402.langchain import last_settlement
result = agent.invoke(
{"messages": [("human", "...")]},
config={"configurable": {"payment_token": token}},
)
receipt = last_settlement()
if receipt:
print(f" credits_redeemed: {receipt.credits_redeemed}")
print(f" remaining_balance: {receipt.remaining_balance}")
print(f" transaction: {receipt.transaction}")
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.
create_paid_react_agent
Thin wrapper over langgraph.prebuilt.create_react_agent that constructs the underlying ToolNode with handle_tool_errors=False. That single change is what lets PaymentRequiredError propagate all the way back to agent.invoke()’s caller with its X402PaymentRequired payload intact — the default ToolNode behaviour stringifies the exception into a ToolMessage for the LLM and loses the payload.
Signature
def create_paid_react_agent(
model: Any,
tools: Sequence[Any],
**kwargs: Any,
) -> CompiledStateGraph
| Parameter | Description |
|---|
model | Anything accepted by create_react_agent’s model argument (a BaseChatModel, a string, a runnable, etc.). |
tools | Sequence of LangChain tools — typically functions decorated with @tool and @requires_payment. |
**kwargs | Forwarded verbatim to create_react_agent (prompt, state_schema, checkpointer, …). Unknown kwargs raise TypeError from LangGraph at call time. |
langgraph is imported lazily so the [langchain] extra need not pull it in. Install LangGraph yourself (pip install langgraph) to use this helper.
Example
from langchain_openai import ChatOpenAI
from payments_py.x402.langchain import create_paid_react_agent, requires_payment
@tool
@requires_payment(payments=payments, plan_id=PLAN_ID, credits=1)
def get_market_insight(topic: str, config=None) -> str:
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. Always call get_market_insight.",
)
End-to-end usage
The canonical x402 flow uses all four symbols together — discovery, acquisition, retry, receipt read:
import os
from dotenv import load_dotenv
from payments_py import Payments, PaymentOptions
from payments_py.x402.langchain import (
PaymentRequiredError,
create_paid_react_agent,
last_settlement,
requires_payment,
)
from payments_py.x402.types import DelegationConfig, X402TokenOptions
load_dotenv()
payments = Payments.get_instance(
PaymentOptions(
nvm_api_key=os.environ["NVM_API_KEY"],
environment=os.getenv("NVM_ENVIRONMENT", "sandbox"),
)
)
# 1. Build the agent (see the LangChain integration guide for the protected tool).
agent = create_paid_react_agent(model, [get_market_insight], prompt=prompt)
# 2. 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]
# 3. Acquire a token against the discovered plan.
pm = next(m for m in payments.delegation.list_payment_methods()
if m.provider == accept.network)
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 and read the settlement.
result = agent.invoke(
{"messages": [("human", QUERY)]},
config={"configurable": {"payment_token": token}},
)
receipt = last_settlement()
For the full, runnable version see tutorials/langchain-paid-agent-py.
Observability with LangSmith
When the optional [langsmith] extra is installed and a LangSmith run is active in the calling context, @requires_payment automatically emits two child spans nested under the active tool span:
nvm:verify — opens around the verify-permissions call, with attributes describing the scheme, plan, payer, and verify duration.
nvm:settlement — opens around the settle-permissions call, with attributes describing credits redeemed, remaining balance, transaction hash, network, and settle duration.
The same nvm.* metadata is also attached to the parent tool span so the trace is searchable from either level.
Install
pip install "payments-py[langchain,langsmith]"
Enable
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<your-langsmith-api-key>
export LANGSMITH_PROJECT=<your-project-name> # optional
No code changes are needed beyond the existing @requires_payment decorator — the spans are emitted automatically when LangSmith is active. If langsmith is not installed or LANGSMITH_TRACING is unset, span emission is a silent no-op.
Regional endpoint
LangSmith hosts accounts across several regions. The SDK defaults to GCP US (https://api.smith.langchain.com); accounts in any other region must set LANGSMITH_ENDPOINT or the trace POST will fail with 403 Forbidden on /runs/multipart.
| Region | Endpoint |
|---|
| GCP US | https://api.smith.langchain.com (default) |
| GCP EU | https://eu.api.smith.langchain.com |
| GCP APAC | https://apac.api.smith.langchain.com |
| AWS US | https://aws.api.smith.langchain.com |
export LANGSMITH_ENDPOINT=https://eu.api.smith.langchain.com
The payment flow itself is unaffected by trace-shipping failures — verify, tool execution, and settle proceed normally. Only the LangSmith trace ingestion fails, surfaced as langsmith.utils.LangSmithError warnings.
Span attributes
| Span | Attribute | Source |
|---|
nvm:verify | nvm.plan_ids | configured plan_id / plan_ids |
nvm:verify | nvm.scheme | resolved scheme (nvm:erc4337 or nvm:card-delegation) |
nvm:verify | nvm.network | CAIP-2 chain or provider name |
nvm:verify | nvm.agent_id | configured agent_id |
nvm:verify | nvm.payer | from VerifyResponse.payer |
nvm:verify | nvm.agent_request_id | from VerifyResponse.agent_request_id |
nvm:verify | nvm.payment_token | first 16 chars + … + last 4 chars of the x402 access token |
nvm:verify | nvm.verify.duration_ms | measured |
nvm:settlement | nvm.credits_redeemed | from SettleResponse.credits_redeemed |
nvm:settlement | nvm.balance.after | from SettleResponse.remaining_balance |
nvm:settlement | nvm.tx_hash | from SettleResponse.transaction |
nvm:settlement | nvm.network | from SettleResponse.network |
nvm:settlement | nvm.payer | from SettleResponse.payer |
nvm:settlement | nvm.payment_token | first 16 chars + … + last 4 chars of the x402 access token |
nvm:settlement | nvm.settle.duration_ms | measured |
Sensitive data in traces
The payment_token that the buyer passes via config["configurable"]["payment_token"] is captured by LangChain into the parent tool span’s metadata, and would normally be inherited by any child span — including the nvm:verify and nvm:settlement spans the decorator emits. The full token grants access to the protected tool until it expires, so the decorator proactively strips payment_token from the parent tool span’s metadata before opening any child span. The full credential never reaches a Nevermined span attribute.
For correlation across spans the decorator surfaces an abbreviated nvm.payment_token attribute (eyJ4NDAyVmVyc2lv…bsig, first 16 chars + ellipsis + last 4) on both nvm:verify and nvm:settlement. That gives you “which token was this?” without exposing the credential itself.
The active redaction covers the documented LangChain-via-configurable path. If you’re surfacing the token through a different channel (custom callbacks, an explicit add_metadata({"payment_token": ...}), raw inputs to a tool whose signature contains the token), the decorator can’t see those — strip them yourself or set export LANGSMITH_HIDE_INPUTS=true for blanket coverage.
Other nvm.* attributes that may be considered sensitive depending on your context:
nvm.payer — the payer’s wallet address (public on-chain, but a stable identifier).
nvm.tx_hash — the settlement transaction id.
nvm.agent_request_id — Nevermined-internal correlation id.
nvm.balance.after — the payer’s remaining credit balance after this settlement. Reveals per-payer depletion patterns to anyone with trace read access on the operator’s LangSmith project. Suppress with LANGSMITH_HIDE_OUTPUTS=true or post-filter.
None of these grant access on their own.
Manual use (non-LangChain paths)
The same context managers are also exported for code that wants to emit Nevermined-flavored spans without going through @requires_payment (e.g. the FastAPI middleware path):
from payments_py.langsmith import (
settlement_span,
verify_span,
build_settle_metadata,
)
with settlement_span(plan_ids=["plan-1"]) as span:
settlement = payments.facilitator.settle_permissions(...)
if span is not None:
span.add_metadata(build_settle_metadata(settlement, ["plan-1"]))
Span emission failures are caught internally — observability is best-effort and will not interfere with the payment flow.