start
examples
Four runnable examples against the live sandbox: an MCP client setup, a TypeScript agent paying an API bill, a Python agent buying ad credits with human approval, and an admin script that sets a mandate.
nicolasfinance/nostro-examples is coming soon. Until then, the full code of each example is on this page. Every example reads its key from an environment variable: get one at nostro.ag/sandbox.- 01 · MCP client config + prompts · MCP
- 02 · TypeScript agent pays an API bill · MCP
- 03 · Python agent buys ad credits · REST
/v1/agent - 04 · admin: mandate, funding, approval · REST
/api
01 · MCP client config + prompts
Claude Code, in one command:
export NOSTRO_AGENT_KEY=nst_...
claude mcp add --transport http nostro https://mcp.nostro.ag/mcp \
--header "Authorization: Bearer $NOSTRO_AGENT_KEY"Then ask, in order:
- What is my Nostro budget today? calls
get_wallet. - Pay 12 € to Linear for our November seat (MCC 5734). settles inside the mandate.
- Pay 900 € to Linear. is denied: above the 500 € per-payment cap.
- Pay 300 € to Google Ads for ad credits. returns
pending_approval(human above 250 €). - Show my last five operations. calls
list_my_activity.
To check the server without a client, test.sh sends the same JSON-RPC calls (curl and jq):
#!/usr/bin/env bash
# Smoke test for the Nostro MCP server, the same JSON-RPC calls an MCP client (Claude Code, Cursor...) makes.
# NOSTRO_AGENT_KEY=nst_... ./test.sh
# Needs curl and jq.
set -euo pipefail
: "${NOSTRO_AGENT_KEY:?Set NOSTRO_AGENT_KEY (nst_...). Get one free at https://nostro.ag/sandbox}"
URL="${NOSTRO_MCP_URL:-https://mcp.nostro.ag/mcp}"
rpc() { # method params
curl -sS "$URL" \
-H "Authorization: Bearer $NOSTRO_AGENT_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":$2}"
}
tool() { rpc tools/call "{\"name\":\"$1\",\"arguments\":$2}" | jq -r '.result.content[0].text' | jq -c "${3:-.}"; }
echo "# initialize"
rpc initialize '{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test.sh","version":"1"}}' | jq -c '.result.serverInfo'
echo "# tools/list"
rpc tools/list '{}' | jq -c '[.result.tools[].name]'
echo "# get_wallet"
tool get_wallet '{}' '{agent:.agent.name, budget}'
echo "# pay 12 EUR to Linear"
tool pay '{"amount":12,"merchant":"Linear","mcc":"5734","description":"Linear seat, November"}'
echo "# pay 900 EUR to Linear (over the per-payment cap)"
tool pay '{"amount":900,"merchant":"Linear","mcc":"5734"}'# initialize
{"name":"nostro","version":"0.1.0"}
# tools/list
["get_wallet","pay","send_transfer","list_my_activity","get_request_status"]
# get_wallet
{"agent":"demo-agent","budget":{"currency":"EUR","perTxMax":500,"approvalAbove":250,"dailyRemaining":1615,"monthlyRemaining":4615}}
# pay 12 EUR to Linear
{"status":"paid","transactionId":"vtx_cc7f5f80e433","amount":12,"merchant":"Linear"}
# pay 900 EUR to Linear (over the per-payment cap)
{"status":"denied","reason":"Dépasse le plafond par transaction (500 €)"}02 · TypeScript agent pays an API bill
Uses the official @modelcontextprotocol/sdk client over streamable HTTP. Reads the wallet, checks the bill fits the mandate, pays, prints its activity.
npm install @modelcontextprotocol/sdk tsx
export NOSTRO_AGENT_KEY=nst_...
npx tsx agent.ts// A TypeScript agent that pays an API bill through Nostro, over MCP, inside its mandate.
//
// NOSTRO_AGENT_KEY=nst_... npx tsx agent.ts
//
// Flow: list the tools, read the wallet, check the bill fits the budget, pay, then read the activity.
// The policy is enforced server side: the agent cannot spend more than its mandate, whatever this code does.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const KEY = process.env.NOSTRO_AGENT_KEY;
const URL_ = process.env.NOSTRO_MCP_URL ?? "https://mcp.nostro.ag/mcp";
if (!KEY?.startsWith("nst_")) {
console.error("Set NOSTRO_AGENT_KEY to an agent key (nst_...). Get one free at https://nostro.ag/sandbox");
process.exit(1);
}
// The bill this agent has to settle. 5734 = computer software stores (MCC).
const bill = { amount: Number(process.env.BILL_AMOUNT ?? 42.5), merchant: "OpenAI API", mcc: "5734", description: "Monthly API usage, invoice 2026-10" };
const client = new Client({ name: "billing-agent", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL(URL_), {
requestInit: { headers: { Authorization: `Bearer ${KEY}` } },
}));
// MCP tool results come back as a JSON string in the first text block.
async function call<T = any>(name: string, args: Record<string, unknown> = {}): Promise<T> {
const r: any = await client.callTool({ name, arguments: args });
const data = JSON.parse(r.content?.[0]?.text ?? "{}");
if (r.isError) throw new Error(`${name}: ${data.error ?? "error"}`);
return data as T;
}
const eur = (n: unknown) => (n == null ? "-" : `€${Number(n).toFixed(2)}`);
const { tools } = await client.listTools();
console.log("tools:", tools.map((t) => t.name).join(", "));
const wallet = await call("get_wallet");
console.log(`wallet: ${wallet.agent.name} · card ${wallet.card.maskedNumber} · ` +
`per payment ≤ ${eur(wallet.budget.perTxMax)} · today ${eur(wallet.budget.dailyRemaining)} left · ` +
`human approval above ${eur(wallet.budget.approvalAbove)}`);
if (bill.amount > wallet.budget.perTxMax || bill.amount > wallet.budget.dailyRemaining) {
console.log(`bill of ${eur(bill.amount)} does not fit the mandate, escalating to a human instead of paying.`);
await client.close();
process.exit(0);
}
const res = await call("pay", bill);
switch (res.status) {
case "paid":
console.log(`paid ${eur(res.amount)} to ${res.merchant} · transaction ${res.transactionId}`);
break;
case "pending_approval":
console.log(`waiting for a human: request ${res.requestId} (${res.reason})`);
break;
default:
console.log(`not paid: ${res.status} · ${res.reason}`);
}
const activity = await call("list_my_activity", { limit: 3 });
console.log("last activity:");
for (const e of activity) console.log(` ${e.created_at} ${e.type.padEnd(13)} ${eur(e.amount).padStart(9)} ${(e.counterparty ?? "").padEnd(12)} ${e.decision}`);
await client.close();tools: get_wallet, pay, send_transfer, list_my_activity, get_request_status
wallet: demo-agent · card 4970 •••• •••• 2007 · per payment ≤ €500.00 · today €1603.00 left · human approval above €250.00
paid €42.50 to OpenAI API · transaction vtx_f7f8cd0c97a4
last activity:
2026-11-05T08:19:46.251Z card_payment €42.50 OpenAI API executed
2026-11-05T08:19:38.125Z card_payment €900.00 Linear denied
2026-11-05T08:19:36.873Z card_payment €12.00 Linear executed03 · Python agent buys ad credits
For agents without MCP. Above the approval threshold the API answers pending_approval; the agent polls the request until a human decides, and never retries or splits the amount.
pip install requests
export NOSTRO_AGENT_KEY=nst_...
python buy_ad_credits.py 300"""Buy ad credits through Nostro's REST API (/v1/agent) and handle human approval.
pip install requests
NOSTRO_AGENT_KEY=nst_... python buy_ad_credits.py 300
An ad-buying agent tops up its ad account. Small top-ups settle at once; anything above the
agent's approval threshold comes back as `pending_approval`, and the agent waits for a human
(approve it from the admin console, see ../04-admin-mandate) instead of retrying or splitting
the amount.
"""
import os
import sys
import time
import requests
BASE = os.environ.get("NOSTRO_API_URL", "https://mcp.nostro.ag")
KEY = os.environ.get("NOSTRO_AGENT_KEY", "")
POLL_SECONDS = int(os.environ.get("POLL_SECONDS", "5"))
TIMEOUT_SECONDS = int(os.environ.get("TIMEOUT_SECONDS", "600"))
if not KEY.startswith("nst_"):
sys.exit("Set NOSTRO_AGENT_KEY to an agent key (nst_...). Get one free at https://nostro.ag/sandbox")
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
def api(method: str, path: str, **kw) -> dict:
r = session.request(method, f"{BASE}/v1/agent{path}", timeout=30, **kw)
body = r.json()
if r.status_code >= 400:
raise RuntimeError(f"{r.status_code} {body.get('error', body)}")
return body
def main(amount: float) -> int:
wallet = api("GET", "/wallet")
budget = wallet["budget"]
print(f"agent {wallet['agent']['name']} · per payment ≤ €{budget['perTxMax']:.2f} · "
f"approval above €{budget['approvalAbove']:.2f} · month left €{budget['monthlyRemaining']:.2f}")
# 7311 = advertising services (MCC)
res = api("POST", "/pay", json={
"amount": amount,
"merchant": "Google Ads",
"mcc": "7311",
"description": "Ad credits for the November search campaign",
})
print(f"pay €{amount:.2f} to Google Ads -> {res['status']}" + (f" ({res.get('reason')})" if res.get("reason") else ""))
if res["status"] == "paid":
print(f"settled · transaction {res['transactionId']}")
return 0
if res["status"] != "pending_approval":
print("refused by the mandate, not retrying")
return 1
req_id = res["requestId"]
print(f"waiting for a human to approve {req_id} (polling every {POLL_SECONDS}s)")
deadline = time.time() + TIMEOUT_SECONDS
while time.time() < deadline:
time.sleep(POLL_SECONDS)
r = api("GET", f"/requests/{req_id}")
if r["status"] in ("pending", "deciding"):
continue
result = r.get("result") or {}
print(f"request {req_id} -> {r['status']}" + (f" · transaction {result.get('transactionId')}" if result.get("transactionId") else ""))
return 0 if r["status"] == "executed" else 1
print("no decision before timeout, leaving the request pending")
return 2
if __name__ == "__main__":
sys.exit(main(float(sys.argv[1]) if len(sys.argv) > 1 else 300.0))agent demo-agent · per payment ≤ €500.00 · approval above €250.00 · month left €4915.00
pay €300.00 to Google Ads -> pending_approval (Au-dessus du seuil d'approbation (250 €))
waiting for a human to approve req_fca22be1 (polling every 3s)
request req_fca22be1 -> executed · transaction vtx_db6da67fbbbc04 · admin: mandate, funding, approval
The company side, with the admin key: create an ads agent restricted to MCC 7311, fund the account, let the agent spend, approve what waits, print the audit trail. Node 18+, no dependencies. node admin.mjs approve only approves pending requests, which pairs with example 03.
export NOSTRO_ADMIN_KEY=nsa_...
node admin.mjs// Admin script: create an agent with a spending mandate, fund the account, approve what waits for a human.
//
// NOSTRO_ADMIN_KEY=nsa_... node admin.mjs # setup + demo + approve
// NOSTRO_ADMIN_KEY=nsa_... node admin.mjs approve # only approve pending requests (pairs with ../03)
//
// Node 18+, no dependencies. Uses the admin REST API (/api). The same actions exist as MCP tools
// (create_agent, fund_account, list_pending_approvals, approve_request) for an admin MCP client.
const BASE = process.env.NOSTRO_API_URL ?? "https://mcp.nostro.ag";
const ADMIN = process.env.NOSTRO_ADMIN_KEY ?? "";
if (!ADMIN.startsWith("nsa_")) {
console.error("Set NOSTRO_ADMIN_KEY to a workspace admin key (nsa_...). Get one free at https://nostro.ag/sandbox");
process.exit(1);
}
async function req(path, { method = "GET", body, key = ADMIN } = {}) {
const r = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
const data = await r.json();
if (!r.ok) throw new Error(`${method} ${path} -> ${r.status} ${data.error ?? ""}`);
return data;
}
const eur = (n) => `€${Number(n).toFixed(2)}`;
async function approveAll() {
const pending = await req("/api/requests?status=pending");
if (!pending.length) console.log("no pending approvals");
for (const p of pending) {
const what = p.payload?.merchant ?? p.payload?.beneficiaryName ?? p.kind;
const done = await req(`/api/requests/${p.id}/approve`, { method: "POST" });
console.log(`approved ${p.id} · ${p.agent_name} · ${eur(p.amount)} to ${what} -> ${done.status}`);
}
}
if (process.argv[2] === "approve") {
await approveAll();
process.exit(0);
}
// 1. A mandate for a paid-media agent: ads only, €300 per payment, human above €150.
const { agent, apiKey } = await req("/api/agents", {
method: "POST",
body: {
name: `ads-buyer-${Date.now().toString(36).slice(-4)}`,
purpose: "Tops up ad accounts for the growth team",
policy: { perTxMax: 300, dailyMax: 600, monthlyMax: 3000, approvalAbove: 150, allowedMccs: ["7311"] },
},
});
const p = agent.policy;
console.log(`created ${agent.id} (${agent.name}) · card ${agent.card.maskedNumber}`);
console.log(` mandate: ≤ ${eur(p.perTxMax)}/payment · ${eur(p.dailyMax)}/day · ${eur(p.monthlyMax)}/month · human above ${eur(p.approvalAbove)} · mcc ${p.allowedMccs.join(",")}`);
console.log(` agent key (shown once): ${apiKey.slice(0, 8)}…`);
// 2. Fund the account (sandbox: fake euros).
await req("/api/fund", { method: "POST", body: { amount: 2000 } });
const ov = await req("/api/overview");
console.log(`funded ${eur(2000)} · balance ${eur(ov.account.available.value)} ${ov.account.available.currency}`);
// 3. The agent spends with its own key: one inside the mandate, one that needs a human, one off-mandate.
for (const t of [
{ amount: 80, merchant: "Meta Ads", mcc: "7311" },
{ amount: 240, merchant: "Google Ads", mcc: "7311" },
{ amount: 45, merchant: "Steam", mcc: "5816" },
]) {
const r = await req("/v1/agent/pay", { method: "POST", key: apiKey, body: { ...t, description: "demo" } });
console.log(`agent pays ${eur(t.amount)} to ${t.merchant} -> ${r.status}${r.reason ? ` (${r.reason})` : ""}`);
}
// 4. A human approves what is waiting.
await approveAll();
// 5. Audit trail for this agent.
const log = await req(`/api/events?agentId=${agent.id}&limit=10`);
console.log("audit:");
for (const e of log.reverse()) console.log(` ${e.type.padEnd(14)} ${(e.amount == null ? "-" : eur(e.amount)).padStart(8)} ${(e.counterparty ?? "").padEnd(11)} ${e.decision}`);created agt_4db1278d (ads-buyer-xsg1) · card 4970 •••• •••• 7424
mandate: ≤ €300.00/payment · €600.00/day · €3000.00/month · human above €150.00 · mcc 7311
agent key (shown once): nst_NtIZ…
funded €2000.00 · balance €11915.00 EUR
agent pays €80.00 to Meta Ads -> paid
agent pays €240.00 to Google Ads -> pending_approval (Au-dessus du seuil d'approbation (150 €))
agent pays €45.00 to Steam -> denied (Catégorie marchand 5816 non autorisée)
approved req_184e730d · ads-buyer-xsg1 · €240.00 to Google Ads -> executed
audit:
agent_created - executed
card_payment €80.00 Meta Ads executed
card_payment €240.00 Google Ads pending_approval
card_payment €45.00 Steam denied
card_payment €240.00 Google Ads executed