veyra-mcp
Give your AI agent a wallet with a spending limit.
MCP server + typed client for Veyra: non-custodial USDC payments on Base, with per-payment caps, daily caps, human approval bands and a recipient allowlist — all enforced server-side, never by the model.
The problem
Agents increasingly need to pay for things: an API call, a dataset, a bounty, another agent. Today the choices are bad:
- Hand the agent a private key. One prompt injection away from an empty wallet.
- Hand it a card. No per-call limits, no allowlist, and your bank will not tell you which tool call spent the money.
- Approve every payment by hand. Then it is not really an agent.
Veyra is the layer in between. You connect a wallet you already own, set what an agent may spend, and hand the agent one MCP endpoint. The agent gets tools like create_payment. It never sees a key, and it cannot exceed the limits you set because the limits are enforced by Veyra's server, not by the model following instructions.
agent ──MCP──▶ veyra.money ──policy──▶ your wallet ──USDC on Base──▶ recipient
│
├─ under auto-max → executes from your capped on-chain allowance
├─ in the ask band → you approve in your own wallet (one link)
└─ over the limits → blocked, agent is told why
60-second setup
- Sign up at veyra.money, connect a wallet (or start with the simulated source, which moves no money).
- Create an MCP endpoint with limits. Copy the credential (it starts with
veyra_). - Add it to your host:
Claude Code
claude mcp add --transport http veyra https://veyra.money/api/mcp \
--header "Authorization: Bearer veyra_..."
Cursor (.cursor/mcp.json)
{
"mcpServers": {
"veyra": {
"url": "https://veyra.money/api/mcp",
"headers": { "Authorization": "Bearer veyra_..." }
}
}
}
OpenAI Codex
export VEYRA_TOKEN=veyra_...
codex mcp add veyra --url https://veyra.money/api/mcp --bearer-token-env-var VEYRA_TOKEN
Claude Desktop and any host that cannot send a bearer header (claude_desktop_config.json)
{
"mcpServers": {
"veyra": {
"command": "npx",
"args": ["-y", "veyra-mcp"],
"env": { "VEYRA_TOKEN": "veyra_..." }
}
}
}
The veyra-mcp binary is a stdio bridge: it holds the credential locally and relays every call to the hosted endpoint. No policy lives in the bridge.
Anything else (plain JSON-RPC)
curl -s https://veyra.money/api/mcp \
-H "Authorization: Bearer veyra_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_budget","arguments":{}}}'
Then ask the agent to do something that costs money and watch the policy decide.
Tools the agent gets
| Tool | What it does |
|---|---|
get_capabilities | Rails, networks, assets, limits and the recipient allowlist in force right now |
get_budget | Daily limit, spent today, per-payment limit, auto and ask bands |
list_payment_sources | Masked sources and whether each can settle without a human |
create_payment | Create a payment intent. Policy is applied server-side and the result says what happened |
get_payment | Canonical state for one payment, including which rail settled it |
list_payments | Recent payments for this endpoint |
cancel_payment | Cancel while still cancellable |
Every payment reports one of these statuses:
| Status | Meaning |
|---|---|
confirmed | Value moved on-chain |
confirmed_simulated | Settled on the simulated rail. No money moved. The agent is told this explicitly |
awaiting_approval | In the ask band. next_action.url is a one-time link the owner opens in their own wallet |
submitted | Transaction sent, waiting for the chain receipt |
pending | Created, not yet evaluated or executed |
failed / cancelled / expired | Terminal. Blocked-by-policy payments are failed with a policy_reason |
Use it from code
npm install veyra-mcp
import {
VeyraClient,
VeyraToolError,
idempotencyKey,
movedRealMoney,
paymentOutcome,
} from "veyra-mcp";
const veyra = new VeyraClient({ token: process.env.VEYRA_TOKEN! });
const budget = await veyra.getBudget();
console.log(`spent ${budget.spent_today} of ${budget.daily_limit} USDC today`);
const payment = await veyra.createPayment({
amount: "1.25",
recipient: "0x1234…abcd", // USDC on Base
reason: "Weather API, 500 calls",
idempotency_key: idempotencyKey("weather"),
});
switch (paymentOutcome(payment)) {
case "settled":
// The only case where value actually moved.
console.log("paid —", (await veyra.getPayment(payment.payment_id)).tx_ref);
break;
case "simulated":
console.log("settled on the simulated rail; no money moved");
break;
case "awaiting_approval": {
console.log("owner must approve:", payment.next_action?.url);
const settled = await veyra.waitForPayment(payment.payment_id);
console.log(movedRealMoney(settled) ? "paid" : paymentOutcome(settled));
break;
}
case "blocked":
console.log("policy refused it:", payment.policy_reason);
break;
}
Policy refusals are not exceptions
This is the one thing worth reading twice. A payment the policy declines comes
back as an ordinary result with status: "failed" — the call did what it was
asked and the answer was no. Only a malformed argument, a bad credential or an
unknown id throws.
So try/catch is the wrong tool for detecting refusals: it misses every
blocked payment and reads it as a success. Use paymentOutcome, and use
movedRealMoney before telling anyone value moved — it returns false for a
simulated settlement even though the status begins with "confirmed".
Reserve catch for what genuinely failed:
try {
await veyra.createPayment({ /* … */ });
} catch (e) {
if (e instanceof VeyraToolError) {
console.log(e.code, e.message); // VALIDATION_ERROR, UNKNOWN_CREDENTIAL, NOT_FOUND
} else throw e;
}
Never retry because you are unsure
If you cannot tell whether a payment worked, read it — do not create
another. getPayment returns tx_ref once a real payment has settled, which
is both your confirmation and what you show the recipient as proof. Reusing the
same idempotency_key returns the original payment; a new key spends again.
Veyra also sets possible_duplicate_of when an identical payment to the same
recipient is already live.
VEYRA_TOOLS exports the tool definitions as JSON Schema, so you can register them with the Vercel AI SDK, LangChain, or plain function calling without a network round-trip. See veyra-examples for complete agents built with the Claude Agent SDK and the Vercel AI SDK.
What you are trusting
Worth being blunt, because this is money.
- Veyra never holds your funds. Payments go from your wallet to the recipient. There is no Veyra balance in the middle.
- Above your auto-approve limit nothing is delegated. You sign each transfer in your own wallet from the approval link. Veyra verifies the on-chain receipt matches the request.
- Within your auto-approve limit you grant a capped ERC-20 allowance to a relayer Veyra operates, the same
approveprimitive every DeFi app uses. The token contract enforces the cap. Veyra's policy engine decides the destination within that cap, so size the allowance like a float you could lose, and use the recipient allowlist when you know who the agent should be paying. Revoke it any time. - The simulated rail exists so you can wire everything up before touching real funds. It is labelled everywhere: status
confirmed_simulated,simulated: true. - Every decision is in the audit log, including limit changes and credential rotations.
Full details: How a payment actually moves and What you are trusting, exactly.
FAQ
Which networks and assets? USDC on Base. One stablecoin keeps the dollar limits exact without a price feed, and Base is where the agent-payment ecosystem already settles. More rails are on the roadmap; open an issue if you need one.
Does this work with x402 or pay-per-call APIs? Yes, in the sense that matters: the agent asks Veyra to pay the seller's address and gets a receipt. Veyra is the spending policy, not the protocol on the other end.
Can I self-host? Not today. This repo is the client and bridge; the policy engine is the hosted service at veyra.money. The wallet-approval path is fully non-custodial regardless.
What does it cost? There is a free plan. See pricing.
Where do I report a bad policy decision? From the dashboard, so support can see your audit log. This repo's issues are for the client and bridge.
Development
npm install
npm test # builds, then runs the suite against a local mock of the endpoint
See CONTRIBUTING.md and SECURITY.md.
Links
- Product: veyra.money · Docs · Pricing
- Examples: zapxlabs/veyra-examples
- MCP Registry name:
io.github.zapxlabs/veyra
MIT © Veyra
