Skip to content
Build

Agent safety

Per-key spending policies, shared budgets for delegated keys and independent approval holds.

Full guide in the repository: docs/agent-safety.md

Bind each agent to its own API key, narrow resource scopes and a server-enforced spending policy. The same admission path protects HTTP and identity-bound MCP transactions, and decisions and usage are persisted in PostgreSQL before asynchronous processing. Prompts, caller metadata and confirm: truedon't supply approval authority.

Set a policy

An administrator, or a key with explicit policies:write, sets a key's policy with PUT /policies/:api_key_id:

PUT /policies/:api_key_idRequest
{  "rules": [    {      "currency": "USD",      "precision": 100,      "max_transaction": "5000",      "daily_limit": "10000",      "approval_above": "2000"    }  ],  "ledger_ids": [    "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23"  ],  "balance_ids": [    "bln_df56a248-7d7e-4c0c-8fbf-a5215c87b603",    "bln_173d086e-57cb-4690-9abe-1cf060fafb13"  ]}
  • Amounts are canonical non-negative integer strings in the rule's units: "5000" at precision 100 is 50.00. "0" is a real zero; a missing limit leaves that dimension uncapped.
  • Currency and precision pairs are allow rules. A missing pair denies admission.
  • Non-empty ledger and balance lists must admit both endpoints of every leg. Limits apply to the aggregate of split and bulk requests, so splitting a payment doesn't multiply the allowance.
  • max_transaction denies anything above the cap. daily_limit covers settled spend plus queued, scheduled and held reservations for the UTC day. approval_above requires approval when the amount is strictly greater than the threshold.
Don't give an agent policies:write, administrator authority or approval authority. A policy is not a sandbox around a credential that can change its own rules.

Approval holds

Agent key submitsPOST /transactions or an MCP write tool
Server-side admissionCurrency and precision rule, ledger and balance allowlists, per-transaction cap and the shared UTC daily budget. Usage is reserved in PostgreSQL.
Within limits2000 or less: admitted without review. In the example, 1000 settles as APPLIED.
Above approval thresholdForced inflight hold with a pending approval. 3000 returns INFLIGHT.
Outside policyOver the cap, budget or allowlist: denied. 5001 is refused.
Initiator tries to approveDenied. The initiating key, its descendants and its siblings cannot approve or reject its hold.
Independent approver decidesA separate key with approvals:write calls approve (settles) or reject (releases). Retrying a decision does not settle twice.

Above the threshold, the server forces an inflight hold and records a pending approval bound to the transaction details. The admission ID is in the transaction metadata:

POST /transactions (3000 units, agent key)excerpt
{  "precise_amount": 3000,  "precision": 100,  "transaction_id": "txn_1a536530-f07d-43a5-8f72-c89965d179cd",  "reference": "payout-2207",  "status": "INFLIGHT",  "inflight": true,  "meta_data": {    "policy_admission_id": "adm_dbebd5ed-78e0-40ce-a4a2-ab598a95e254",    "policy_approval": "pending"  }}

The agent's own key can't decide it:

POST /approvals/:admission_id/approve (agent key)403 Forbidden
{  "error": "spending policy denied transaction"}

A separately provisioned approver with approvals:write can:

POST /approvals/:admission_id/approve (approver key)200 OK
{  "admission_id": "adm_dbebd5ed-78e0-40ce-a4a2-ab598a95e254",  "api_key_id": "api_key_8224a822-7ec0-4e0e-8f8f-d95039fc8fb3",  "identity": "payout-2207",  "policy_version": 1,  "day": "2026-10-10T00:00:00Z",  "decision": "approved",  "decided_by": "api_key_02790ec7-4f42-462b-8044-a0df92c70086",  "legs": [    {      "transaction_id": "txn_1a536530-f07d-43a5-8f72-c89965d179cd",      "reference": "payout-2207",      "source": "bln_df56a248-7d7e-4c0c-8fbf-a5215c87b603",      "destination": "bln_173d086e-57cb-4690-9abe-1cf060fafb13",      "source_ledger": "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23",      "destination_ledger": "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23",      "currency": "USD",      "precision": 100,      "amount": "3000"    }  ]}

The initiating key can't approve or reject its own request, and nor can its descendants, siblings or intermediate ancestors within the same delegation root. The non-delegated root may decide a descendant's hold if it has explicit approvals:write. A key from another root may decide, and the master key has administrator authority. Wildcard scopes alone don't grant approval or policy powers. Decisions are final and retryable; a retried approval doesn't settle twice.

A different key is an independent server principal, not proof that a human reviewed the payment. Keep the approver credential out of the agent's environment and run your review process around it.

Delegation and shared budgets

Delegated keys inherit their ancestors' restrictions. A child can narrow scopes, expiry, allowed pairs, allowlists and limits, but can't remove a parent's constraints. Parents and children share the ancestor's daily budget: creating ten keys doesn't create ten allowances.

GET /policies/effective200 OK
{  "capacity": [    {      "budgets": [        {          "api_key_id": "api_key_8224a822-7ec0-4e0e-8f8f-d95039fc8fb3",          "used_minor": "3000",          "daily_limit": "10000"        }      ],      "currency": "USD",      "precision": 100,      "day": "2026-10-10T00:00:00Z",      "used_minor": "3000",      "remaining_minor": "7000"    }  ],  "policy": {    "api_key_id": "api_key_8224a822-7ec0-4e0e-8f8f-d95039fc8fb3",    "version": 1,    "rules": [      {        "currency": "USD",        "precision": 100,        "max_transaction": "5000",        "daily_limit": "10000",        "approval_above": "2000"      }    ],    "ledger_ids": [      "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23"    ],    "balance_ids": [      "bln_df56a248-7d7e-4c0c-8fbf-a5215c87b603",      "bln_173d086e-57cb-4690-9abe-1cf060fafb13"    ]  }}

budgets lists every charged ancestor; remaining_minoris the smallest remaining allowance across them. A hold admitted before midnight UTC stays in that day's usage. Voids, expiry and failures release unused capacity; a refund is a new outgoing operation, not a credit to the budget.

Endpoints

OperationEndpoint
Inspect the calling key's effective policy and UTC capacityGET /policies/effective
List pending admissionsGET /approvals
Approve and settle held legsPOST /approvals/:admission_id/approve
Reject and release held legsPOST /approvals/:admission_id/reject

What policies don't cover

Policies protect against an agent submitting disallowed operations through the authenticated HTTP and MCP paths, including fan-out through split, bulk or delegated requests. They don't protect against a database administrator, a master key, an unbound local MCP process, stolen approver credentials, incorrect policy setup, compromised server code or an authorised but wrong business decision. They don't provide currency conversion or fraud detection. The agent-policy example runs these scenarios end to end against the local stack.