docs
fren clé sandbox

démarrer

exemples

Quatre exemples exécutables sur la sandbox en ligne : configuration d'un client MCP, agent TypeScript qui règle une facture d'API, agent Python qui achète des crédits publicitaires avec approbation humaine, et script admin qui fixe un mandat.

Un dépôt public nicolasfinance/nostro-examples arrive bientôt. En attendant, le code complet de chaque exemple est sur cette page. Chaque exemple lit sa clé dans une variable d'environnement : obtenez-en une sur nostro.ag/sandbox.

01 · config client MCP + prompts

Claude Code, en une commande :

shell
export NOSTRO_AGENT_KEY=nst_...
claude mcp add --transport http nostro https://mcp.nostro.ag/mcp \
  --header "Authorization: Bearer $NOSTRO_AGENT_KEY"

Puis demandez, dans l'ordre :

  1. Quel est mon budget Nostro aujourd'hui ? appelle get_wallet.
  2. Paie 12 € à Linear pour notre siège de novembre (MCC 5734). est réglé dans le mandat.
  3. Paie 900 € à Linear. est refusé : au-dessus du plafond de 500 € par paiement.
  4. Paie 300 € à Google Ads pour des crédits publicitaires. renvoie pending_approval (humain au-delà de 250 €).
  5. Montre mes cinq dernières opérations. appelle list_my_activity.

Pour vérifier le serveur sans client, test.sh envoie les mêmes appels JSON-RPC (curl et jq) :

test.sh
#!/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"}'
sortie vérifiée, sandbox en ligne, 5 novembre 2026
# 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 · agent TypeScript qui règle une facture d'API

Utilise le client officiel @modelcontextprotocol/sdk en streamable HTTP. Lit le portefeuille, vérifie que la facture rentre dans le mandat, paie, affiche son activité.

shell
npm install @modelcontextprotocol/sdk tsx
export NOSTRO_AGENT_KEY=nst_...
npx tsx agent.ts
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();
sortie vérifiée, sandbox en ligne, 5 novembre 2026
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       executed

03 · agent Python qui achète des crédits publicitaires

Pour les agents sans MCP. Au-delà du seuil d'approbation, l'API répond pending_approval ; l'agent interroge la demande jusqu'à la décision humaine, sans jamais réessayer ni fractionner le montant.

shell
pip install requests
export NOSTRO_AGENT_KEY=nst_...
python buy_ad_credits.py 300
buy_ad_credits.py
"""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))
sortie vérifiée, sandbox en ligne, 5 novembre 2026
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_db6da67fbbbc

04 · admin : mandat, alimentation, approbation

Côté entreprise, avec la clé admin : créer un agent publicitaire limité au MCC 7311, alimenter le compte, laisser l'agent dépenser, approuver ce qui attend, afficher la piste d'audit. Node 18+, sans dépendance. node admin.mjs approve approuve seulement les demandes en attente, en complément de l'exemple 03.

shell
export NOSTRO_ADMIN_KEY=nsa_...
node admin.mjs
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}`);
sortie vérifiée, sandbox en ligne, 5 novembre 2026
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