·5 min read·The PayGraph Team

Integrating payments into LangGraph agents safely

A walkthrough for LangGraph payment integration: wrap a tool node with PayGraph's guarded_tool, see the state before and after, and fit policy checks into the graph.

LangGraph gives you state machines for agents. It does not give you spending controls. If your graph has a node that moves money, you need a policy layer between the model's decision and the payment rail. Here is how to wire that up in a LangGraph agent with PayGraph's guarded_tool.

What is LangGraph payment integration?

LangGraph payment integration is the pattern of adding payment capability — Stripe Issuing, x402, or an internal ledger API — to a LangGraph agent as a callable tool or node, with the policy enforcement, approval routing, and audit logging that production requires. The naive version is a ToolNode that wraps a Stripe SDK call. The production version adds a pre-flight check: does this transaction match policy, and if not, does it get rejected or routed to a human?

PayGraph is an open-source SDK for policy-controlled spending, approvals, and audit logs for AI agents. It has first-class LangGraph support. The integration is a single decorator on the tool function your graph already calls.

Where does the policy check fit in the graph?

A typical LangGraph payment agent has four nodes: plan, tools, approve, and finalize. The model proposes an action in plan, the tools node executes it, optional approve handles human review, and finalize writes state.

The wrong place to check policy is inside plan. The model isn't the trust boundary — the tool call is. You want enforcement between the moment the model emits a tool call and the moment that tool actually talks to Stripe.

PayGraph inserts itself at exactly that seam. When you decorate a tool with @engine.guarded_tool, three things happen on every invocation, in order: policy evaluation, approval routing if the policy flags it, then execution. The LangGraph graph itself doesn't need new nodes. The enforcement lives inside the tool callable that ToolNode already invokes.

The before state: a raw LangGraph payment tool

Here's a stripped-down payment agent without any controls. The model decides, the tool fires, the money moves.

from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
import stripe
 
@tool
def make_payment(amount_usd: float, vendor: str, category: str) -> str:
    """Charge the company card for a vendor invoice."""
    charge = stripe.Charge.create(
        amount=int(amount_usd * 100),
        currency="usd",
        description=f"{category}: {vendor}",
        source="tok_visa",
    )
    return f"Paid {vendor} ${amount_usd}: {charge.id}"
 
graph = StateGraph(AgentState)
graph.add_node("plan", planner_node)
graph.add_node("tools", ToolNode([make_payment]))
graph.add_edge("plan", "tools")
graph.add_edge("tools", END)

The failure modes: no cap on amount_usd, no vendor allowlist, no approval gate, no audit trail. A prompt injection that rewrites the vendor argument goes through clean. A retry loop charges the card until it declines.

The after state: wrapping the tool with guarded_tool

The change is small. Define a policy, create an engine, and swap the decorator. The LangGraph graph structure stays identical.

from paygraph import PolicyEngine, Policy
from paygraph.langgraph import approval_interrupt
from langchain_core.tools import tool
import stripe
 
policy = Policy(
    max_per_transaction_usd=500,
    daily_cap_usd=2000,
    allowed_categories=["software", "ads", "vendor_invoice"],
    vendor_allowlist=["aws", "openai", "google-ads"],
    require_approval_above_usd=100,
)
 
engine = PolicyEngine(
    policy,
    on_approval_required=approval_interrupt,
    audit_sink="s3://paygraph-audit/agent-prod/",
)
 
@tool
@engine.guarded_tool
def make_payment(amount_usd: float, vendor: str, category: str) -> str:
    """Charge the company card for a vendor invoice."""
    charge = stripe.Charge.create(
        amount=int(amount_usd * 100),
        currency="usd",
        description=f"{category}: {vendor}",
        source="tok_visa",
    )
    return f"Paid {vendor} ${amount_usd}: {charge.id}"

Three things are now true that weren't before. Calls with amount_usd > 500 are rejected before touching Stripe. Calls between $100 and $500 trigger a LangGraph interrupt — the graph pauses, the approver decides, the graph resumes. Every attempt lands in the audit log whether it executed, was rejected, or was approved by a human.

The approval_interrupt handler uses LangGraph's native interrupt/resume mechanism, so pausing for human review is a graph checkpoint, not a custom side channel. You resume with graph.invoke(None, config) once the approver responds.

What changes in the LangGraph state?

The state object gets three new fields that PayGraph writes automatically:

FieldTypeWritten when
paygraph_decisionapproved / rejected / pending_approvalEvery tool call
paygraph_audit_idUUIDEvery tool call
paygraph_policy_violationslist of violation codesOn rejection or pending

Your existing nodes can read these. A common pattern is routing on paygraph_decision in the edge after tools:

def route_after_tools(state):
    decision = state.get("paygraph_decision")
    if decision == "pending_approval":
        return "wait_for_approval"
    if decision == "rejected":
        return "replan"
    return END
 
graph.add_conditional_edges("tools", route_after_tools)

Now the agent can self-correct. When the policy rejects a $12,000 ad buy because the cap is $500, the violation codes flow back into the planner, and the model proposes a smaller, compliant action.

How does this compare to building it yourself?

PayGraph guarded_toolCustom LangGraph wrapperPost-hoc reconciliation
SetupOne decoratorNew nodes + state fieldsCron + dashboard
Pre-flight checksYesYes, you write themNo — damage done
Human approvalsNative interruptCustom channelNo
Audit logBuilt-inYou own the schemaStripe logs only
Graph changes neededZeroSignificantN/A
Works with x402YesRebuild per railNo

The case for the decorator is pragmatic. LangGraph already gives you the graph, the state, the interrupts. PayGraph fills the control plane that LangGraph leaves out by design. You keep the graph shape you already have.

For the broader argument on why this layer exists, see why AI agents need policy-controlled spending.

Where to start

If your LangGraph agent already has a payment tool, wrapping it with guarded_tool is a ten-minute change that closes the biggest gap between demo and production.