Skip to content

paymentTools()

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.

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 8

The tools, in order:

ToolWhat it doesPays?
piprail_discoverFind payable resources on the open x402 indexes (“what can I buy?”).No
piprail_quote_paymentPrice a gated URL and check it against policy.No
piprail_plan_paymentCheck you can pay: balance, gas, recipient readiness across every rail.No
piprail_pay_requestPay if needed and return the result.Yes
piprail_registerList a resource you run on the open indexes.No
piprail_budgetRead remaining budget + time leash (Mode A self-check).No
piprail_guideRead the agent contract: how to quote/plan/pay and read a refusal.No
piprail_verify_receiptRe-verify a verifiable receipt against the chain (wallet-free).No

Each tool is documented in detail on The agent tools.

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.

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 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.

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 guards

onBeforePay 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.

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.