Skip to content

paymentTools()

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.

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 7

The seven 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

Each tool is documented in detail on The 7 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
import { 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.

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

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 guards

onBeforePay receives the PipRailQuoteq.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.