Skip to main content
The agent’s default vocabulary is the platform’s own: recipients, mandates, destinations. If your product calls them payees, spending limits and payout methods, a customer reading the agent’s reply meets three words they have never seen — in the middle of authorizing money. client_policy fixes that, and does more than rename things. It changes what the drafting model sees and constrains what it may propose, so the agent narrates in your nouns and refuses what your product refuses.
Beta, and entirely opt-in. Register nothing and the agent behaves exactly as it always has — platform’s own vocabulary, no added constraints. Registering an empty policy is the same thing, so adopting it is reversible: nothing you can set here is a one-way door.

The shape

Strict by design. An unknown key, an unknown value, or a label for a concept the server does not implement is a 400. “Accepted” always means “enforced” — there is no silently-ignored field.

Registering it

Registration is the only way to set a policy. It describes the client, so it is not something a request carries — a conversation is two calls, and both are judged by the same registered policy.
No client id in the path. You are resolved from your API key, so a policy can only ever be your own — there is nothing to look up and nothing to point at by mistake. GET /agentic-policy returns what is currently registered. It then applies to every drafting turn and every accept, with nothing to remember per request. A full replace, not a merge. Send the whole policy each time; whatever you omit is cleared. An identical re-registration is a no-op, so updated_at means “when this policy last changed” rather than “when a service last restarted”. Changes take effect on the next turn. There is no cache — which is also how you test a policy: register, run a conversation, register something else. To go back to platform defaults, register an empty policy.

Every field, and what changes

Each field is independent. Omit one and that dimension behaves exactly as it does with no policy at all — the examples below show both sides.

payee_model — how many things a payee with two payout methods is

One person with a bank account and a wallet is one payee in the platform’s model and two in a product that treats a payout method as the payee. Pick the one your own screens already use, because your customer is looking at those screens while they read the agent’s reply. Say Acme is saved with a bank account, and your customer now wants to pay them in USDC on Base. What their recipients list looks like afterwards:
Both are the same two payout methods on the same platform recipient. What differs is how many rows your product draws — and therefore what “add a way to pay Acme” means to the person reading it. The agent is given the same shape, so it says the matching thing:
  • nested — “I’ll add a USDC payout method to Acme.” One recipient, now with a second way to be paid.
  • flat — “I’ll add a new payee, Acme, paid in USDC on Base — note you already have an Acme paid by bank account ••••4321.” A second row appears, so it is announced as a new entry and it warns about the near-duplicate, which is the thing a flat list makes easy to get wrong.
That warning is not a rule the model was given. Under flat both entries are in front of it, so it writes the sentence that tells them apart. Underneath, the tool result it reads:
Two things to notice. paid_by is a phrase the platform renders from the payout method’s own fields, so the flat view carries no network_id or address for the model to reason about — but rails survives, because a bank payout has to name a rail the account really supports and that cannot be inferred from prose. And payee_ref is the destination id, entity_ref is the recipient id — opaque strings the model copies into actions, not a second id space to translate. The disambiguation in the second is not a rule the model was given. Both entries are in front of it, so it writes the sentence that tells them apart.

mandate_strategy — whether the agent may create authority

“Pay Priya 5 USDC”, where Priya is not on the customer’s limit:
  • unset — the agent proposes the payment and a new mandate to authorize it, and the customer signs both together.
  • external_only — the agent may not draft one. It proposes the payee alone and returns a blocker; your app collects the signature in its own limit editor. create_mandate in a proposal is refused at accept:

payout_route — how money is allowed to leave

“Pay Priya 5 USDC on Base”, funded from a stablecoin balance:
  • unset — a direct payment when a funding wallet is already on Base; a conversion only when the chains differ.
  • conversion_account_only — always paired: every create_scheduled_payments must come with a create_auto_account in the same proposal, so every payout leaves through the conversion account.
  • bank_only — a crypto payout is refused outright. create_crypto_destination is not a legal action, and a conversion must name a bank rail.

payout_assets — what a payee may receive

“Pay Joe 0.45 RD”, where RD is what the customer holds:
  • unset — the agent drafts an RD payout. RD settles on Base, so nothing refuses it.
  • ["USDC","USDT"] — it does not, and says why:
A recipient can only receive USDC or USDT — RD isn’t a supported output asset. Which would you like Joe to receive?
This constrains the payout leg only. The deposit leg — what funds a conversion — is untouched, and is usually a different asset. That distinction is the point: a customer naming RD has named what they hold, not what Joe gets.

labels — your nouns, in the data the model reads

Three concepts can be renamed. Anything else is a 400. Labels rewrite the tool results, so the model never learns platform’s word in the first place. With {"limit": "spending limit"}, active_mandates returns:
Both the container key and the id field take the noun, with spaces becoming underscores — spending limit gives you spending_limit and spending_limit_id. Every budget field keeps its own name and every number is untouched: this is a rename, not a reshape, because the arithmetic here has to stay identical to the one the gate applies at fire time. amounts_in appears only when you set limit_unit, and it is why the reply changes:
  • no labels — “Your mandate covers 1000 RD per transaction.”
  • labelled — “Your spending limit covers 1000 USD per transaction.”
A limit is stored in the asset that funds it. Under a system-chosen funding route the customer never picked that asset and should never be shown it — but without a unit the agent can only quote the number bare or reach for the funding asset, and it reached.
rule.asset and rule.network_id are still visible above because labels alone do not hide them. Set payout_route: conversion_account_only and they move out of rule entirely — with an internal funding leg they are plumbing the customer did not choose, and leaving them in front of the model made it refuse a payment that fit the limit exactly, on the grounds that the asset did not match.
The charsets are a security boundary, not tidiness. A label becomes a JSON field name in a platform-built struct and a word in the model’s context, so an unbounded one is a channel for smuggling a sentence — an instruction, a fake tool result — into the drafting prompt through a field you control.

Blockers

A turn that cannot draft has always ended the same way: no proposals, and a sentence of prose. That is fine for a human and useless for your application — “extend the limit to cover Priya”, “I need her bank details” and “that rail is not supported” all arrive as an empty list and some text. A blocker is that refusal in a form software can branch on.
Two properties worth relying on: Blockers may accompany proposals, and routinely do. The common case is a payee who does not exist yet: the turn proposes creating them and reports that the limit will not reach them, because your app has to do both, in that order — accept the proposal so the payee has an id, then amend the limit to include it. Treat the two as independent, never as alternatives. payee_name always names a payee the proposal creates. It is your join key back to the draft — a blocker carries no recipient id, because in the case this blocker is about the payee does not exist yet. The name is checked against the create_recipient actions in the same turn and snapped to their exact spelling, so “Rita” in the proposal never arrives as “Rita Fernandez” in the blocker. If it matches none of them, the blocker is dropped rather than handed to you unjoinable. Every blocker is verified server-side. The model reports one; the platform re-checks it against its own data and drops it if the facts do not hold — a limit that targets any (which covers every payee), an expired limit, a payee already on the list. A code you receive never reflects only the model’s opinion.
Switch on code and ignore codes you do not know — new ones are added over time, and an unrecognized code should leave your existing behaviour unchanged.

The full loop

The amendment adds a target and changes nothing else, so it can never raise a limit — which is what makes it reasonable to offer inside a conversation. See mandate signing for the bytes your customer signs.

A worked example

A consumer banking app — payees are “recipients”, the limit is a “spending limit” edited in its own settings screen, and every payout is converted from the customer’s stablecoin balance — registers:
Which produces, for “send Priya 5 USDC on base-sepolia at 0x2Bd9…”:
  • the agent calls a payee a recipient and a mandate a spending limit, everywhere
  • it never offers to create or raise a limit — it points at the app instead
  • it proposes the payee alone, so the app has an id to add
  • it returns mandate_does_not_cover_payee naming the limit and the payee
  • amounts are quoted in USD, not in the stablecoin that funds them

The conversion fee, and which side of it your amount is on

Not part of client_policy — you send it per request — but it changes what the numbers in a proposal mean, so it belongs next to them. Send your fee on both calls: developer_fee_defaults on POST /payment-agents/{id}/proposals so the agent can tell the customer what it costs, and on POST /instructions so it is applied. The drafting one only lets the agent explain the charge; the accept one is what bills it.
A payout type takes either a percentage, developer_fee_bps, or a fixed amount per payment, developer_fee_fixed. Sending both keys for one payout type is refused with a 400, even when one of them is 0. A fixed fee is a decimal string such as "10.00", with at most 2 decimal places, and must be greater than 0: to charge nothing, set developer_fee_bps to 0 instead. It has no currency of its own: it is charged in units of the stablecoin the payment sends, so "10.00" on a USDC payment is 10.00 USDC.
Each payout type is independent. Omit one, or set its developer_fee_bps to 0, and that payout type has no default fee. Nothing is charged and nothing is added to the amount unless the action sets its own developer_fee_bps or developer_fee_fixed, and the agent describes that payout type as free.
Renamed from developer_fee. developer_fee.swap_bps is now developer_fee_defaults.swap.developer_fee_bps, developer_fee.offramp_bps is now developer_fee_defaults.offramp.developer_fee_bps, and the action’s fee_bps is now developer_fee_bps. The old names are no longer accepted: a request that uses one gets a 400 whose detail names the replacement, for example developer_fee was renamed to developer_fee_defaults; swap_bps is now developer_fee_defaults.swap.developer_fee_bps. or actions[0].fee_bps was renamed to developer_fee_bps. A request-level developer_fee_bps, shown in earlier versions of this page, is no longer read: send developer_fee_defaults.
By default amount is what the payee RECEIVES. The server adds the fee on top, and the payee receives exactly the amount named. How much more leaves the funding wallet depends on the kind of fee. With a percentage fee, slightly more leaves the funding wallet:
At 1000 bps that actuates as 1.333334 leaving the funding wallet, and the payee receives the 1.20 that was named. Rounded UP at 6dp, because the provider rounds its own fee up and rounding to nearest leaves the payee an atomic unit short. With a fixed fee, the whole fee is added: what leaves the funding wallet is amount + developer_fee_fixed, exactly, with no rounding. A fixed fee is the same on every payment whatever its size, so on a small payment it can be a large share of what leaves:
Under a fixed fee of "10.00" that actuates as 110.00 USDC leaving the funding wallet, and the payee receives the 100.00 that was named. Under a fixed fee the amount must be at least 0.01, because the provider does not deliver less than that after the fee; a smaller amount is refused. Spending limits apply to the grossed amount, under either kind of fee: what leaves the funding wallet, fee included. The 100.00 payment above is checked as 110.00 against max_per_tx, and counts 110.00 toward every amount window cap, aggregate and per-target. A mandate that allows exactly 100.00 per payment does not cover it, and neither does a max_per_tx of 1.20 cover the 1.20 payment at 1000 bps. Some shortfalls are caught while the agent drafts the payment in conversation. When the proposal drafts the mandate itself, its max_per_tx is checked against each grossed payment, and its max_amount_in_window against the grossed payments that fall in one window (all of them, for a window of NONE); the agent is told to raise either one. A series that starts when the customer approves it, with no dates and no start_at, could land in any window, so it is checked at the most of its payments one window can hold, whatever the approval date, on top of the fullest window of the proposal’s other payments. A max_amount_per_target_in_window is not checked until fire time. Under mandate_strategy: external_only, a payment the customer’s existing limit doesn’t cover (over its per-payment cap, or no room left in its window) is refused while drafting, and the agent asks the customer to change their limit in your app. Otherwise a payment an existing mandate doesn’t cover fails at fire time with mandate_denied, as does any shortfall in a series you send straight to POST /instructions. Every cap is still enforced at fire time. See Mandate rule bounds. Set amount_includes_fee: true when the customer means the total instead — “send 10 in all”, “including the fee”. Then amount is what leaves and the payee receives less. Under a fixed fee the amount must be at least the fee plus 0.01, so that something reaches the payee: with a fee of "10.00", an amount of "110.00" delivers 100.00, "10.01" delivers 0.01, and "10.00" is refused. On POST /instructions that is a 400 whose detail names the action and ends amount 10.00 does not cover the fixed developer fee of 10.00. Send at least 10.01 or lower the fee. While drafting, the agent is told the same and asks the customer to send more, or to have the fee added on top instead.

Where the fee applies

Only where something actually converts. The server derives this; you do not declare it: Charging for bank payouts is your pricing decision. Leave offramp out and bank payouts carry no developer fee. To set a fee for one account only, put developer_fee_bps or developer_fee_fixed on its create_auto_account action, never both. The gross-up follows whichever fee applies:
or, for a fixed fee:
Not every account can charge a fixed fee. When the provider that would run the account can’t, POST /instructions refuses the action with a 400, whether the fixed fee came from developer_fee_defaults or from the action itself. Its detail names the action and ends developer_fee_fixed is not supported for this account; use developer_fee_bps. The provider is chosen on accept, so drafting cannot catch this and a draft the customer approved can still be refused here. Charge developer_fee_bps for that payout type instead. The same restriction applies to accounts created through the REST API; see Where fixed fees are available. The agent is told the fee, never the arithmetic. It can quote your fee to a customer and explain the two readings; it cannot compute a grossed figure, write one into an action, or be talked into a different fee. The number that bills is the one on your request.

One more refusal worth knowing about

An identical payment cannot be drafted twice. Same destination, same amount, same asset, same UTC day — whether the first one is still scheduled or has already gone out — is refused, and the agent is told why so it can correct itself in the same turn rather than showing your customer a second card. This exists because a conversation can revisit a request. The agent is stateless and re-reads the transcript each turn, so a follow-up like “go ahead” — sent after something unrelated was sorted out — can read as “draft all of it again”. Repeating a payment deliberately is still expressible — change the amount, or the date. What is refused is the one that is indistinguishable from an accident.

Testing a policy

Register one, run a conversation, register the next. Changes apply to the very next turn, so the loop is as tight as editing a config file.
A quick way to see the strictness is to register something wrong on purpose:
Registration is where a policy is validated, so a bad one is refused before any conversation happens rather than on a customer’s first turn.