Build
Private agents
Give an AI agent its own private balance with hard spending limits. Connect through the umbr-mcp server or call the SDK directly.
- MCP server
- umbr-mcp (preview)
- Transport
- stdio today
- Limits
- Per payment and per day
- Networks
- Testnets only
Preview
Why agents need privacy#
An agent that pays for things leaves a trail. If it pays from a public wallet, anyone can read what it bought, from whom, how often and for how much, and from that infer what its owner is working on. A research agent paying for data sources gives away the research. A procurement agent gives away its suppliers and prices.
With UMBR, the agent holds a private balance and pays from it. Observers see pool interactions, not the vendor, the amount or the pattern. Hard limits sit next to the balance, so a confused or manipulated agent cannot spend beyond what you allowed.
Two ways in#
| Path | Best for | Status |
|---|---|---|
| MCP server (umbr-mcp) | Agents in an MCP client: a desktop assistant, an IDE, an agent framework with MCP support | Preview, run locally |
| SDK (@umbr/sdk) | Agents you write yourself, where your code decides when to pay | Preview |
Both use the same engine: proofs are made on the machine the agent runs on, and the same limits apply.
Connect by URL#
There is no hosted UMBR MCP endpoint today. Today you run umbr-mcp locally, as shown in the next section. A remote endpoint over Streamable HTTP is planned. When it exists, adding it will look like any other remote MCP server, a config entry with a url:
{
"mcpServers": {
"umbr": {
"url": "https://mcp.example/umbr"
}
}
}A hosted server would hold the agent's key and make its proofs, which is a different trust model from running it yourself. We will document that trade-off when it ships rather than before.
The MCP server#
umbr-mcp runs as a local process over stdio. Your MCP client starts it with npx and passes its settings as environment variables:
{
"mcpServers": {
"umbr": {
"command": "npx",
"args": ["-y", "umbr-mcp"],
"env": {
"UMBR_NETWORK": "tempo-moderato",
"UMBR_AGENT_KEY": "<read from your secret store>",
"UMBR_LIMIT_PER_PAYMENT": "2.00",
"UMBR_LIMIT_PER_DAY": "50.00"
}
}
}
}| Variable | Meaning |
|---|---|
UMBR_NETWORK | tempo-moderato or robinhood-testnet |
UMBR_AGENT_KEY | The agent's own private key, injected from a secret store. Never paste a real key into a config file. |
UMBR_LIMIT_PER_PAYMENT | Largest single payment the server will make, as a decimal string |
UMBR_LIMIT_PER_DAY | Total the server will spend in a rolling day |
Fund the agent by sending test stablecoins to its public wallet and calling umbr_shield, or by sending privately to its umbr1q... address from your own balance.
Tools the server exposes#
| Tool | What it does | Moves funds |
|---|---|---|
umbr_address | Returns the agent's private umbr1q... address. | No |
umbr_balance | Syncs and returns private balances by asset. | No |
umbr_shield | Moves public stablecoins from the agent's wallet into its private balance. | Yes, within limits |
umbr_send | Sends a private payment to an UMBR address, with an optional memo. | Yes, within limits |
umbr_unshield | Withdraws to a public 0x address. | Yes, within limits |
umbr_create_link | Creates a claim link for someone without an UMBR address. | Yes, within limits |
umbr_payroll_run | Pays a list of recipients in one run. | Yes, within limits |
umbr_prove | Makes a scoped proof with an audience and expiry. | No |
umbr_verify | Checks a proof link or JSON and returns the statement. | No |
umbr_limits | Reports the limits and how much of today's allowance is left. | No |
Most MCP clients can require a human to approve each tool call. Keep approval on for every tool that moves funds until you trust the agent's behaviour.
An agent on the SDK#
If you write the agent loop yourself, call the SDK directly. Give the agent its own client with its own key and limits, and route anything over a limit back to a person:
import { createUmbrClient, networks, UmbrError } from "@umbr/sdk";
const agent = createUmbrClient({
network: networks.tempoModerato,
signer: process.env.UMBR_AGENT_KEY, // the agent's own key, never your treasury key
relayer: "default", // the agent's public wallet stays out of view
limits: { perPayment: "2.00", perDay: "50.00" },
});
export async function payVendor(vendorAddress: string, amount: string, memo: string) {
try {
return await agent.send({ to: vendorAddress, asset: "pathUSD", amount, memo });
} catch (err) {
if (err instanceof UmbrError && err.code === "LIMIT_EXCEEDED") {
return { needsApproval: true, amount }; // hand it back to a human
}
throw err;
}
}Spending limits#
- Per payment caps any single send, unshield, claim link or payroll run.
- Per day caps the total over a rolling 24 hours.
- Limits are checked before any proof is made. A call over a limit throws
LIMIT_EXCEEDEDand nothing is signed or submitted. - Limits live in the client, not the pool contract. They stop the agent's own code and prompts; they do not stop someone who has the key and uses different software. The real ceiling is the balance you give the agent.
Fund it like a wallet, not a bank account
Paying for HTTP requests#
Some APIs charge per request using HTTP status 402 Payment Required. An UMBR agent can answer that from its private balance. This is an integration pattern, not a shipped feature: neither the SDK nor umbr-mcp handles 402 for you yet, and the header name below is an example.
Request
The agent calls the API as usual.402 response
The server replies 402 with the asset, the amount and an UMBR address to pay.Private payment
The agent sends the amount from its private balance, within its limits.Retry with proof
The agent makes a payment proof addressed to that server and retries with it in a header.Server verifies
The server checks the proof with verifyProof, confirms the amount and audience, and serves the response.
import { verifyProof } from "@umbr/sdk";
// Integration pattern, not a shipped API.
async function paidFetch(url: string, init: RequestInit = {}) {
const first = await fetch(url, init);
if (first.status !== 402) return first;
// The server says what it wants: asset, amount and an UMBR address.
const { asset, amount, payTo } = await first.json();
const { paymentId } = await agent.send({ to: payTo, asset, amount, memo: url });
const proof = await agent.prove({
claim: "payment",
payment: paymentId,
reveal: "exact",
audience: new URL(url).host,
expiresIn: "1d",
});
return fetch(url, {
...init,
headers: { ...init.headers, "x-umbr-payment": proof.link },
});
}
// On the server: const r = await verifyProof(req.headers["x-umbr-payment"]);
// then check r.valid, !r.expired, r.audience and r.statement before serving.The server should also remember which payments it has already accepted, so one proof cannot buy two responses.
Pay only for results that pass
Keys and custody#
- The agent's key is the agent's money. Generate a fresh key for each agent; never reuse yours.
- Load it from a secret manager at start-up. Do not commit it, log it or put it in a prompt.
- The key never leaves the process running umbr-mcp or the SDK. No UMBR service holds it, which also means no one can recover it for you.
- Back up the note store (
./.umbrby default in Node) and the key together. To retire an agent, unshield or send its balance back to you, then destroy the key.
What it does not do#
- It does not judge whether a payment is a good idea. Limits bound the damage; they do not supply judgement.
- It does not hide the agent's IP address or timing from the RPC provider or the relayer.
- Shielding and unshielding are public. An agent that shields right before each payment gets little privacy.
- There is no hosted MCP endpoint and no built-in 402 handling yet.
- It runs on testnets only. Nothing here should hold real funds. See Product status.