Skip to main content
LANGGRAPH INTEGRATION

LangGraph payment integration.

Add USDC payments to any LangGraph workflow. Drop-in payment node, webhook-driven resume via interrupt, durable across runs.

SHORT ANSWER

Blockchain0x provides a drop-in BlockchainOxPaymentNode for LangGraph workflows. Install @blockchain0x/langgraph, add the node to your StateGraph, place an interrupt() after it to suspend until payment confirms, and route a conditional edge based on the resulting paymentStatus. The webhook handler resumes the graph with resumeGraph(). Payments settle on Base.

WHY LANGGRAPH FITS THIS PATTERN

Durable execution makes the resume-after-payment pattern natural.

The hard part of agent-driven payments is the async gap: you request payment, the buyer pays minutes later (or never), and you have to resume work without losing the state of where you were. Most agent frameworks rely on a job queue + manual state-store to bridge this gap. LangGraph already has the primitive built in: interrupt() suspends a graph at a node and a checkpointer persists the state to disk. Resuming is one call.

Our integration leans into this. BlockchainOxPaymentNode writes the payment_request_id and hosted_url into the graph state. A wait_for_payment node uses interrupt() to suspend. The webhook handler calls resumeGraph() with the matching payment_request_id, which finds the suspended state by thread_id, updates paymentStatus to "confirmed", and continues execution. The graph picks up exactly where it left off, including any LLM context from earlier nodes.

INSTALLATION

One npm install. Three environment variables.

Targets Node 18+ and @langchain/langgraph 0.2+. The TypeScript/JS LangGraph is the primary supported runtime; Python LangGraph users should install the equivalent pip install blockchain0x-langgraph package and follow the Python examples in the starter repo.

INSTALL
npm install @blockchain0x/langgraph
ENVIRONMENT VARIABLES
export BLOCKCHAIN0X_API_KEY=sk_live_...
export BLOCKCHAIN0X_AGENT_ID=agt_abc123
export BLOCKCHAIN0X_SIGNING_SECRET=whsec_...

BLOCKCHAIN0X_API_KEY and BLOCKCHAIN0X_AGENT_ID come from the agent's settings page after creation. BLOCKCHAIN0X_SIGNING_SECRET is needed in the process that handles webhooks. If your graph runs in one process and your webhook handler in another, both need access to the same checkpointer (e.g. shared Postgres) so they can find each other's state.

FULL GRAPH EXAMPLE

A three-node graph with billing, suspend, and research.

Below is a complete LangGraph workflow with a typed ResearchState, a BlockchainOxPaymentNode that handles the billing API call, an interrupt-based wait_for_payment node, and a research node that delivers the work after payment confirms.

GRAPH.TS
import { StateGraph, START, END, MemorySaver, interrupt } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { BlockchainOxPaymentNode } from "@blockchain0x/langgraph";

interface ResearchState {
  request: string;
  amountUsdc: string;
  paymentRequestId?: string;
  hostedUrl?: string;
  paymentStatus?: "pending" | "confirmed" | "failed";
  report?: string;
}

const llm = new ChatOpenAI({ model: "gpt-4o" });

// Payment node from the SDK - creates a payment request and writes
// paymentRequestId + hostedUrl to state.
const billingNode = new BlockchainOxPaymentNode<ResearchState>({
  amount: (state) => state.amountUsdc,
  reason: (state) => state.request,
});

async function waitForPayment(state: ResearchState) {
  // Suspend the graph until the webhook handler resumes it.
  const updatedState = interrupt<ResearchState>(state);
  return updatedState;
}

async function deliverResearch(state: ResearchState, { llm }: { llm: ChatOpenAI }) {
  const res = await llm.invoke(`Produce a report on: ${state.request}`);
  return { report: res.content };
}

const graph = new StateGraph<ResearchState>({ channels: {} })
  .addNode("billing", billingNode.asNode())
  .addNode("wait_for_payment", waitForPayment)
  .addNode("research", deliverResearch)
  .addEdge(START, "billing")
  .addEdge("billing", "wait_for_payment")
  .addConditionalEdges("wait_for_payment", (state) =>
    state.paymentStatus === "confirmed" ? "research" : END,
  )
  .addEdge("research", END)
  .compile({ checkpointer: new MemorySaver() });

When the graph runs, the billing node creates the payment request and writes paymentRequestId + hostedUrl into state. The wait_for_payment node calls interrupt(), which suspends the graph and persists state via the checkpointer. The conditional edge waits to be told paymentStatus="confirmed" before routing to research; if the payment fails, it routes to END.

WEBHOOK HANDLING

resumeGraph() finds the suspended state and continues.

The SDK's resumeGraph helper does the heavy lifting: looks up the suspended graph by payment_request_id (which the SDK maps to a LangGraph thread_id), updates the state with the webhook event data, and calls graph.invoke() to continue execution from the interrupt point.

WEBHOOK.TS
import express from "express";
import { verifyWebhook, resumeGraph } from "@blockchain0x/langgraph";

const app = express();

app.post(
  "/webhooks/payment",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.header("X-Blockchain0x-Signature") ?? "";
    if (!verifyWebhook(req.body, signature, process.env.BLOCKCHAIN0X_SIGNING_SECRET!)) {
      return res.status(401).send("Invalid signature");
    }
    const event = JSON.parse(req.body.toString());
    if (event.type === "payment.confirmed") {
      // Resume the suspended graph for this payment_request_id.
      await resumeGraph({
        graph,
        paymentRequestId: event.data.payment_request_id,
        update: { paymentStatus: "confirmed" },
      });
    }
    res.status(200).send("ok");
  },
);

verifyWebhook uses HMAC-SHA256 in constant time on the raw body. resumeGraph internally calls graph.updateState with the confirmed payment status, then graph.invoke(null, thread_id) to resume from the persisted checkpoint. The graph completes the research node and exits cleanly. For payment.failed events, call resumeGraph with the failed status instead; the conditional edge routes to END.

STARTER REPOSITORY

Working examples with checkpointers and human-in-the-loop.

A complete LangGraph repository at the GitHub link below. Includes the three-node example above (TS), a Python variant, a SqliteSaver production setup, a PostgresSaver production setup, a hybrid graph combining payment + human-in-the-loop interrupts, and a LangGraph Platform deployment example.

github.com/blockchain0x/agent-wallet-langgraph

Repository structure: graph.ts (TS three-node graph), python/graph.py (Python equivalent), webhook.ts (Express handler), platform/ (LangGraph Platform deployment config), production/ (SqliteSaver + PostgresSaver setups), docker-compose.yml.

COMMON PITFALLS

Five LangGraph-specific traps to avoid.

LangGraph's durable-execution model is powerful but has its own footguns around checkpoints, interrupts, and state updates.

PITFALL 1

Missing checkpointer

LangGraph's interrupt() only works if the graph has a checkpointer configured. Without one, the state evaporates the moment your process exits, and the webhook handler has no graph to resume. Use MemorySaver for development and SqliteSaver or PostgresSaver in production - the starter repo shows both. The error 'cannot resume without checkpointer' on the first webhook is the giveaway.

PITFALL 2

interrupt() vs human-in-the-loop confusion

LangGraph's interrupt() was originally designed for human-in-the-loop pauses where a human approves a step. We use the same primitive for webhook-driven resume: the graph suspends at wait_for_payment, the webhook updates the state, the graph resumes. If your interrupt logic tries to call Command() or invoke() from inside the node (the old human-in-the-loop pattern), you get nested-invocation errors. Use the modern resumeGraph helper from our SDK, which calls graph.updateState + graph.invoke correctly.

PITFALL 3

Conditional edges and END semantics

The conditional edge from wait_for_payment to either research or END can trip people up. If you forget to handle the payment.failed case, the graph keeps suspending forever. The pattern in the example routes failed to END, which terminates cleanly; the user gets a status update but no work happens. You can also route failed back to billing for a retry, but that requires a retry counter in state to avoid infinite loops.

PITFALL 4

Streaming and checkpoint conflicts

LangGraph supports graph.stream() for streaming intermediate states. When combined with interrupt, the stream pauses at the interrupt point but does not close the stream by default. If your UI is consuming the stream and treats every chunk as 'still running', it will hang at the payment-pending state forever. Configure the stream to close on interrupt with streamMode='updates' and check for the special interrupt marker. The starter repo has a streaming example.

PITFALL 5

Webhook race against checkpoint persistence

Very rarely, the webhook arrives before the checkpoint is fully persisted (e.g. you are using MemorySaver in production - do not - and the graph hasn't finished writing to memory yet). The resumeGraph helper has built-in retry with exponential backoff for exactly this case, but if you wrote your own resume logic, you might see 'no checkpoint found for thread_id' errors. The recommended pattern is to use the helper.

FREQUENTLY ASKED

Three LangGraph-specific questions.

Does this work with LangGraph Cloud / LangGraph Platform?

Yes. LangGraph Cloud and LangGraph Platform (the managed deployment of LangGraph) both use the same graph definition. The BlockchainOxPaymentNode and resumeGraph helper work in either runtime. The only LangGraph Platform-specific note is that you should use the platform's webhook endpoint configuration (not a separate Express server) - point Blockchain0x's webhook URL at the LangGraph Platform endpoint and our SDK adapts. See the starter repo for the LangGraph Platform variant.

Can I use this with the Python LangGraph (langgraph 0.2+) as well?

Yes, but a separate package: pip install blockchain0x-langgraph (Python) exposes the equivalent primitives (BlockchainOxPaymentNode, resume_graph). The graph definition syntax is virtually identical between Python and TypeScript LangGraph. The starter repository has a python/ subdirectory with the same example reimplemented. Pick whichever language matches your existing stack.

How does this work with human-in-the-loop nodes in the same graph?

Cleanly. interrupt() supports multiple interruption points in a single graph, each waited on by different external events. The pattern: a billing node, a wait_for_payment interrupt, a human_review interrupt, and the research node, all in one graph. The webhook handler resumes the payment interrupt; a separate UI action resumes the human_review interrupt. As long as both resumes update the correct field on the state, the graph routes through them in sequence. The starter repo includes a hybrid example.

Add durable payments to your graph.

Pause on payment, resume on webhook. Pro at $9/agent/month.