paymentTools()
Introduction
Section titled “Introduction”paymentTools(client) hands an LLM the ability to discover, quote, plan, and pay x402
resources. It returns an array of seven framework-agnostic AgentTool
descriptors — each a name + description + JSON Schema parameters + an invoke function —
that adapt to MCP, the Vercel AI SDK, OpenAI/Anthropic function-calling, or LangChain in a
couple of lines.
The toolkit ships zero dependencies: it’s plain data plus an invoke closure over the
PipRailClient you pass in. Because the budget rides on
that client, the model can’t bypass it — every payment goes through the same policy and
onBeforePay guard.
Basic use
Section titled “Basic use”Build a client with a wallet and a spend policy, then derive the tools:
import { PipRailClient, paymentTools } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'base', wallet: { privateKey: process.env.AGENT_KEY! }, policy: { maxTotal: '5.00' },})const tools = paymentTools(client) // → AgentTool[] of length 7The seven tools, in order:
| Tool | What it does | Pays? |
|---|---|---|
piprail_discover | Find payable resources on the open x402 indexes (“what can I buy?”). | No |
piprail_quote_payment | Price a gated URL and check it against policy. | No |
piprail_plan_payment | Check you can pay — balance, gas, recipient readiness across every rail. | No |
piprail_pay_request | Pay if needed and return the result. | Yes |
piprail_register | List a resource you run on the open indexes. | No |
piprail_budget | Read remaining budget + time leash (Mode A self-check). | No |
piprail_guide | Read the agent contract — how to quote/plan/pay and read a refusal. | No |
Each tool is documented in detail on The 7 tools.
Wiring into a framework
Section titled “Wiring into a framework”An AgentTool is deliberately shaped to map onto any runtime. The pattern is the same
everywhere: expose name + description + parameters, and route the runtime’s call to
invoke.
// Vercel AI SDKimport { tool, jsonSchema } from 'ai'
const aiTools = Object.fromEntries( paymentTools(client).map((t) => [ t.name, tool({ description: t.description, parameters: jsonSchema(t.parameters), execute: t.invoke }), ]),)For OpenAI/Anthropic function-calling, the name, description, and parameters go straight
into the tools array; dispatch the model’s tool_use block to the matching invoke. The
@piprail/mcp server wires these same descriptors into an MCP server —
see Use as a library if you want to host them yourself.
The AgentTool shape
Section titled “The AgentTool shape”interface AgentTool { name: string // snake_case, namespaced `piprail_…` description: string // written for an LLM to read parameters: Record<string, unknown> // JSON Schema (draft-07 object) annotations?: ToolAnnotations // advisory MCP-style hints outputSchema?: Record<string, unknown> // result schema, on stable read-only tools invoke: (args: Record<string, unknown>) => Promise<unknown>}invoke always resolves to a JSON-serialisable value — it does not throw for a payment
failure. The pay tool funnels every SDK error into a structured object — { ok: false, code, reason, explain, ref?, reasonCode?, declined? } — so the model reasons about it instead of
crashing; see Challenge triage and Why payments
fail.
outputSchema is declared only on the stable read-only tools (quote, plan, budget) and is
kept open — no additionalProperties: false — so additive result fields never break a strict
client that validates against it.
Annotations
Section titled “Annotations”annotations mirror the MCP spec’s ToolAnnotations: advisory hints that let a client reason
about a tool’s nature. They are hints only — never make a security decision on them. The
spend policy is the real boundary.
interface ToolAnnotations { title?: string // human-friendly tool title readOnlyHint?: boolean // only reads — no state change, no funds moved destructiveHint?: boolean // may move value or do something not easily undone idempotentHint?: boolean // repeating with the same args has no extra effect openWorldHint?: boolean // reaches the open world — indexes, chains, arbitrary URLs}The only tool with readOnlyHint: false is piprail_pay_request — it carries
destructiveHint: true and idempotentHint: false because a payment moves value and paying
twice means two payments. Every other tool is readOnlyHint: true.
The budget rides on the client
Section titled “The budget rides on the client”You configure spend limits once, on the client. The tools inherit them: piprail_pay_request
passes through policy and onBeforePay, and piprail_budget reads back what’s left.
const client = new PipRailClient({ chain: 'base', wallet: { privateKey: process.env.AGENT_KEY! }, policy: { maxAmount: '1.00', maxTotal: '20.00' }, // per-payment + lifetime ceilings onBeforePay: async (q) => q.withinPolicy, // final auto-approval gate})const tools = paymentTools(client) // every pay goes through both guardsonBeforePay receives the PipRailQuote — q.withinPolicy,
q.amountFormatted, q.symbol, q.network, and so on — after the policy passes, so it’s
your last word before any funds move. PipRail has no price oracle, so there is no USD figure to
branch on; gate on q.withinPolicy or Number(q.amountFormatted) against your own ceiling.
In a headless run (Mode A), have the model call piprail_budget before paying so it discovers
the leash by reading it, not by hitting a decline. See Spend ledger
and Evaluate policy.
Making the output legible
Section titled “Making the output legible”Three of the tools embed a one-line, model-readable rendering of their result alongside the
structured data: piprail_plan_payment includes a summary from summarizePlan(),
piprail_budget includes a report from formatSpendReport(), and a declined payment carries
an explain from explainDecline(). These are pure helpers you can also call directly — see
Renderers.