Skip to content

The agent guide

PIPRAIL_AGENT_GUIDE is the PipRail contract written for the model: one tight Markdown string an LLM reads once and then drives the payment tools correctly with near-zero other docs. It covers the quote → plan → pay loop, how to read a refusal without crashing or double-spending, and the difference between the two operating modes.

It’s a pure static constant — no imports, no I/O — so you can prepend it to any agent’s system prompt, MCP or not.

import { PIPRAIL_AGENT_GUIDE } from '@piprail/sdk'
const systemPrompt = `${PIPRAIL_AGENT_GUIDE}\n\n${yourTaskInstructions}`

agentGuide() is a function that returns the same string, for callers that prefer a call over a constant:

import { agentGuide } from '@piprail/sdk'
const guide = agentGuide()
// → '# Paying with PipRail — the agent contract\n…' (identical to PIPRAIL_AGENT_GUIDE)

The loop it teaches: quote → plan → pay

Section titled “The loop it teaches: quote → plan → pay”

The guide tells the model to always run three steps in order so it never commits to a payment it can’t finish:

StepToolWhat it does
1. Price itpiprail_quote_payment(url)Returns amount, token, chain, and whether it’s within policy (wraps quote()). No funds move.
2. Can I afford it now?piprail_plan_payment(url)Reads balance, gas, and recipient readiness across rails (wraps planPayment()) → { payable, best, fundingHint, session? }.
3. Paypiprail_pay_request(url, method?, body?)Pays — but only if the plan was payable — and returns the result.

The rule it states plainly: always plan before you pay. If payable is false, the model is told not to attempt the payment — fundingHint says exactly what to fix.

Reading a refusal — never crash, never double-spend

Section titled “Reading a refusal — never crash, never double-spend”

The guide drills the single most important agent behaviour: a failed pay returns a structured object, never a thrown error. The model branches on code (always reliable) and on reasonCode when declined is true.

{ "ok": false, "code": "...", "reason": "...", "explain": "...", "ref": "...", "reasonCode": "...", "declined": true }

The guide enumerates how the model should respond to each case:

code / reasonCodeWhat the model is told to do
declined + SESSION_EXPIREDTerminal. Time budget over. Stop — retry no payment this process.
declined + APPROVALA human or hook declined. Terminal for this pay; do not auto-retry.
declined + OUTSIDE_WINDOWRolling rate-limit exhausted. Wait for it to free, then retry — don’t raise the amount.
declined + POLICY / BUDGETA spend cap or allowlist refused it. Pick a cheaper/allowed resource.
INSUFFICIENT_FUNDSTop up the wallet (token and/or native gas), then retry.
PAYMENT_TIMEOUT / MAX_RETRIES_EXCEEDED / CONFIRMATION_TIMEOUTThe payment may already be on-chain — never re-pay (it would double-spend).
NO_COMPATIBLE_ACCEPT / UNSUPPORTED_SCHEMENot payable on this chain/scheme; explain says which.

The guide points the model at piprail_budget to self-check before paying: it reports how much budget and time are left per (network, asset), plus spend so far. Read-only — it moves no funds. See the 7 tools for the full tool surface.

The contract spells out the two ways an agent runs, so the model knows whether the policy is the consent or whether a human is in the loop:

  • Mode A (headless, default): the agent runs free inside a pre-set budget + time envelope. The policy is the consent — there is no per-payment prompt. piprail_budget shows what’s left.
  • Mode B (supervised): the host may ask a human to approve each payment. A decline/cancel/timeout comes back as declined: true with reasonCode: 'APPROVAL' — the model is told not to retry it as if it were a transient error.

See Modes for how the MCP server selects A or B.

The guide closes with two facts the model must not get wrong:

  • Spend caps are per (network, asset). There is no single cross-token dollar cap — budgets aren’t summed across tokens, because there’s no price oracle.
  • Spend totals and the time envelope live in-memory for this process — they reset on restart. It’s a convenience, not a durable ledger.

When you run @piprail/mcp with the guide enabled (default on), the same string is exposed two ways, so any MCP client can pull it into context:

SurfaceIdentifier
Promptpiprail_agent_guide
Resourcepiprail://guide (text/markdown)

A headless, non-MCP agent doesn’t need either — import PIPRAIL_AGENT_GUIDE directly and prepend it to the system prompt.