Agent safety
Per-key spending policies, shared budgets for delegated keys and independent approval holds.
Full guide in the repository: docs/agent-safety.mdBind 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:
{ "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_transactiondenies anything above the cap.daily_limitcovers settled spend plus queued, scheduled and held reservations for the UTC day.approval_aboverequires approval when the amount is strictly greater than the threshold.
policies:write, administrator authority or approval authority. A policy is not a sandbox around a credential that can change its own rules.Approval holds
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:
{ "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:
{ "error": "spending policy denied transaction"}A separately provisioned approver with approvals:write can:
{ "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.
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.
{ "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
| Operation | Endpoint |
|---|---|
| Inspect the calling key's effective policy and UTC capacity | GET /policies/effective |
| List pending admissions | GET /approvals |
| Approve and settle held legs | POST /approvals/:admission_id/approve |
| Reject and release held legs | POST /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.