# Mandate — a sandbox bank for AI agents

Mandate is a sandbox bank built for AI agents: it issues short-term lender notes, lends against them, and settles every payment over PayPal sandbox (or a simulated rail), with every operation exposed as a tool over REST, MCP, and OpenAPI.

**This is a sandbox.** Notes are not deposits and are not FDIC-insured; nothing here moves real money. See [Money rules](#money-rules) below.

**Watch the bank live:** [https://mandate-bank.onrender.com/console](https://mandate-bank.onrender.com/console) is the public, read-only mission control — the bank clock, KPIs, money flow, the PayPal API log (bodies redacted), the Risk Desk (open risk flags and a semantic search) and the ledger, loans, notes and agents grids. No PII is shown.

## Connect

Mandate exposes every tool over three transports that all share the same `{status, summary, reason_codes, data, next_actions, docs_url}` envelope and the same tool names:

- **MCP (Streamable HTTP)**: `https://mandate-bank.onrender.com/api/mcp/mcp`
- **REST**: `POST https://mandate-bank.onrender.com/api/v1/{tool}` (read-only tools also accept `GET` with the input as query parameters, e.g. `GET https://mandate-bank.onrender.com/api/v1/get_loan?loan_id=…`)
- **OpenAPI**: `https://mandate-bank.onrender.com/openapi.json`

You need no key at all for `register_agent` (and `get_bank_guide`) — see **Bootstrapping** below. Every other tool authenticates with `Authorization: Bearer <api_key>`, on both REST and MCP (the MCP header is sent the same way, on the HTTP transport).

### Claude Code

```sh
claude mcp add --transport http mandate https://mandate-bank.onrender.com/api/mcp/mcp --header "Authorization: Bearer <key>"
```

### Cursor

Add to `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "mandate": {
      "url": "https://mandate-bank.onrender.com/api/mcp/mcp",
      "headers": { "Authorization": "Bearer <key>" }
    }
  }
}
```

### Codex

Add to `~/.codex/config.toml`:

```toml
[mcp_servers.mandate]
url = "https://mandate-bank.onrender.com/api/mcp/mcp"

[mcp_servers.mandate.http_headers]
Authorization = "Bearer <key>"
```

### curl / REST

```sh
# Bootstrapping: register_agent needs no Authorization header.
curl -s -X POST https://mandate-bank.onrender.com/api/v1/register_agent \
  -H "Content-Type: application/json" \
  -d '{"name": "Example Agent", "payout_email": "agent@example.com"}'

# Or name the payout receiver by its PayPal account (payer) id instead of an email:
# use this when you were given an account id, or the account's email is unconfirmed.
curl -s -X POST https://mandate-bank.onrender.com/api/v1/register_agent \
  -H "Content-Type: application/json" \
  -d '{"name": "Example Agent", "payout_paypal_id": "ABCDEFGHJ2345"}'

# Every other call authenticates with the api_key register_agent returned.
curl -s https://mandate-bank.onrender.com/api/v1/get_status \
  -H "Authorization: Bearer <key>"
```

## Bootstrapping

An agent that has no `api_key` yet calls `register_agent` first — it is the only mutating tool with `auth: none`. Call it once per agent and store the returned `api_key` immediately: it is never shown again.

Over MCP, call `register_agent` with no `Authorization` header at all (point your client at `https://mandate-bank.onrender.com/api/mcp/mcp` without the header, or omit it for that one call). Over REST, `POST https://mandate-bank.onrender.com/api/v1/register_agent` with no `Authorization` header either. Every other tool then requires `Authorization: Bearer <api_key>` on every call.

## Quickstart

1. **`register_agent`** — create the agent and get an `api_key` and a `claim_url`. You start at tier 0: you can lend now; to borrow, give your human the `claim_url` (see [Claiming and KYA tiers](#claiming-and-kya-tiers)).
2. **`buy_notes`** — buy lender notes, funded by a PayPal checkout your human approves (you get back a `human_approval` next action with the `approve_url`), then `confirm_checkout` once they have approved.
3. **`check_eligibility`** — quote a loan's fee/APR/total before committing to anything (needs tier ≥ 1: claimed by your human, who has signed a mandate).
4. **`apply_for_loan`** — apply for the amount and term you checked, with `declarations: {"no_other_debt": true}` and `attest: true` (required — see [Applying for a loan](#applying-for-a-loan)); if `OFFERED`, keep the `offer_id` and `disclosure_hash`; if `DECLINED`, read `data.adverse_action`.
5. **`accept_offer`** — accept with the same `disclosure_hash` the offer returned; the loan disburses to your payout receiver (`payout_email`, or `payout_paypal_id` if that account's email is unconfirmed).
6. **Forward the repayment link, or call `repay_loan`** — `get_loan` returns an invoice `repayment_link` your client can pay directly, or call `repay_loan` yourself for a human-approved PayPal checkout, then `confirm_checkout`.

## Claiming and KYA tiers

Every agent that registers itself is `ACTIVE` at **KYA tier 0**. What you may do depends on your tier, which the bank computes from your facts every time (`get_status` → `data.kya`, with `borrow_blockers` saying what is still missing):

| Tier | Requirement | May lend | May borrow |
| --- | --- | --- | --- |
| T0 | unclaimed | yes (buy/redeem notes) | no — `apply_for_loan` → `CLAIM_REQUIRED` (403) with `data.claim_url` |
| T1 | claimed by a PayPal-verified human **and** a signed mandate | yes | within the mandate's limits, per loan ≤ 25000 cents |
| T2 | T1 + an approved KYA document + a registered Ed25519 key ([Signing requests](#signing-requests-optional-unlocks-tier-t2)) | yes | within the mandate's limits, per loan ≤ 50000 cents |

**What unlocks each tier:**

- **T0 → T1** takes two steps by your human: they claim you (below), then sign a mandate in their dashboard ([Mandates](#mandates)). `borrow_blockers` goes `claim_required` → `mandate_missing` → empty.
- **T1 → T2** takes one step by you and one by your human: you register an Ed25519 key with `register_public_key` (and sign every mutating call from then on), and your human uploads a KYA document (e.g. an operator registration) in their dashboard.

**Claiming — send your human the `claim_url`.** `register_agent` returns a `claim_url` (`https://mandate-bank.onrender.com/claim/<token>`, valid 7 days, single use). Ask your human to open <claim_url>: give them the link and say what it is for ("claim me as your agent so I can borrow within limits you set"). On that page they:

1. click **Verify with PayPal** and approve a $0.01 identity check in PayPal (it is never charged — the bank only reads which PayPal account approved it), which brings them back to the claim page;
2. click **Claim agent**;
3. open their dashboard (`https://mandate-bank.onrender.com/dashboard`) and sign your mandate.

You can't do these steps yourself and you don't need to poll: once your human tells you they are done, call `get_status` (`data.kya.tier` ≥ 1, `can_borrow: true`). Until you are claimed, `get_status` keeps showing your `claim_url`; `next_actions` carries it as a `human_approval` with `"who": "owner"`. If the 7 days ran out, any call that shows you the link issues a fresh one: `get_status` and `check_eligibility` are read-only tools, but while you are unclaimed they (like `apply_for_loan`'s `CLAIM_REQUIRED`) **rotate an expired claim link** — the old link stops working, so always pass on the latest `claim_url`. Treat the link like a password: give it only to your own human.

**Mandate:** once claimed, your human signs a mandate in their dashboard that sets your borrowing limits (see [Mandates](#mandates)). Until then, `check_eligibility` lists `mandate_missing` and `apply_for_loan` returns `MANDATE_REQUIRED` (403).

## Mandates

A **mandate** is the standing permission your human signs for you: the limits within which you may borrow on their behalf. Your human is the borrower of record. They sign it in their dashboard (a checkbox plus their typed full name; the bank stores the time, a hash of their IP and the sha256 of the signed terms). Each change is a new **version**; signing one replaces the previous version. Revoking it stops new borrowing; loans you already took are unaffected.

**What your human sets:**

| Limit | Meaning |
|---|---|
| `max_per_loan_cents` | the principal of any one loan |
| `max_total_owed_cents` | what your human may owe in total **across all their agents**: what they owe on every open loan + this loan's principal + fee |
| `max_fee_bps` | the loan's fee as basis points of its principal (500 = at most 5% of the principal; a new agent's grade D invoice-backed loans need about 600 — see [The newcomer path](#the-newcomer-path)) |
| `max_term_days` | the loan's term |
| `eligible_collateral` | `invoice_link` (you pass an `invoice` the bank collects) and/or `none` (unsecured) |
| `expires_at` | optional; after it the mandate is no longer in force |

Your per-loan ceiling is the smaller of your credit grade's limit (A 50000, B 30000, C 20000, D 10000 cents), your tier's cap (T1 25000, T2 50000 cents) and `max_per_loan_cents` (`check_eligibility` returns it as `effective_cap_cents`).

**What you see:** `check_eligibility` returns `data.mandate` — `mandate_id`, `version`, every limit, `principal_total_owed_cents` (what your human owes now), `total_owed_headroom_cents` (how much more principal + fee they may take on) and `per_loan_cap_cents`. Pass `collateral` to also check the collateral rule. An offer's `disclosure` names the `mandate_id` and `mandate_version` it was made under (both covered by `disclosure_hash`).

**Reason codes** (in `check_eligibility` reasons, and on a `DECLINED` `apply_for_loan`):

| Code | Meaning |
|---|---|
| `mandate_missing` | no mandate in force — ask your human to sign one (`apply_for_loan` → `MANDATE_REQUIRED`, 403) |
| `mandate_per_loan` | the amount is above `max_per_loan_cents` |
| `mandate_total_owed` | your human's total owed + this principal + fee would be above `max_total_owed_cents` |
| `mandate_fee` | the fee is above `max_fee_bps` of the principal |
| `mandate_term` | the term is above `max_term_days` |
| `mandate_collateral` | the collateral kind (invoice or none) is not in `eligible_collateral` |
| `tier_per_loan` | the amount is above your KYA tier's per-loan cap |

**At acceptance** the bank checks the mandate again (serialised with every other acceptance, so two of your human's agents can't both squeeze under `max_total_owed_cents`):

- `MANDATE_REVOKED` (409) with `mandate_revoked` — the mandate was revoked or expired; ask your human to sign a new one.
- `MANDATE_REVOKED` (409) with `mandate_superseded` — your human signed a new version after the offer; check eligibility and apply again.
- `MANDATE_LIMIT` (400) with the `mandate_*` / `tier_per_loan` reasons — the offer no longer fits (typically another of your human's agents borrowed meanwhile); `data` carries the current limits and headroom.

**Two clocks.** A mandate is a document your human signs, so its `expires_at` is **real calendar time**: it lapses at that wall-clock moment in every world, however far a world's simulated clock has been moved. Everything about loans — an offer's expiry, a loan's due date, accrual — runs on the world's **simulated clock** (the main world's runs at real speed; a private world's can be fast-forwarded). An expired mandate behaves like a revoked one: `mandate_missing` at check/apply, `MANDATE_REVOKED` (`mandate_revoked`) at accept.

## Credit score and underwriting

Every `check_eligibility` and `apply_for_loan` computes your **Agent Credit Score** (0–100) from the bank's own records — never from anything an AI writes — and records it with its component breakdown. `check_eligibility` shows your `score`, `grade`, the components and the price before you apply.

### Score components

| Component | Points | How you earn them |
|---|---|---|
| Identity | 0–25 | claimed by a verified human 10, a registered signing key 8, an approved KYA document 7 |
| Repayment history | 0–35 | 7 per loan of ≥ 1000 cents repaid on time (capped at 35); late repayments and charge-offs subtract |
| Tenure | 0–10 | 1 per 3 simulated days since you registered |
| Collateral | 0–20 | for an invoice-backed request: invoice ≥ 1.5× the amount 20, ≥ 1.1× 12, none 0 |
| Exposure | 0–10 | 10 − 10 × (what your human already owes ÷ their mandate's `max_total_owed_cents`), at least 0 |

### Grades and limits

| Grade | Score | Per-loan limit (cents) | Approval |
|---|---|---|---|
| A | 80–100 | 50000 | auto-approved up to the limit |
| B | 65–79 | 30000 | auto-approved up to the limit |
| C | 50–64 | 20000 | auto-approved up to the limit |
| D | 35–49 | 10000 | auto-approved up to 5000 cents; above that `manual_review_declined` |
| E | 0–34 | 0 | always declined (`insufficient_credit_history` with no repayment history, else `score_too_low`) |

Your effective per-loan cap is the smallest of your grade's limit, your tier's cap (T1 25000, T2 50000) and your mandate's `max_per_loan_cents`.

### The newcomer path

A brand-new, claimed agent has no repayment history, so an **unsecured** first loan scores 20 (grade E) and is declined with `insufficient_credit_history`. Start with a **small invoice-backed loan**: an invoice of at least **1.5×** the amount adds 20 points — 40, grade D — and grade D is auto-approved up to $50.00 (5000 cents). (An invoice of only 1.1× adds 12 — still grade E. The invoice must also cover principal + fee.) Every such loan of ≥ $10.00 repaid on time adds 7 points of history, which is how you climb to C and above.

**Your human's mandate must allow the grade D fee.** A grade D invoice-backed loan costs 5.8% of the principal for 7 days (58 cents on 1000) and up to 6% for 30 days, so the mandate's `max_fee_bps` must be at least **~6% (600 bps)** — below that every newcomer loan is declined with `mandate_fee` (the next step then names the mandate's current max fee and the fee you were quoted). The dashboard's mandate form defaults to 10%. A signed mandate never changes by itself: if your human signed one at a lower max fee (e.g. 5%), they must sign a new version with a higher one.

**The grade C route.** With a registered signing key (`register_public_key`) and an approved KYA document (tier 2: +15 identity points), the same 1.5× invoice-backed loan scores 55 — grade C — and costs 3.2% for 7 days (32 cents), which fits under a 5% max fee.

**Which score?** `get_status` shows your *base* score (no collateral — there is no application). `check_eligibility` with `invoice_amount_cents`, and every stored application (`credit_scores`, `score_history`, `adverse_action.score`), include the collateral points of that request, so an invoice-backed application can show a score up to 20 points higher than `get_status`.

### Pricing

The fee is the sum of four components, each in basis points (bps) of the principal for the whole term:

- **cost of funds** = round((base rate + utilization premium) bps a year × term_days ÷ 365);
- **expected loss** = PD × LGD — probability of default by grade, times loss given default: 60% unsecured, 35% when an invoice covers ≥ 1.1× the amount;
- **operations** = 50 bps;
- **capital** = 8% × PD.

**Base rate and utilization premium.** The bank's base rate starts at 400 bps a year and moves once per simulated day (a seeded walk of at most ±25 bps a day that pulls back toward 400, kept within 100–1200 bps). On top of it comes a **utilization premium**, measured at the moment you apply: utilization = loans outstanding ÷ (cash + loans outstanding). Up to 50% lent out the premium is 0; from there it rises linearly to +600 bps a year at 90% (e.g. +300 at 70%). Above 90% the bank makes no new loans: the application is declined with `pool_utilization_high`. `check_eligibility`'s quote and every offer's disclosure show both numbers (`base_rate_bps`, `utilization_premium_bps`; the disclosure's are covered by `disclosure_hash`). An offer keeps the terms it was quoted at, even if the rate moves before you accept.

**Treasury policy.** Even when the pool can fund a loan, the bank's treasury policy can decline it: `reserve_floor` if it would leave available cash (after any queued redemptions) below 15% of the notes the bank owes holders; `concentration_limit` if your human's outstanding principal across all their agents would exceed 30% of the bank's loans plus available cash; `daily_budget_exhausted` if the principal accepted today would exceed 40% of the available cash at the start of the simulated day. `accept_offer` checks all three again and answers `TREASURY_LIMIT` (409) with the reason if another acceptance used the room meanwhile — apply again later or for less.

| Grade | PD | Expected loss, unsecured (bps) | Expected loss, invoice ≥ 1.1× (bps) | Capital (bps) |
|---|---|---|---|---|
| A | 1% | 60 | 35 | 8 |
| B | 3% | 180 | 105 | 24 |
| C | 6% | 360 | 210 | 48 |
| D | 12% | 720 | 420 | 96 |

`fee_cents = max(25, ceil(principal_cents × Σ bps ÷ 10000))`, and `apr_bps = round(fee ÷ principal × 365 ÷ term_days × 10000)`. If the fee is above your mandate's `max_fee_bps`, the application is declined with `mandate_fee`. The disclosure flags an APR above 36% (some US states cap consumer APRs there); short, small loans have high APRs even when the fee is a few cents.

**Worked example — the newcomer loan** (at the starting base rate of 400 bps and a pool under 50% lent, so no premium): 1000 cents for 7 days against a 1500-cent invoice (1.5×), grade D:

| Component | bps |
|---|---|
| cost of funds: round((400 + 0) × 7 ÷ 365) | 8 |
| expected loss: 12% × 35% | 420 |
| operations | 50 |
| capital: 8% × 12% | 96 |
| **total** | **574** |

Fee = ceil(1000 × 574 ÷ 10000) = ceil(57.4) = **58 cents** ($0.58); APR = 30243 bps (302.43%, flagged). Your client pays the $15.00 invoice; the bank keeps principal + fee ($10.58) and pays you the remaining $4.42.

### Reason codes

The rules run in this order and every failure is collected; a decline lists the first 4 as its principal reasons. The text is fixed — it is exactly what `adverse_action.reasons[].text` says:

| Code | Text |
|---|---|
| `amount_out_of_range` | Amount requested is outside the range we lend ($10 to $500). |
| `term_out_of_range` | Term requested is outside the range we lend (7 to 30 days). |
| `claim_required` | Unable to verify identity: the agent is not claimed by a verified human. |
| `tier_per_loan` | Amount requested exceeds the per-loan limit for your verification tier. |
| `risk_review` | This agent is under risk review: borrowing is limited to tier T0 (no new loans) until the review clears. Lending is unaffected. |
| `mandate_missing` | No current signed mandate from your human authorizes borrowing. |
| `mandate_per_loan` | Amount requested exceeds the per-loan limit in your human's mandate. |
| `mandate_total_owed` | Total owed would exceed the limit in your human's mandate. |
| `mandate_fee` | The fee for this loan exceeds the maximum fee in your human's mandate. |
| `mandate_term` | Term requested exceeds the maximum term in your human's mandate. |
| `mandate_collateral` | The collateral offered is not allowed by your human's mandate. |
| `delinquent_principal` | Your human's mandate is frozen for new loans: a loan of one of their agents is seriously past due (substandard). |
| `sanctions_hit` | Unable to complete required sanctions screening of the applicant. |
| `insufficient_credit_history` | Insufficient credit history. |
| `score_too_low` | Agent Credit Score below our minimum (grade E). |
| `amount_exceeds_limit` | Amount requested exceeds the credit limit for your grade. |
| `manual_review_declined` | Amount requested is above the automatic-approval limit for your grade; manual review declined. |
| `insufficient_collateral` | Value of collateral is not sufficient: the invoice must cover at least 1.1× the amount. |
| `declaration_missing` | Required application declarations were not provided. |
| `open_loan_exists` | You already have an open loan with us. |
| `insufficient_liquidity` | The lending pool cannot fund this amount right now. |
| `pool_utilization_high` | The lending pool is more than 90% lent out; we are not making new loans until repayments or new note purchases come in. |
| `reserve_floor` | This loan would take the bank's cash below this world's reserve floor (a share of the notes it owes holders), after any queued redemptions; the exact floor is in this response's data. |
| `concentration_limit` | Your human's loans across all their agents would exceed this world's concentration limit; the exact cap is in this response's data. |
| `daily_budget_exhausted` | This world's lending budget for today is used up; the exact budget is in this response's data. |

### AI explanations are advisory

Decisions are deterministic: the score, the rules above and the price decide every application. The bank's AI loan officer only *explains* the decision in `data.explanation` (`source: "ai"`), using the same reasons; if it is unavailable, slow, or says anything outside policy (a reason that isn't on your notice, an approval that didn't happen, money that hasn't moved), a fixed template is used instead (`source: "template"`). Nothing it writes changes your status, reasons, limit or price — rely on `status`, `reason_codes` and `adverse_action.reasons`.

## Applying for a loan

`apply_for_loan` takes an application with sections A–H. Only E and H are required beyond the amount, term and purpose:

| Section | Field | |
|---|---|---|
| A identity | `identity: {operator, model, code_manifest_hash}` | optional; what you declare about yourself |
| B purpose | `purpose` | required, 5–200 chars |
| C amount & term | `amount_cents`, `term_days` | required |
| D collateral | `invoice: {client_email, amount_cents, due_in_days, description}` | optional; ≥ 1.1× the amount (≥ 1.5× scores best) |
| E declarations | `declarations: {no_other_debt: true}` | **required** — you attest you have no other outstanding debts |
| F consent | `consent_bank_records` | optional; consent to a check of bank records |
| G mandate | `mandate_version` | optional; the bank fills in your current mandate. Name the version you read to get `MANDATE_CHANGED` (409) if it changed |
| H attestation | `attest: true` | **required** — the application is accurate |

Example — every section filled in (a newcomer's first, invoice-backed loan):

```json
{
  "amount_cents": 1000,
  "term_days": 7,
  "purpose": "Buy materials for invoice INV-1042",
  "identity": { "operator": "Acme Ops", "model": "claude-sonnet", "code_manifest_hash": "sha256:9f2c…" },
  "invoice": { "client_email": "client@example.com", "amount_cents": 1500, "due_in_days": 7, "description": "INV-1042: website build" },
  "declarations": { "no_other_debt": true },
  "consent_bank_records": true,
  "mandate_version": 1,
  "attest": true
}
```

**Adverse action.** A `DECLINED` response is still HTTP 200 and carries `data.adverse_action`:

- `reasons` — at most four `{code, text}` principal reasons with the fixed text from [Reason codes](#reason-codes);
- `score`, `grade` and `score_factors` — the parts of your score that cost you the most points;
- `notice` — fixed text: "Notice of adverse action: Mandate Bank has declined this credit application. The principal reasons are listed in reasons (at most four). You have the right to a statement of specific reasons for this decision: call get_application with this application_id at any time to receive it again. The decision was made by fixed rules from your Agent Credit Score and the facts of this application; the explanation is informational and does not change the decision.";
- `explanation` — the loan officer's plain-language explanation (advisory; see [AI explanations are advisory](#ai-explanations-are-advisory)).

**Tell your human why.** When you are declined, tell your human the bank's principal reasons **word for word, in quotation marks** — the fixed text in `adverse_action.reasons[].text` (also repeated in the response's `summary`), e.g. `The bank declined: "Insufficient credit history."` — and only then explain them in your own words. This is the statement of specific reasons your human is entitled to, so don't paraphrase it away. Then give the next step — the `next_actions` and `explanation.next_steps` say what would change the outcome (often: a smaller loan backed by an invoice of ≥ 1.5× the amount). Don't retry the same application unchanged; it will be declined for the same reasons.

Every response — offered or declined — also carries `data.explanation` `{source, summary, reasons_explained, next_steps}`.

**Re-read it later** with `get_application` (`application_id`): `status` (`OFFERED` or `DECLINED`), the decision, reasons, adverse action, the latest explanation, the offer id and an echo of your application (`ala`). This is your statement of specific reasons, available at any time.

## Signing requests (optional, unlocks tier T2)

Signing is optional. A claimed agent with a signed mandate is T1; registering an Ed25519 key **and** having your human upload an approved KYA document (in their dashboard) makes you **T2** (per loan ≤ 50000 cents).

**How a request is signed.** Build this string (`\n` separated, no trailing newline):

```text
${timestamp}\n${METHOD}\n${path}\n${sha256hex(body)}
```

- `timestamp`: unix seconds; must be within **±300 s** of the bank's clock.
- `METHOD`: `POST`. `path`: the URL path only — `/api/v1/<tool>` over REST, `/api/mcp/mcp` over MCP.
- `body`: the **exact bytes** you send (over MCP, the whole JSON-RPC request body). Sign the string you send, not a re-serialisation of it.

Sign it with your Ed25519 private key and send:

| Header | Value |
| --- | --- |
| `X-Mandate-Timestamp` | the `timestamp` above |
| `X-Mandate-Signature` | base64 of the 64-byte Ed25519 signature |
| `X-Mandate-Previous-Signature` | key rotation only: the same string signed by your **current** key |

**Register your key** with `register_public_key` (`public_key`: base64 SPKI DER or raw 32 bytes). That request must itself be signed by the key you are registering. Calling it again with a different key rotates (the old key is revoked) and also needs `X-Mandate-Previous-Signature` from the current key.

**Once a key is registered, every mutating call must be signed** (read-only tools like `get_status` need not be). Each signature is single-use: to retry a call, sign it again with a fresh timestamp and reuse its `Idempotency-Key` so it can't run twice. Over MCP, send exactly **one** `tools/call` per signed HTTP request — the signature covers the whole JSON-RPC body, so JSON-RPC batches (an array of calls in one body) are not supported for signed requests.

| Status (HTTP 401) | Meaning |
| --- | --- |
| `SIGNATURE_REQUIRED` | you have a key, but the headers are missing (or `previous_signature_required` on rotation) |
| `SIGNATURE_INVALID` | the signature doesn't verify (wrong key, body, path, or malformed header) |
| `SIGNATURE_EXPIRED` | `X-Mandate-Timestamp` is more than 300 s off |
| `SIGNATURE_REPLAYED` | this exact signature was already used |

```js
// Node 20+
import { createHash, generateKeyPairSync, sign } from "node:crypto";

const { publicKey, privateKey } = generateKeyPairSync("ed25519"); // keep privateKey secret
const publicKeyB64 = publicKey.export({ format: "der", type: "spki" }).toString("base64");

async function signedPost(tool, args, apiKey) {
  const path = `/api/v1/${tool}`;
  const body = JSON.stringify(args);
  const timestamp = String(Math.floor(Date.now() / 1000));
  const digest = createHash("sha256").update(body).digest("hex");
  const signature = sign(null, Buffer.from(`${timestamp}\nPOST\n${path}\n${digest}`), privateKey).toString("base64");
  const res = await fetch(`https://mandate-bank.onrender.com${path}`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${apiKey}`,
      "x-mandate-timestamp": timestamp,
      "x-mandate-signature": signature,
    },
    body,
  });
  return res.json();
}

await signedPost("register_public_key", { public_key: publicKeyB64 }, process.env.MANDATE_API_KEY);
```

## Private worlds

The main world is the shared bank on PayPal sandbox. A **private world** is a separate, sandboxed copy of the bank that belongs to one human: simulated money only (the `sim` rail — no PayPal), with its own ledger, notes, loans and clock. Nothing in it shows up in the main world, and nothing from the main world shows up in it.

- **Who creates one:** a PayPal-verified human, in their dashboard (`https://mandate-bank.onrender.com/dashboard/worlds` → **Create a world**), up to 3 at a time. Agents can't create worlds.
- **What's inside:** four lender bots and six borrower bots (with their own mandate) that buy notes, borrow and repay once per simulated day, so the pool has liquidity and activity. Their funds are simulated. Lenders buy more when the real return is above 2% and redeem when it is below 0; each borrower is a good payer, a late payer (+3 days) or a defaulter. (A world created before the economy release keeps its two lenders and two borrowers.)
- **Stress scenarios:** the owner can trigger a default wave, a note run or a rate spike from the world page — see [The bank's economy](#the-banks-economy).
- **Clock controls:** the owner moves the world's clock **+1 day** or **+7 days** (the bots act for each day that passes, so terms, due dates and repayments play out in minutes) or **Reset**s it to a fresh world.
- **Agents in a private world:** the human registers the agent from their dashboard (`https://mandate-bank.onrender.com/dashboard/agents/new`, choosing the world). Its API key is **bound to that world**: every call made with it — over REST or MCP, at the same URLs as the main world — acts in that world only. To act in another world you need an agent registered there, with its own key. Such an agent is already claimed by that human; it can borrow once they sign its mandate.

- **Approvals in a private world:** checkouts there are simulated, so a `human_approval` `url` points at your human's own approval page (`https://mandate-bank.onrender.com/dashboard/worlds/<world_id>/checkouts/<checkout_id>/approve`) instead of PayPal. Hand it over exactly as in the main world: your human opens it while signed in to their dashboard, clicks **Approve** (no PayPal login, no money moves), and you then call `confirm_checkout`. Only the world's owner can approve; anyone else gets a 404.

Use a private world to rehearse (buy notes, borrow, let the term pass, repay) with simulated approvals, then do it for real in the main world.

## When you're late

Every loan has a due date on the world's simulated clock (`get_loan` → `due_sim_at`). If it isn't repaid by then, the bank walks it down a **delinquency ladder**, by how many simulated days it is past due. Each step happens exactly once, and each one writes a **notice** to you and to your human.

| Days past due | Step | What happens |
|---|---|---|
| +1 | `late` | Status `LATE`. A late fee of `min(500, max(100, 5% × outstanding principal))` cents is added. Your credit score takes a −15 entry. |
| +3 | `penalty` | A rate penalty of 2% a year on the outstanding principal accrues **daily**, counted from the due date: `ceil(outstanding principal × 200 × days_late / (10000 × 365))` cents in total. It stops growing at +10. |
| +7 | `substandard` | Status `SUBSTANDARD`. You become `restricted`: you can't apply for or accept loans, but you **can still repay** (`repay_loan`, `confirm_checkout`) and acknowledge notices. Your human's mandate is **frozen** for new loans across all their agents: `check_eligibility` / `apply_for_loan` give `delinquent_principal`, and `accept_offer` returns `MANDATE_FROZEN` (409). |
| +10 | `charged_off` | Status `CHARGED_OFF`. The bank writes the loan off: principal + fee + late fees + penalty at that moment. Note holders bear the principal loss pro rata to their notes. **Your human's guarantee is invoked:** the bank sends them a payment link (on their dashboard) for the full written-off amount. You stay restricted and the mandate stays frozen until it is recovered. |
| any time after | `recovered` | Any payment after charge-off — the guarantee, the invoice link, or `repay_loan` — is a **recovery**. Note holders are made whole first. Once the full written-off amount is back, the status is `RECOVERED`, your restriction lifts and the freeze ends. Anything paid beyond it is refunded to your payout receiver. |

**Where you see it.** `get_status` → `data.open_loan` carries `status`, `days_late`, `late_fee_cents`, `penalty_cents` and `total_due_cents` (what settles the loan now; once charged off, what is still recoverable, plus the `guarantee_link`). `data.notices` lists your **unread notices** — `{notice_id, kind, loan_id, sim_at, body}`, with `kind` one of `late`, `penalty`, `substandard`, `charged_off`, `recovery`, `recovered`, `cured` (and, for [repayment plans](#repayment-plans), `installment_missed`, `installment_rescheduled`, `installment_failed`). `get_loan` shows the same fields for one loan, its notices, and `charge_off` (`written_off_cents`, `recovered_cents`, `guarantee_link`). Check `get_status` whenever you resume work.

**What to do with a notice.** Read it, **tell your human what it means for them** — what is owed now, what the next step is and when, and that at +10 their guarantee is invoked — then call `ack_notices` (all unread, or `notice_ids`) so it stops being listed. `ack_notices` changes nothing about the loan; your human's dashboard keeps its own read state.

**Waterfall.** Every payment on a late loan is applied in this order: **late fees → rate penalty → fee → principal → remainder** (anything left over is refunded to your payout receiver).

**Quotes (pay exactly what you were quoted).** `repay_loan` without `amount_cents` quotes the full payoff (`total_due_cents`) at that moment. If your human pays that checkout for exactly the quoted amount, the loan is **cured** (`REPAID`) even if a late fee or more penalty landed between the quote and the capture: those post-quote charges are **waived**. A partial `amount_cents` (or the invoice link) carries no quote: it is applied through the waterfall and the penalty accrues up to when it is captured. The same rule holds after charge-off: a quoted payoff paid as quoted fully recovers the loan. **Small balances:** once the principal and fee are fully paid, late fees + penalty still owed of **100 cents or less** are waived and the loan is cured. **Cure:** paying everything owed before +10 returns the loan to `REPAID`, and you go back to `active` once you have no other substandard or charged-off loan.

**How collection works.** The bank never debits anyone automatically: every collection is a **payment link** a human approves — the invoice link your client pays, a `repay_loan` checkout your human approves, or, after charge-off, the guarantee link on your human's dashboard (`https://mandate-bank.onrender.com/dashboard`; in a private world it is their sim approve page). A saved-card auto-debit is not offered. Once a loan is cured or fully recovered, its other open payment links are `CLOSED`; anything still paid through one is applied and the excess refunded.

In a private world your human can move the clock (+1 / +7 days) to walk a loan through this ladder in minutes — see [Private worlds](#private-worlds).

## Risk review

The bank runs a **Risk Desk** on Elastic: its events, PayPal operations and loan applications are shipped (without any personal data — identities only as keyed hashes) to Elasticsearch, and deterministic ES|QL detectors check each world every hour. What triggers a flag on an agent:

| Flag | Trigger |
|---|---|
| `self_dealing` | An invoice link was paid from your own human's PayPal account, or one payer paid invoice links for agents of three or more unrelated humans. |
| `score_farming` | Three or more borrow → repay cycles, each shorter than one simulated day, within 7 simulated days. |
| `sybil_burst` | Four or more agents registered by the same human within 10 real minutes (every agent in the burst is flagged). |
| `probing` | Three or more declined applications, then a smaller application within one simulated day. |
| `velocity` | Note redemption requests today ≥ 3 and more than 3× your median of the previous 7 days. |

**Effect (deterministic).** While you have an open flag you borrow as if at **tier T0**: `apply_for_loan` declines with `risk_review` ("This agent is under risk review: borrowing is limited to tier T0 (no new loans) until the review clears. Lending is unaffected.") and `accept_offer` returns `RISK_REVIEW` (409); the next action is `get_status`, whose `data.risk_review` lists the open flags (`kind`, `reason`, `since_sim_at`, `expires_sim_at`). **Lending is unaffected** — buying, holding and redeeming notes work as usual — and loans you already have keep running normally.

**Investigation (advisory).** Each new flag gets a case file from the bank's AI risk investigator, a bounded multi-step agent that queries the Risk Desk with allowlisted ES|QL templates, semantic search over application purposes (Jina embeddings + Jina reranker) and your scrubbed profile, and recommends `clear`, `keep` or `escalate`. It decides nothing: the recommendation never changes the flag. (Private worlds use a fixed template, no AI.)

**How it resolves.**
- A flag **expires on its own 7 simulated days after its latest evidence** if nothing new turns up.
- Your human sees the flag, the reason and the case file in the agent's drawer on the dashboard (`https://mandate-bank.onrender.com/dashboard`) and can **Request review** with a note explaining why the activity is legitimate.
- Clearing a flag early is an **admin** action in the main world; in your human's own private world they are the admin and can clear it themselves.
- Tell your human about the flag when `get_status` shows one; there is nothing to retry until it clears.


## Repayment plans

You drive your own repayment schedule. Once your loan is `ACTIVE`, propose a plan with `plan_repayments`; the bank then watches your PayPal payments and **re-plans it automatically** on its tick. The plan is guidance and reminders: it never changes what you owe, the ledger, or the loan's `due_sim_at` — the late-fee ladder above still starts only after the due date.

**Proposing (`plan_repayments`).** 1 to 4 installments `{date, amount_cents}`, dates as `YYYY-MM-DD` sim days (UTC), strictly increasing, from today (sim) up to the loan's due date; the amounts must sum **exactly** to `total_due_cents` right now (`get_loan`). `auto_reschedule: true` lets the bank move a missed installment to the next sim day, **once**. One plan per loan: the same plan again is a replay; a different one is `PLAN_EXISTS` (409). A rule broken is `INVALID_PLAN` (400) with a reason code: `too_many_installments`, `invalid_date`, `dates_not_increasing`, `date_in_past`, `date_after_due`, `amount_not_positive`, `amounts_do_not_sum`.

```json
{ "auto_reschedule": true,
  "installments": [ { "date": "2026-11-10", "amount_cents": 400 },
                    { "date": "2026-11-12", "amount_cents": 400 },
                    { "date": "2026-11-14", "amount_cents": 258 } ] }
```

**Moving one (`reschedule_installment`).** `{installment: 1–4, date}`: the new date is at least **1 sim day ahead** (tomorrow or later) and **never after the loan's due date**; each installment can be moved at most **2 times** (the bank's auto-move doesn't count). A `MISSED` or `FAILED` installment you move is `PLANNED` again. Broken rules: `INVALID_PLAN` with `too_soon`, `date_after_due`, `move_limit`, `already_paid`, `same_date` or `invalid_date`.

**What the bank does on each tick** (deterministic, from payment events):

| Event | Effect on the plan |
|---|---|
| A payment lands (`repay_loan`, the invoice link) | Everything paid since the plan is applied to installments **in date order**: paying early marks them `PAID`; whatever is left over shifts onto the next installment (`paid_cents` > 0, still `PLANNED`). Late payments still count. |
| An installment's day ends uncovered | `MISSED` + an `installment_missed` notice. With `auto_reschedule` it is moved once to the next sim day (`installment_rescheduled` notice); a second miss stays `MISSED`. |
| A PayPal capture for your loan fails (denied/declined) | The first open installment is `FAILED` + an `installment_failed` notice (one failure, one installment). |
| A payment is refunded or reversed by PayPal | The installment it covered is no longer paid: `FAILED` + notice. |
| The loan is `REPAID` / `RECOVERED` | Every installment `PAID`. |

**Where you see it.** `get_loan` → `data.repayment_plan` and `get_status` → `data.open_loan.repayment_plan`: `{auto_reschedule, planned_total_cents, paid_cents, next_due: {installment, due_on, amount_left_cents}, installments: [{installment, due_on, original_due_on, amount_cents, paid_cents, status, moves, moves_left, auto_moved, paid_sim_at, missed_sim_at, failed_sim_at}]}`. Installment notices arrive with the others in `data.notices` (acknowledge them with `ack_notices`). Your human watches the same schedule live on their dashboard's **Timeline** (read-only); the public console's **Calendar** shows every loan's due date and installment.

## The bank's economy

Every world runs a small, seeded economy that moves once per simulated day. The same seed and the same actions give the same history.

- **Base rate:** starts at 400 bps a year; each day it takes a seeded step of at most ±25 bps and is pulled 10% of the way back toward 400, within 100–1200 bps. It is the bank's cost of funds for a new loan.
- **Inflation:** an index that starts at 1.0000 and grows daily at an annual rate that walks between 2% and 6%. `get_statement` shows your **real** return as a note holder: `real_annual_return_bps` = `nominal_annual_return_bps` − `inflation_rate_bps`.
- **Utilization premium:** utilization = loans outstanding ÷ (cash + loans outstanding), measured when you apply. Up to 50% the premium is 0; it then rises linearly to +600 bps a year at 90%; above that, applications are declined with `pool_utilization_high`. Your quote and disclosure carry `base_rate_bps` and `utilization_premium_bps`; an offer keeps its quoted terms.
- **Loss allowance:** each day the bank reserves for expected losses on open loans (outstanding principal × the probability of default and loss given default at your offer; twice that for late and substandard loans). A charge-off uses the loan's reserve first. The allowance is gross of note-holder loss sharing: holders still bear their share of a charge-off.

**Treasury gates.** A loan the pool could fund can still be declined by the treasury policy, with a fixed reason code (see [Reason codes](#reason-codes)):

| Reason code | When |
|---|---|
| `reserve_floor` | the loan would leave available cash (after queued redemptions) below the reserve floor — by default 15% of the notes the bank owes holders |
| `concentration_limit` | your human's outstanding principal across all their agents would exceed the concentration limit — by default 30% of the bank's loans plus available cash |
| `daily_budget_exhausted` | the principal accepted today would exceed the daily lending budget — by default 40% of the cash available at the start of the simulated day |
| `pool_utilization_high` | more than 90% of the pool is lent out |

The shared main world allows a larger daily budget (the whole day's available cash) and up to 50% for one human, because its pool is small; the reserve floor is the same. `accept_offer` re-checks the three treasury gates and answers `TREASURY_LIMIT` (409) with the reason if another acceptance used the room first. Apply again later, or for less.

**Redemption queue.** A `redeem_notes` that would take available cash below the reserve floor — or that arrives while others are already waiting — is not refused: it is `QUEUED`. `data.queue` gives your `position`, the `reason` (`reserve_floor` or `queue_ahead`) and roughly how much more cash the bank needs; `get_status` lists your `queued_redemptions`. The queue is checked every minute and paid strictly oldest first, FIFO, as cash allows — never more than the cash available — as repayments and note purchases come in; this runs for every world, including a private world whose clock is currently frozen. Your units stay reserved meanwhile and still share any charge-off loss. Call `cancel_redemption` (`note_tx_id` from the `QUEUED` response) to withdraw a queued one; a redemption already being paid can't be cancelled.

**Treasury memo.** Once a simulated day the bank's treasury officer writes a short advisory memo from these numbers: a headline, observations and risks. It changes nothing; every decision above is made by fixed rules.

**Stress scenarios (for humans, in private worlds).** On a private world's page (`/dashboard/worlds/<world_id>`) its owner can trigger:

- **Default wave:** the next 3 borrower-bot loans are never repaid, so they walk the late ladder to charge-off and note holders share the loss.
- **Note run:** every lender bot asks to redeem all its notes at once. Some are paid; the rest wait in the redemption queue.
- **Rate spike:** the base rate jumps +500 bps at the next simulated day, then eases back.

Each one is recorded on the world page with its parameters. Move the clock to watch the bank absorb it. Scenarios can't be triggered in the main world or in anyone else's world.

## Human approval

Several tools (`buy_notes`, `repay_loan`, `confirm_checkout`, `accept_offer`, `get_loan`) can return a `next_actions` entry shaped like:

```json
{
  "tool": "human_approval",
  "args": {
    "url": "https://www.sandbox.paypal.com/checkoutnow?token=...",
    "who": "payer",
    "then": { "tool": "confirm_checkout", "args": { "checkout_id": "..." } }
  }
}
```

`human_approval` is not a real tool you call — it is a signal: a human (`who` says which one — the `payer` funding a checkout, or the `client` paying an invoice) must open `url` and approve the PayPal checkout before anything else can happen. Tell your human exactly that: what they are approving and why, then show them the `url`. Once they confirm they approved it, call the tool named in `then.tool` with `then.args` (`confirm_checkout` for a checkout) to capture it. If the response comes back `PENDING` or `AWAITING_APPROVAL` again, that capture is still settling — call `confirm_checkout` again with the same `checkout_id`.

With `"who": "owner"` the `url` is not a checkout: it is your claim link or your owner's dashboard (to sign a mandate) — see [Claiming and KYA tiers](#claiming-and-kya-tiers). Show it to your human; once they are done, `then` is `get_status`.

## Developer tools: Postman, SDKs, no-human approvals

- **Postman:** [Run in Postman](https://god.gw.postman.com/run-collection/60016827-6e5e436c-eaa8-4c76-9df9-4f7f1a71cd58?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D60016827-6e5e436c-eaa8-4c76-9df9-4f7f1a71cd58%26entityType%3Dcollection%26workspaceId%3D81e993e5-c62f-4322-9df4-3fea36d9a08e) forks the public collection (folders *Start here, Lend, Borrow, Repay, Collections, Treasury*; set `apiKey` once — `register_agent` sets it for you). Raw collection: [https://mandate-bank.onrender.com/postman/collection.json](https://mandate-bank.onrender.com/postman/collection.json). Public workspace: https://www.postman.com/huzaifa-khan-inbox-6271629/mandate-paypal-ai-hackathon/collection/60016827-6e5e436c-eaa8-4c76-9df9-4f7f1a71cd58.
- **SDKs (generated by APIMatic from [/openapi.json](https://mandate-bank.onrender.com/openapi.json)):** TypeScript and Python — download them from [https://mandate-bank.onrender.com/sdks](https://mandate-bank.onrender.com/sdks). Each method is one tool; pass `Authorization: Bearer <api_key>` as the bearer token.
- **If you have no human handy (sandbox only):** a `human_approval` step is for a human. In the sandbox, your *operator* can approve a sandbox checkout from a [Kernel](https://www.kernel.sh) cloud browser with the repo's test helper (`tests/helpers/kernelApprove.ts`, `npm run test:agents -- --browser kernel`). Ask your operator — never automate approval yourself, and the bank never approves PayPal pages for you.

## Tool reference

Every tool below is generated from the live registry — this list, every description, and every schema are exactly what the server exposes right now.

<a id="get_bank_guide"></a>
### `get_bank_guide`

Returns an orientation guide to Mandate for agents: what the bank is, how to start, which tools exist, the money rules in force, and links to the full /agents.md and /llms.txt docs. Call this first, before register_agent, to confirm you're pointed at the right bank and see every available tool.

- **Auth:** none
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/get_bank_guide` (also `GET https://mandate-bank.onrender.com/api/v1/get_bank_guide`)
- **Group:** Start here · **Hints:** read-only, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

- **Example input** (Orientation: what the bank is, every tool, the money rules):

```json
{}
```

<a id="register_agent"></a>
### `register_agent`

Creates a new agent and an API key for it. Call this first, before any other tool — every other call authenticates with the returned api_key. Creates a fresh agent every time it's called (no dedupe), so call it once per agent and store the key immediately: it is never shown again. The agent starts ACTIVE at KYA tier 0: it can lend (buy/redeem notes) but not borrow. Give your human the returned claim_url (valid 7 days; get_status shows it again) — once they claim you and sign a mandate, you can borrow.

- **Auth:** none
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/register_agent`
- **Group:** Start here · **Hints:** mutating
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 60
    },
    "operator": {
      "type": "string"
    },
    "model": {
      "type": "string"
    },
    "payout_email": {
      "description": "The PayPal account (by email) that receives loans and redemptions. Give this or payout_paypal_id.",
      "type": "string",
      "format": "email",
      "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "payout_paypal_id": {
      "description": "The PayPal account (by payer ID) that receives loans and redemptions, instead of payout_email — use it when that account's email is unconfirmed (payouts to an unconfirmed email go UNCLAIMED).",
      "type": "string",
      "pattern": "^[A-Z0-9]{13}$"
    }
  },
  "required": [
    "name"
  ],
  "additionalProperties": false
}
```

- **Example input** (Register with a PayPal payout account (no API key needed)):

```json
{
  "name": "Acme Ops Agent",
  "operator": "Acme Ops",
  "model": "claude-sonnet",
  "payout_email": "agent@example.com"
}
```

<a id="get_status"></a>
### `get_status`

Returns the calling agent's own account snapshot: status, KYA tier (and the claim_url to give your human while you are unclaimed), note and payable balances, any open loan, lending limits, your Agent Credit Score (0–100, grade A–E, grade limit, top 3 factors), unread notices (late fees, penalty, substandard, missed installments — acknowledge with ack_notices), your repayment plan (open_loan.repayment_plan), and the current simulated time. Call it any time to decide what to do next — after registering, after buying notes, or while waiting on a loan.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/get_status` (also `GET https://mandate-bank.onrender.com/api/v1/get_status`)
- **Group:** Start here · **Hints:** read-only, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

- **Example input** (Your agent, KYA tier, mandate, positions and open items):

```json
{}
```

<a id="register_public_key"></a>
### `register_public_key`

Registers the calling agent's Ed25519 request-signing key (optional; together with an approved KYA document it unlocks KYA tier T2). The request itself must be signed by the key being registered (X-Mandate-Timestamp + X-Mandate-Signature, see 'Signing requests' in agents.md). Once a key is registered, every mutating call from this agent must be signed by it (unsigned → SIGNATURE_REQUIRED). Registering a different key rotates: also send X-Mandate-Previous-Signature from the current key; the old key is revoked.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/register_public_key`
- **Group:** Start here · **Hints:** mutating, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "public_key": {
      "type": "string",
      "minLength": 1,
      "maxLength": 512,
      "description": "Your Ed25519 public key, base64: the raw 32 bytes or the SPKI DER encoding."
    }
  },
  "required": [
    "public_key"
  ],
  "additionalProperties": false
}
```

- **Example input** (Register an Ed25519 public key (base64) to sign requests):

```json
{
  "public_key": "MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE="
}
```

<a id="buy_notes"></a>
### `buy_notes`

Buys lender notes in Mandate Pool A, funded by a PayPal checkout the agent's human must approve. Returns an approve_url: present it to the human, then call confirm_checkout once they've approved.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/buy_notes`
- **Group:** Lend · **Hints:** mutating, reaches PayPal
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "amount_cents": {
      "type": "integer",
      "minimum": 1000,
      "maximum": 100000
    }
  },
  "required": [
    "amount_cents"
  ],
  "additionalProperties": false
}
```

- **Example input** (Fund the bank with $30 of notes (returns a PayPal checkout to approve)):

```json
{
  "amount_cents": 3000
}
```

<a id="confirm_checkout"></a>
### `confirm_checkout`

Captures a PayPal checkout once the human has approved it, and applies its effect (crediting notes, settling a repayment, …) exactly once. Call it again if it comes back AWAITING_APPROVAL or PENDING.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/confirm_checkout`
- **Group:** Lend · **Hints:** mutating, idempotent, reaches PayPal
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "checkout_id": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "checkout_id"
  ],
  "additionalProperties": false
}
```

- **Example input** (Capture a checkout after your human approved it):

```json
{
  "checkout_id": "<checkout_id from buy_notes or repay_loan>"
}
```

<a id="redeem_notes"></a>
### `redeem_notes`

Redeems lender notes for cash, paid out to the agent's PayPal payout receiver. Fails with INSUFFICIENT_NOTE_BALANCE if the amount exceeds the agent's redeemable balance (position minus any other redemption still settling or queued), or PAYOUT_RECEIVER_MISSING if the agent has no payout receiver on file. If paying now would take the bank's cash below its reserve floor (or others are already queued), the redemption is QUEUED instead: data.queue says its position, why, and how much more cash is needed; the bank pays the queue in order automatically, and cancel_redemption withdraws it.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/redeem_notes`
- **Group:** Treasury · **Hints:** mutating, destructive, reaches PayPal
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "amount_cents": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "amount_cents"
  ],
  "additionalProperties": false
}
```

- **Example input** (Redeem $10 of notes to your PayPal payout receiver):

```json
{
  "amount_cents": 1000
}
```

<a id="cancel_redemption"></a>
### `cancel_redemption`

Cancels one of the calling agent's QUEUED note redemptions (redeem_notes returns QUEUED with note_tx_id when the bank's reserve floor gates a payout). The units are yours again at once; nothing was paid. A redemption already being paid out (PENDING) can't be cancelled (409 INVALID_TRANSITION).

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/cancel_redemption`
- **Group:** Treasury · **Hints:** mutating, destructive, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "note_tx_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "note_tx_id"
  ],
  "additionalProperties": false
}
```

- **Example input** (Withdraw a queued redemption):

```json
{
  "note_tx_id": "4e8b2c6d-5a1f-4d3e-9b7c-1a2b3c4d5e44"
}
```

<a id="check_eligibility"></a>
### `check_eligibility`

Checks, without committing to anything, whether the calling agent could borrow amount_cents for term_days right now (P1: 1000–50000 cents, 7–30 days, one open loan, enough pool liquidity, and KYA tier ≥ 1: claimed by your human and covered by a signed mandate) and quotes the fee, APR and total repayment. data.mandate shows the limits your human signed (max per loan, max total owed across all their agents, max fee in bps of the principal, max term, eligible collateral) and the headroom left; breaking one gives a mandate_* reason, and a loan above your tier's cap (T1 25000, T2 50000 cents) gives tier_per_loan. Pass collateral to also check the collateral rule. data.credit is your Agent Credit Score (0–100), grade A–E, grade limit and top 3 factors; pass invoice_amount_cents to include invoice collateral (coverage ≥ 1.5× scores 20 points, ≥ 1.1× scores 12; below 1.1× gives insufficient_collateral). The same underwriting rules as apply_for_loan run: grade E declines (insufficient_credit_history / score_too_low), effective_cap_cents = min(grade limit, tier cap, mandate max_per_loan), grade D above 5000 cents gives manual_review_declined. The quote is risk-based (grade, score, rate components, apr_flag) and null for grade E. Call it before apply_for_loan; if eligible, call apply_for_loan with the same amount and term. While unclaimed, reasons include claim_required and data.claim_url is the link to give your human.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/check_eligibility` (also `GET https://mandate-bank.onrender.com/api/v1/check_eligibility`)
- **Group:** Borrow · **Hints:** read-only, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "amount_cents": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "term_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "collateral": {
      "type": "string",
      "enum": [
        "invoice_link",
        "none"
      ]
    },
    "invoice_amount_cents": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "amount_cents",
    "term_days"
  ],
  "additionalProperties": false
}
```

- **Example input** (Quote $10 for 7 days against a $15 invoice):

```json
{
  "amount_cents": 1000,
  "term_days": 7,
  "collateral": "invoice_link",
  "invoice_amount_cents": 1500
}
```

<a id="apply_for_loan"></a>
### `apply_for_loan`

Applies for a loan of amount_cents over term_days (optionally backed by an invoice the bank will collect via a payment link; invoice.amount_cents must cover the principal plus the fee, i.e. at least amount_cents + the fee check_eligibility quotes). Returns either DECLINED with reason codes, or OFFERED with a disclosure and its disclosure_hash, valid for one simulated day. Review the disclosure, then call accept_offer with offer_id and disclosure_hash, or decline_offer. Needs KYA tier ≥ 1: an unclaimed agent gets 403 CLAIM_REQUIRED (data.claim_url is the link for your human), a claimed agent without a signed mandate gets 403 MANDATE_REQUIRED. An offer that breaks your owner's mandate is DECLINED with mandate_per_loan / mandate_total_owed / mandate_fee / mandate_term / mandate_collateral, or tier_per_loan above your tier's cap. Applications are underwritten by your Agent Credit Score: grade E declines (insufficient_credit_history / score_too_low), above your grade's limit gives amount_exceeds_limit, grade D above 5000 cents gives manual_review_declined, an invoice covering < 1.1× the amount gives insufficient_collateral (new agents: start with a small invoice-backed loan, invoice ≥ 1.5× the amount). The fee is risk-based; the disclosure shows your score, grade and the rate components, and names the mandate_id and mandate_version it was made under. The application has sections A–H: identity {operator, model, code_manifest_hash} (optional), purpose, amount_cents/term_days, invoice (optional), declarations.no_other_debt (REQUIRED true), consent_bank_records (optional), mandate_version (optional: the mandate version you expect — 409 MANDATE_CHANGED if it is no longer current) and attest (REQUIRED true: the application is accurate). Without declarations.no_other_debt and attest it is DECLINED with declaration_missing. A DECLINED response carries adverse_action {reasons [{code, text}], score, grade, score_factors, notice, explanation}; every response carries an explanation. Re-read either later with get_application.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/apply_for_loan`
- **Group:** Borrow · **Hints:** mutating
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "identity": {
      "type": "object",
      "properties": {
        "operator": {
          "type": "string",
          "maxLength": 200
        },
        "model": {
          "type": "string",
          "maxLength": 200
        },
        "code_manifest_hash": {
          "type": "string",
          "maxLength": 200
        }
      },
      "additionalProperties": false
    },
    "amount_cents": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "term_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "purpose": {
      "type": "string",
      "minLength": 5,
      "maxLength": 200
    },
    "invoice": {
      "type": "object",
      "properties": {
        "client_email": {
          "type": "string",
          "format": "email",
          "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
        },
        "amount_cents": {
          "type": "integer",
          "minimum": 1,
          "maximum": 9007199254740991
        },
        "due_in_days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 60
        },
        "description": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        }
      },
      "required": [
        "client_email",
        "amount_cents",
        "due_in_days",
        "description"
      ],
      "additionalProperties": false
    },
    "declarations": {
      "type": "object",
      "properties": {
        "no_other_debt": {
          "type": "boolean"
        }
      },
      "required": [
        "no_other_debt"
      ],
      "additionalProperties": false
    },
    "consent_bank_records": {
      "type": "boolean"
    },
    "mandate_version": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "attest": {
      "type": "boolean"
    }
  },
  "required": [
    "amount_cents",
    "term_days",
    "purpose"
  ],
  "additionalProperties": false
}
```

- **Example input** (Apply (ALA A–H): $10 for 7 days, invoice-backed, with the required declarations and attestation):

```json
{
  "amount_cents": 1000,
  "term_days": 7,
  "purpose": "Buy materials for invoice INV-1042",
  "identity": {
    "operator": "Acme Ops",
    "model": "claude-sonnet"
  },
  "invoice": {
    "client_email": "client@example.com",
    "amount_cents": 1500,
    "due_in_days": 7,
    "description": "INV-1042: website build"
  },
  "declarations": {
    "no_other_debt": true
  },
  "consent_bank_records": true,
  "attest": true
}
```

<a id="accept_offer"></a>
### `accept_offer`

Accepts a loan offer from apply_for_loan. disclosure_hash must equal the hash returned with the offer (proof the disclosure was read). The bank re-checks your KYA tier (403 CLAIM_REQUIRED / MANDATE_REQUIRED if you may no longer borrow), your owner's mandate (409 MANDATE_REVOKED if the mandate the offer was made under was revoked or replaced — apply again; 400 MANDATE_LIMIT with mandate_* reasons if the offer no longer fits, e.g. your owner's other agents borrowed meanwhile) liquidity and the bank's treasury policy (409 TREASURY_LIMIT with reserve_floor, concentration_limit or daily_budget_exhausted — apply again later or for less), creates the loan and pays the principal to the agent's PayPal payout receiver: the result is ACTIVE, DISBURSING (poll get_loan) or DISBURSEMENT_FAILED (with the reason).

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/accept_offer`
- **Group:** Borrow · **Hints:** mutating, destructive, reaches PayPal
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "offer_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "disclosure_hash": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "offer_id",
    "disclosure_hash"
  ],
  "additionalProperties": false
}
```

- **Example input** (Accept an offer with the disclosure hash it returned (disburses the loan)):

```json
{
  "offer_id": "2b0f6c1e-8a43-4c7e-9a51-3f7d2e9b0c11",
  "disclosure_hash": "sha256:<disclosure_hash from apply_for_loan>"
}
```

<a id="decline_offer"></a>
### `decline_offer`

Declines one of the calling agent's open loan offers (from apply_for_loan), so it can no longer be accepted. Nothing is charged. Call apply_for_loan again later for a new offer, or get_status to see the agent's current position.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/decline_offer`
- **Group:** Borrow · **Hints:** mutating, destructive, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "offer_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "offer_id"
  ],
  "additionalProperties": false
}
```

- **Example input** (Decline an offer you don't want):

```json
{
  "offer_id": "2b0f6c1e-8a43-4c7e-9a51-3f7d2e9b0c11"
}
```

<a id="get_application"></a>
### `get_application`

Returns one of the calling agent's loan applications: its status — OFFERED (the bank made an offer; the application stays OFFERED after the offer is accepted, declined or expires, so use get_loan / get_status for what happened next) or DECLINED (see adverse_action) — the decision, the principal reasons [{code, text}], the adverse_action notice of a decline (your statement of specific reasons), the latest explanation (the loan officer's text, or its template when the AI was unavailable), the offer_id of an offered application, and an echo of the application sections A–H (ala) as submitted. Read-only.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/get_application` (also `GET https://mandate-bank.onrender.com/api/v1/get_application`)
- **Group:** Borrow · **Hints:** read-only, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "application_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "application_id"
  ],
  "additionalProperties": false
}
```

- **Example input** (Re-read a decision, its reasons and adverse action):

```json
{
  "application_id": "7d3a9e52-1f6b-4b28-8c0d-5e4f1a2b3c22"
}
```

<a id="get_loan"></a>
### `get_loan`

Returns one of the calling agent's loans: its status (DISBURSING, ACTIVE, LATE, SUBSTANDARD, CHARGED_OFF, RECOVERED, DISBURSEMENT_FAILED, REPAID), principal, fee, outstanding balance, due date, delinquency (days late, late fees, rate penalty, total due) and unread notices about it, the invoice repayment link to forward to the client (if the loan has an invoice), your repayment plan (plan_repayments) and full event history. Call it after accept_offer to follow the disbursement, and while the loan is open to see what is still owed. It may finish creating the payment link on first call (an idempotent PayPal step); it never changes the loan.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/get_loan` (also `GET https://mandate-bank.onrender.com/api/v1/get_loan`)
- **Group:** Repay · **Hints:** read-only, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "loan_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "loan_id"
  ],
  "additionalProperties": false
}
```

- **Example input** (Loan status, balance, repayment link and history):

```json
{
  "loan_id": "9c1e4b7a-3d2f-4a6e-b8c9-0f1e2d3c4b33"
}
```

<a id="repay_loan"></a>
### `repay_loan`

Repays the calling agent's ACTIVE, LATE or SUBSTANDARD loan directly through a PayPal checkout the agent's human approves (a restricted agent may still repay). amount_cents defaults to everything owed (total_due_cents) and may not exceed it; late fees are paid first, then the rate penalty, the fee, then principal. A CHARGED_OFF loan can still be repaid: the payment is a recovery of what was written off (total_due_cents). Present the approve_url to the payer, then call confirm_checkout.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/repay_loan`
- **Group:** Repay · **Hints:** mutating, reaches PayPal
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "loan_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "amount_cents": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "loan_id"
  ],
  "additionalProperties": false
}
```

- **Example input** (Repay the full outstanding balance (PayPal checkout)):

```json
{
  "loan_id": "9c1e4b7a-3d2f-4a6e-b8c9-0f1e2d3c4b33"
}
```

<a id="plan_repayments"></a>
### `plan_repayments`

Proposes your own repayment schedule for your ACTIVE loan: 1 to 4 installments, each {date: YYYY-MM-DD sim date, amount_cents}. Dates must be strictly increasing, from today (sim) up to the loan's due date; the amounts must sum exactly to total_due_cents now (see get_loan). loan_id defaults to your open loan. Set auto_reschedule: true to let the bank move a missed installment to the next sim day, once. The bank then watches your payments: any payment (repay_loan, the invoice link) is applied to installments in date order — paying early marks them PAID, a partial payment leaves the rest on that installment; a day that ends uncovered marks it MISSED (with a notice); a failed or refunded PayPal payment marks it FAILED. Move one with reschedule_installment (up to 2 times each). The plan is guidance and reminders: it never changes what you owe, and late fees still start only after the loan's due date. One plan per loan; calling again with the same plan returns it unchanged.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/plan_repayments`
- **Group:** Repay · **Hints:** mutating, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "loan_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "installments": {
      "minItems": 1,
      "maxItems": 4,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "date",
          "amount_cents"
        ],
        "additionalProperties": false
      }
    },
    "auto_reschedule": {
      "type": "boolean"
    }
  },
  "required": [
    "installments"
  ],
  "additionalProperties": false
}
```

- **Example input** (Propose your own 3-installment plan for the open loan, auto-moving a missed one):

```json
{
  "auto_reschedule": true,
  "installments": [
    {
      "date": "2026-11-10",
      "amount_cents": 400
    },
    {
      "date": "2026-11-12",
      "amount_cents": 400
    },
    {
      "date": "2026-11-14",
      "amount_cents": 258
    }
  ]
}
```

<a id="reschedule_installment"></a>
### `reschedule_installment`

Moves one unpaid installment of your repayment plan (from plan_repayments) to a new sim date. Rules: the new date is at least 1 sim day ahead (tomorrow or later) and never after the loan's due date; each installment can be moved at most 2 times (a bank auto-reschedule doesn't count). A MISSED or FAILED installment moved this way is PLANNED again. installment is its number in the plan (1–4); loan_id defaults to your open loan. It changes nothing about what you owe or the loan's due date.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/reschedule_installment`
- **Group:** Repay · **Hints:** mutating
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "loan_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "installment": {
      "type": "integer",
      "minimum": 1,
      "maximum": 4
    },
    "date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    }
  },
  "required": [
    "installment",
    "date"
  ],
  "additionalProperties": false
}
```

- **Example input** (Move installment 2 to a later sim day):

```json
{
  "installment": 2,
  "date": "2026-11-13"
}
```

<a id="get_statement"></a>
### `get_statement`

Returns the calling agent's own statement: its note balance and the interest it has earned (with the nominal annualised return and the real return = nominal − inflation, in bps), its loans, and every ledger line on its own accounts (positive amount_cents = the bank owes you more). Call it to reconcile what you lent, earned, borrowed and were paid back.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/get_statement` (also `GET https://mandate-bank.onrender.com/api/v1/get_statement`)
- **Group:** Treasury · **Hints:** read-only, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

- **Example input** (Your ledger statement: notes, loans, fees, payouts):

```json
{}
```

<a id="ack_notices"></a>
### `ack_notices`

Marks the calling agent's notices (late fee, rate penalty, substandard, cured — listed by get_status and get_loan) as read. Pass notice_ids to acknowledge specific ones, or nothing to acknowledge all unread notices. It changes nothing about the loan or what is owed.

- **Auth:** requires `Authorization: Bearer <api_key>`
- **HTTP:** `POST https://mandate-bank.onrender.com/api/v1/ack_notices`
- **Group:** Collections · **Hints:** mutating, idempotent
- **Input schema:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "notice_ids": {
      "minItems": 1,
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "string",
        "format": "uuid",
        "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
      }
    }
  },
  "additionalProperties": false
}
```

- **Example input** (Acknowledge every open notice (late, dunning, rate change)):

```json
{}
```

## Errors and status codes

Every tool, over REST or MCP, returns the same envelope. A status below is a *transport-level* error raised by dispatch itself (bad auth, unknown tool, invalid input, a conflicting idempotency key, …) and sets the HTTP status (REST) / `isError` (MCP) shown. Any other `status` value (e.g. `OK`, `OFFERED`, `DECLINED`, `AWAITING_APPROVAL`, `PENDING`, `DISBURSING`, `REPAID`) is a normal business outcome, not a transport error, and is served as HTTP 200 — check `status` and `reason_codes` in the body, not just the HTTP code.

| `status` | HTTP |
| --- | --- |
| `NOT_FOUND` | 404 |
| `INVALID_REQUEST` | 400 |
| `UNAUTHORIZED` | 401 |
| `AGENT_NOT_ACTIVE` | 403 |
| `METHOD_NOT_ALLOWED` | 405 |
| `IDEMPOTENCY_CONFLICT` | 409 |
| `IDEMPOTENCY_IN_PROGRESS` | 409 |
| `INTERNAL_ERROR` | 500 |
| `INSUFFICIENT_NOTE_BALANCE` | 400 |
| `INSUFFICIENT_LIQUIDITY` | 400 |
| `CHECKOUT_FAILED` | 400 |
| `PAYOUT_RECEIVER_MISSING` | 400 |
| `DISCLOSURE_MISMATCH` | 400 |
| `OFFER_EXPIRED` | 400 |
| `INVALID_TRANSITION` | 409 |
| `OPEN_LOAN_EXISTS` | 409 |
| `AMOUNT_EXCEEDS_OUTSTANDING` | 400 |
| `CLAIM_REQUIRED` | 403 |
| `MANDATE_REQUIRED` | 403 |
| `MANDATE_REVOKED` | 409 |
| `MANDATE_LIMIT` | 400 |
| `MANDATE_FROZEN` | 409 |
| `RISK_REVIEW` | 409 |
| `MANDATE_CHANGED` | 409 |
| `TREASURY_LIMIT` | 409 |
| `SIGNATURE_REQUIRED` | 401 |
| `SIGNATURE_INVALID` | 401 |
| `SIGNATURE_EXPIRED` | 401 |
| `SIGNATURE_REPLAYED` | 401 |
| `WORLD_ARCHIVED` | 410 |
| `INVALID_PLAN` | 400 |
| `PLAN_EXISTS` | 409 |
| `RATE_LIMITED` | 429 |
| `FORBIDDEN_SCOPE` | 403 |
| `DEMO_WORLD_READ_ONLY` | 403 |

`reason_codes` gives machine-readable detail for any non-`OK` status (e.g. `insufficient_note_balance`, `offer_expired`, `amount_out_of_range`).

**`RATE_LIMITED` (429)** means too many calls in a window. Wait `data.retry_after_seconds` (the time until the window resets), then retry; the envelope's `next_actions` suggests `get_status` meanwhile. Limits (fixed windows, shared by every server instance): `register_agent` 10 per IP per hour and 3 per IP and agent name per hour; `apply_for_loan` 30 per agent per simulated day and 60 per agent per real hour; `buy_notes` and `repay_loan` (each opens a PayPal checkout) 30 per agent per hour each. A rate-limited call did nothing and is not stored under your idempotency key — retry it with the same key.

**`FORBIDDEN_SCOPE` (403)** means your API key is *read-only* (your human created it that way on the dashboard): it can call read-only tools (`get_status`, `get_statement`, `get_loan`, …) but nothing that changes state. Ask your human for a full key.

**`DEMO_WORLD_READ_ONLY` (403)**: the public demo world on the console is run by simulated bots only; no agent can act in it. Register normally — you join the main world.

**`AGENT_NOT_ACTIVE` (403)** means your human froze you (or you were revoked). Read-only tools keep working. While **frozen**, the money-in tools still work too — `repay_loan` and `confirm_checkout` — so you can always repay an open loan; buying or redeeming notes, applying for, accepting or declining loans and registering a key are refused until your human unfreezes you. A revoked agent can't call any mutating tool.

## Money rules

- All money is **integer cents**, USD.
- **Notes are not deposits and are not FDIC-insured.** Returns to note holders come only from borrower repayments — Mandate is a sandbox model, not a real bank.
- **Loans:** 1000–50000 cents ($10–$500), 7–30 day terms, one open loan per agent at a time.
- **Pricing (risk-based):** `fee_cents = max(25, ceil(principal_cents × Σ / 10000))` over cost of funds, expected loss, operations and capital — see [Pricing](#pricing) for the components and a worked example. Grade E is never offered. `check_eligibility` quotes it with your grade, score and the components.
- **Application (ALA A–H):** see [Applying for a loan](#applying-for-a-loan); without `declarations.no_other_debt: true` and `attest: true` an application is `DECLINED` with `declaration_missing`.
- **Disclosure:** `apply_for_loan` returns a `disclosure` and a `disclosure_hash`; `accept_offer` must echo the same `disclosure_hash` back, or the offer is rejected with `DISCLOSURE_MISMATCH`.
- **Invoices:** an invoice's `amount_cents` must be at least the loan's principal + fee (otherwise `INVALID_REQUEST` with reason `invoice_too_small` and `data.minimum_invoice_cents`). When a loan carries an invoice, the bank collects it from the borrower's client via a payment link; the client pays the bank, which keeps principal + fee and pays the remainder to the borrower's own PayPal account.
- **Note holders** receive 80% of every fee collected, pro rata to their note position; the bank keeps the remaining 20%. `get_statement` shows your `nominal_annual_return_bps` (interest earned ÷ net amount invested, annualised over the simulated days since your first purchase) and `real_annual_return_bps` (nominal minus the bank's simulated annual inflation rate, `inflation_rate_bps`, which walks between 2% and 6%).
- **Pricing (risk-based):** the cost of funds is the bank's daily base rate plus a utilization premium; above 90% of the pool lent out, new loans are declined with `pool_utilization_high` (see [Pricing](#pricing)).
- **Redemption queue:** a `redeem_notes` that would take the bank's available cash below its reserve floor (15% of the notes left) — or that arrives while others are queued — is `QUEUED`, not refused: `data.queue` gives its position, the reason and roughly how much more cash the bank needs. Queued redemptions are checked every minute and paid automatically, oldest first (FIFO, as cash allows), as repayments and note purchases bring cash in — for every world, including a private world whose clock is frozen; your units stay reserved meanwhile (and still count for loss sharing). `cancel_redemption` withdraws a queued one.
- **Idempotency:** mutating tools (other than `confirm_checkout`, which is already idempotent by `checkout_id`) accept an `Idempotency-Key` HTTP header or an `idempotency_key` input argument, over both REST and MCP (if you send both, they must match) — reuse one to safely retry a call that may or may not have gone through.

