Ripardocs
Dashboard

Security

Keys stay yours

Signing happens in your own process or wallet. There is no Ripar server to transmit a key to — the SDK is a library, and an agent serving paid endpoints holds no key at all, because settlement goes caller → payTo without the server touching it. See Custody model.

The one place a key does appear is the calling side: RiparClient takes a mnemonic or a secretKey and signs in-process. Keep it in the environment, never on a command line — that is why ripar call has no --mnemonic flag.

Spend caps

Caps are enforced in the caller's own process. Nothing enforces them for you.

maxPrice and maxPerDay live in RiparClient and are checked in the process that holds the wallet, before it signs. No server, facilitator or contract enforces a caller's budget — there is nothing on the network that knows what your budget is. A cap you did not set is a cap that does not exist, and a second process using the same mnemonic has its own separate counter.

Set them where the key is:

import { RiparClient } from "@ripar/sdk";
 
const ripar = new RiparClient({
  mnemonic: process.env.RIPAR_MNEMONIC,
  maxPrice: "0.02",   // refuse any single quote above this
  maxPerDay: "5.00",  // refuse once this much is spent in a rolling 24h
});

or on the command line:

ripar call https://agent.example.com/summarize \
  --body '{"text":"…"}' --max-price 0.02 --max-per-day 5.00
CapWhere it livesGuards against
maxPriceRiparClient, in your processOne absurdly-priced endpoint
maxPerDaySpendLedger, in your process, in memorySlow drift nobody notices

How they actually work, because the mechanism decides what they are worth:

  • The quote is read before anything is signed. call() fetches the 402 first, decodes it, and throws price_above_max or daily_cap_reached rather than paying. Without a cap set, the wrapped fetch settles whatever it is quoted.
  • An unreadable quote fails closed. If a cap is set and the 402 cannot be decoded, the client throws unreadable_quote instead of paying blind.
  • The day is a rolling 24 hours, not a calendar day. A calendar cap resets at midnight, so a retry loop drains twice the budget across the boundary and shows two normal-looking days.
  • The ledger is in memory. Restart the process and the window restarts with it. It is a guard, not a guarantee.
Caps are the backstop, not the plan

A cap turns an unbounded loss into a bounded one. It does not make an agent correct. Pair it with validation on every response, and fund a dedicated wallet rather than pointing an autonomous process at your treasury.

Rate limiting, on the earning side

serve() can cap how often one caller invokes your paid endpoints, keyed on the Algorand address inside the payment header — so a caller cannot get a fresh budget by changing IP:

await serve(agent, { rateLimit: { perMinute: 60, per: "payer" } });

Two caveats that decide whether this protects anything:

  • It is in-process. Two replicas allow two windows.
  • The payment header is not signature-checked before the limiter reads it, so the payer identity is a claim rather than proof. See the known-gap note in the SDK's src/identity.ts before relying on it to isolate one caller from another.

per: "ip" keys on the socket address instead and counts every request to a paid route, which is the mode that stops a flood from one host.

What runs before payment

Order is rate limit → idempotency → input validation → payment → handler, and the position is the feature: a request rejected by any of the first three has not been charged, because the payment middleware was never reached.

Idempotency is opt-in and in-process:

await serve(agent, { idempotency: { windowMs: 10 * 60_000 } });

The guarantee is exactly "a completed 2xx is replayed, not re-run". It is not "a dropped connection can never be charged twice" — a request that settled and then lost its socket before writing a response releases its claim, and the retry does settle again.

Attribution

Every settled call carries the transaction that paid for it. A running agent keeps the last calls in a ring buffer:

curl -s https://your-host/_ripar/runs | jq '.runs[] | {endpoint, status, ms, txId}'

That is in-memory and capped (100 by default), so it is an operational view, not an accounting record. For accounting, read the chain: the MCP server's ripar_settlements lists real USDC transfers for an address from the Algorand indexer.

Reporting a vulnerability

Email security@ripar.io. Please include reproduction steps and give a reasonable window before disclosure.

What this does not cover

Being honest about the boundary:

  • Endpoint quality. Anyone can serve an x402 endpoint. Nothing audits it, and there is no ranking system that would filter one out. Validate what you get back.
  • On-chain reputation. A credit requires the settling USDC transfer to be a transaction in the same atomic group, sent from the client's registered address to the server's — both resolved through the IdentityRegistry. So the amount is read off a transfer the AVM has already validated and cannot be fabricated. What a score still does not tell you is whether the work was any good: it counts money that moved, not results anyone judged. A validator verdict is a separate field.
  • Prompt injection. If your agent feeds untrusted text to an LLM that can call paid endpoints, maxPrice bounds the damage per call — it does not prevent the call.
  • Your own handler. Code you run holds the secrets you give it.