An agent has one policy. It says what every grant on the agent may do, and the owner signs all of it. See How it works for how the policy, grants and the other checks fit together.
At a glance
  • One policy per agent, shared by every grant: actions, assets, limits, destinations, USD budget, timelock, schedule, owner approval, freeze and native accounts.
  • One owner signature per change. Each policy_update replaces the complete policy and names the expected_revision it edits.
  • In force once status is APPLIED and provider_policy_synced is true.
  • One change per 10 minutes per agent. Freezing is never delayed.

Common choices


The policy

Every field is required except schedule and native. Amounts are atomic-unit strings, durations are milliseconds, and the field names are snake_case.
native is for advanced users. The rest of the policy does not protect native methods: a destination list, a USD budget, a timelock, a schedule or owner approval never stops them. With native.near.transfer on, the agent can send NEAR to any account whatever destinations says.
Every control is a separate layer. None overrides another and none adds allowance to another: a larger USD budget never lifts a per-asset limit. Enabling an action applies to every grant at once, and so does disabling it or freezing the agent. The API enforces destinations, budget, timelock_ms and schedule itself. The wallet provider enforces the rest (actions, assets, limits, max_actions_per_hour, owner approval and freeze). The API also counts the per-asset limits over a window and max_actions_per_hour before anything reaches the wallet: see Per-asset limits. It also refuses a disallowed action (policy_action_denied) and a frozen agent (wallet_frozen) before it calls the provider. A refusal from the provider’s rules is policy_denied. Native requests are checked against native by both the API and the wallet provider. See Native Accounts.

Destinations

destinations is the one address rule for every grant. The safe starting rule is { "mode": "only", "list": [] }: swaps and shield/unshield work, but nothing leaves the agent until the owner lists a destination. The list holds up to 256 entries: Use the canonical chain id (eth, not ethereum) and lowercase hex for EVM addresses, including hyperevm, adi and hood. only and except compare addresses in canonical form, so a bech32 address (btc, ltc, Cardano addr1, Zcash zs1 and u1, aleo) or a Bitcoin Cash CashAddr matches in any case, with or without bitcoincash:, and starknet, sui, aptos and movement hex matches in any case and with or without leading zeros. Base58 addresses keep their exact case. Adding a destination is one policy_update. Every live grant can use it at once, with no new grant.

Per-asset limits

The API keeps its own count of each agent’s use of limits.hourly, limits.daily, limits.monthly and max_actions_per_hour, shared by every grant. It checks the count at the dispatch commitment, so parallel requests cannot pass a limit together. The wallet provider checks the same rules again, and per_transaction too.
  • Swaps, withdrawals, transfers, shield and unshield count, at the amount of the asset that leaves the balance. Moving funds between the public and the confidential balance uses the same limits and actions.
  • near and nep141:wrap.near share one count. When several limits name the same asset, the smallest applies.
  • Pending, held and uncertain actions count until their outcome is known. A completed action counts for the full hour, day or 30 days after it completes. An action the wallet provably never ran stops counting.
  • Removing a limit keeps the usage: adding it back counts the same history.
Over a limit, the action fails with policy_limit_exceeded (403), or policy_action_limit_exceeded (403) for the hourly action count. policy_usage_unavailable (409) means the agent has earlier actions the API has no count for. The API refuses rather than guess while they could still fall inside the window. A new execution that runs now is refused before anything is recorded: no operation, no FAILED status, no charge. The same key is judged again and refuses again while the cause lasts. A FAILED row is recorded only when a request loses a concurrent race at the dispatch commitment, or a timelocked request is released over budget or a limit; its key then replays that outcome.

What the read shows

limits and assets report the configured rules. They do not show how much of a per-asset window is left. A per_transaction maximum never shrinks. Limits over a window do.

Read the policy

GET /v1/agents/{agent_id}/policy is the one current-policy read.
A signed revision is in force only once status is APPLIED and provider_policy_synced is true. Until then, executions fail with policy_not_ready. Do not present the new rules as usable yet.
GET /v1/agents/{agent_id}/policy/history lists the signed revisions, newest first. Page with limit (1 to 100) and cursor.

Update the policy

Changing the policy takes one owner signature over the complete policy. Send every rule, including the ones you are not changing.
1

Read the current revision

Use GET …/policy as above and keep revision and policy.
2

Generate a policy_update

Pass the complete edited policy and the revision you read.
3

The owner signs, you submit and wait

Your frontend has the owner’s wallet sign intent.payload. Show preview.policy, the complete proposal, next to the wallet prompt. Then submit and follow the status as for any owner intent.
  • A change to only destinations, budget, timelock_ms or schedule needs no blockchain transaction. The owner signs an off-chain message, and the API applies the complete revision when you submit it. The status receipt has transaction_hash: null.
  • A change to any other field, including native, is written to the agent wallet’s own rules on-chain. New actions pause while it applies, and the service pays the gas. The revision is in force once the transaction and the provider’s read-back confirm it.
  • If the policy changed since you read it, generating fails with policy_revision_conflict. Read it again and generate a new intent. If another policy intent is still signable or settling, you get policy_reconciliation_required: wait for it to settle or expire.
  • If an on-chain change’s storage cost changed since you generated the intent, submitting fails with policy_storage_changed (409) and nothing is sent. Generate a new intent and sign that one.
  • Every policy change invalidates actions admitted under the previous revision that have not been dispatched yet, such as queued ones. They fail with authorization_stale.

Cooldowns

To limit rapid rewrites, an agent’s policy can change at most once every 10 minutes. cooldowns.policy_change_available_at and cooldowns.unfreeze_available_at on the agent and on the policy read show when the next change is possible, or null. A change inside the window fails with policy_change_throttled or agent_unfreeze_throttled (HTTP 429), with errors[0].meta.available_at and a Retry-After header.

USD budget

Limits in the policy are per asset. The USD budget adds one dollar cap across every asset. budget has three caps. Each is a USD string with at most two decimals, or null for no cap. Each cap’s periods start when an applied policy turns it on. A daily cap turned on at 05:00 runs from 05:00 to 05:00 the next day, and a weekly cap runs seven days from that moment. A monthly cap renews on the same date and time each month (UTC), or on the last day of a shorter month: on from January 31, it renews on February 28, then March 31. When a period ends, the whole cap is available again. "0" allows no counted spending. Set all three on every update: each policy_update replaces the whole policy.
  • One budget per agent. Every grant shares it. Two connections under a $200 daily cap share $200, not $200 each. There are no per-grant budgets.
  • Changing an amount keeps the period. Raising a cap from $200 to $300 with $110 counted leaves $190. Lowering a cap below current usage leaves nothing until the period ends.
  • Periods are fixed, not rolling. Spending can bunch at a reset: under a $100 daily cap turned on at 05:00, $100 at 04:59 and another $100 at 05:00 both pass, up to twice the cap within minutes. That is the trade-off for a cap that comes back whole at a known time. A smaller shorter-window cap, per-asset hourly limits or timelock_ms bound the burst.
  • A cap counts only while it is on. Turning a cap on starts its first period, and earlier spending is not backfilled. Turning it off ends its period. Turning it on again starts a new one from that moment, so earlier spending no longer counts. An agent with no cap on counts nothing.

What counts

The budget counts gross activity, not net loss: an internal swap counts even though nothing leaves the agent. Moving funds to your own account on another chain or venue, such as your Hyperliquid or Polymarket address, counts like any withdrawal, and a deposit back does not credit it. An execution counts at its USD value when it dispatches, so a queued execution counts when it runs, not when it was queued.

Prices

The budget prices assets with the same list as GET /v1/tokens. A price is usable only when it was updated less than 10 minutes ago and uses the same decimals as the agent wallet. Otherwise price in the list is null, and a counted execution is refused with spend_price_unavailable (503) instead of counting as free. An agent with no cap on needs no prices. The value is computed exactly from the price and rounded up once, to the micro-dollar.

When the charge happens

The API decides the charge at the execution’s dispatch commitment, in the same database transaction that commits the dispatch. Concurrent requests therefore cannot exceed a cap. Over budget, a new execution that runs now is refused with spend_budget_exceeded (403) before anything is recorded or reaches the agent wallet. A timelocked one is judged when it is released, and ends FAILED with this code if it is then over budget. A charge is released only when the wallet provider provably refused or never ran the action, which the status reports as a failed action that never executed. A failure reason alone, such as a balance that ran short, is not proof and keeps the charge. Anything that may have moved value stays counted for the rest of its period. spent_usd is therefore counted usage, including actions whose outcome is still uncertain, not settled cash. A budget refusal records nothing, so after the allowance or prices change, send the request again. Never send a new key for an action that is uncertain or already dispatched.

Read the budget

usage.budget in the policy read has one object per cap: A swap quote or withdrawal preview (dry: true) is judged against the budget too, but it charges and reserves nothing, and the budget never refuses it. Its answer carries the same three caps as spend.budget, as they stand before that request, and spend.amount_usd, what it would count. When the live request would be refused, warnings has spend_budget_exceeded (or spend_price_unavailable without a fresh price). The answer is a snapshot, not a reservation: other work can use the budget first. See Executions.

Timelock

timelock_ms makes every money action wait before it runs: swap, withdraw, transfer, shield and unshield. Deposit addresses, native requests and the owner’s own intents are not delayed. The value is in milliseconds, from 0 (off) to 30 days (2592000000).
  • A delayed action returns QUEUED with details.execute_after. See Executions.
  • The owner cancels a waiting action with an execution_cancel intent.
  • usage.timelock.scheduled_count is how many executions the timelock holds. GET /v1/agents/{agent_id}/executions/scheduled lists them.

Schedule

schedule limits money actions to weekly windows on the owner’s clock. Leave it out to allow any time. A window whose end is earlier than its start runs past midnight and belongs to the day it starts on: fri 22:00 to 02:00 covers Friday night into Saturday morning. An action is judged when it would run: after timelock_ms for a new request, and again when a queued action is released. Outside the schedule, the API refuses it with policy_schedule_denied (403) and meta.available_at, the earliest time to submit again. Nothing is sent. A queued action that comes due outside the schedule fails with the same code. Retrying with the same Idempotency-Key returns the original action, even after the window closes. Deposits and native requests ignore the schedule, and so does the owner’s approval vote.

Freeze, archive and delete

These are owner intents, signed like any other change.

Next steps

Executions

How each action meets these rules.

Status & Errors

Every error code and what to do about it.