Wallet and Tenant Limits
Per-merchant and per-agent limit model with tenant isolation checks
Wallet and Tenant Limits
Use this page for BDT-314 on the M5 Mainnet Safety milestone.
The goal is to make spending and wallet access predictable without letting one merchant or agent read or mutate another tenant's data.
Identity scope
Every request must resolve to one merchant scope.
| Scope key | What it controls | Notes |
|---|---|---|
merchant_id | Merchant-owned sessions, wallets, webhooks, spend policies, and exports | Primary tenant boundary. |
api_key | Server-to-server access for the merchant | Must map to exactly one merchant. |
agent_id | Agent-owned approvals, budget checks, and automation traces | Must be bound to the same merchant scope. |
wallet_id | Wallet read/write operations | Wallets inherit the merchant scope that created them. |
session_id | Payment session reads/writes | Sessions must never cross tenant scope. |
Limit types
Plaidly enforces the same limit model across dashboard, API, and agent workflows.
| Limit | What it bounds | Common use |
|---|---|---|
| Session amount | Maximum value for a single payment session | Prevents oversized deposits or test payments that exceed policy. |
| Daily volume | Total value a merchant or agent can move in a day | Caps aggregate exposure. |
| Spend amount | Maximum value for an outbound spend decision | Stops a single approval from draining the budget. |
| Failed attempts | Count of failed sensitive actions within a window | Detects brute force, misconfiguration, or repeated abuse. |
Limits are evaluated before the write is committed. If a request would cross the boundary, reject it and record the failure in the audit log.
The same mainnet gate applies to the current outbound spend-attempt path:
sandbox merchants stay testnet-only, and a merchant must be KYC-verified before
payment sessions, payout requests, or agent-wallet spend attempts can use
mainnet.
Agent spend policy evaluation
The current agent-wallet spend-policy evaluator matches active rules on these fields:
merchant_idagent_wallet_id- optional
api_domain - optional
chain - optional
network - optional
token_symbol - optional
min_amount_subunits/max_amount_subunits - optional
window_start/window_end
Decision precedence is explicit:
denyrequire_approvalallow
Within the same decision class, the highest priority wins; ties fall back to the oldest created_at.
If no policy matches, the evaluator currently defaults to allow.
Every evaluation writes both:
- an
agent_wallet_spend_decision_logsrow - an audit event named
agent_wallet.spend_policy_decided
POST /v1/agent-wallets/{agent_wallet_id}/spend-attempts invokes this
evaluator before any ledger or broadcast side effect. Denied attempts return
denied and do not create ledger rows. Allowed attempts create a pending
spent ledger entry and return pending; require_approval attempts return
approval_required.
Tenant isolation checks
Apply these checks to every sensitive route:
- The credential or agent must belong to the same
merchant_idas the target object. - The target
wallet_id,session_id, or policy id must resolve to that same merchant scope. - Read paths and write paths use the same ownership check.
- List and export paths must filter by the caller's merchant scope before applying paging or date filters.
- Admin or support overrides must still write an audit event with the actor identity and reason.
Wallet-specific rules
- Wallet creation, rotation, funding, and sweep activity inherit merchant scope.
- Settlement wallets stay hidden outside the owning merchant and support-only surfaces.
- Wallet identifiers can appear in logs and audit records, but secrets and private keys do not.
- If a wallet is reassigned or rotated, the old and new wallet states must remain distinguishable in the audit trail.
Failure behavior
When a limit or isolation check fails:
| Outcome | Expected behavior |
|---|---|
| Standard merchant request | Return a safe validation or authorization error. |
| Agent-driven request | Surface the same denial to the agent and the dashboard. |
| Export or list request | Return only data from the caller's own merchant scope. |
| Repeated boundary probing | Escalate to the abuse monitoring runbook. |
Treat mainnet_kyc_required and sandbox_mainnet_blocked as policy gates, not abuse by themselves. They become an abuse signal only when the same actor keeps probing after the denial.
Do not leak whether another tenant exists. The caller should see that the request is not allowed, not that a different merchant owns the object.