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 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, LangChain, or
elizaOS 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.
paymentTools() accepts any PayingClient, either a single-chain PipRailClient or a
MultiChainPayer (one wallet per chain, auto-routing to
whichever chain the 402 asks for). The tools are identical either way.
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: { key: process.env.AGENT_KEY! }, schemes: ['onchain-proof', 'exact'], // pay standard x402 servers too, GASLESS for the buyer autoRoute: true, // and auto-prefer the cheapest settleable rail (the gasless one) policy: { maxTotal: '5.00' },})const tools = paymentTools(client) // → AgentTool[] of length 8The 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 |
piprail_verify_receipt | Re-verify a verifiable receipt against the chain (wallet-free). | No |
Each tool is documented in detail on The agent 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 SDK (v5/v6. Note: AI SDK v4 used `parameters` instead of `inputSchema`)import { tool, jsonSchema } from 'ai'
const aiTools = Object.fromEntries( paymentTools(client).map((t) => [ t.name, tool({ description: t.description, inputSchema: 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.
Ready-made wiring: elizaOS (a native plugin built on these exact descriptors), OpenClaw, and Hermes ship this wiring pre-built; see the Integrations section.
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, and 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, so 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}Two tools are readOnlyHint: false: piprail_pay_request and piprail_register. Only
piprail_pay_request is destructiveHint: true with idempotentHint: false (it moves value,
and paying twice means two payments); piprail_register is destructiveHint: false (it writes
a listing to an external index but moves no funds). The other six tools are 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: { key: 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.