> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Client Policy

> Make the agent speak your product's language, and refuse what your product refuses

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.

<Info>
  **Alpha, 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.
</Info>

## The shape

```json theme={null}
{
  "payee_model": "flat",
  "labels": {
    "limit": "spending limit",
    "payee": "recipient",
    "limit_unit": "USD"
  },
  "mandate_strategy": "external_only",
  "payout_route": "conversion_account_only",
  "payout_assets": ["USDC", "USDT"]
}
```

| Field              | What it changes                                                                                                                                                                   |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payee_model`      | `nested` (default) or `flat` — whether a person with two payout methods is one thing or two. [See what changes.](#payee-model-how-many-things-a-payee-with-two-payout-methods-is) |
| `labels`           | Your nouns for platform concepts. Applied to the prompt, the tool descriptions, **and the tool results** — so the model reasons in your words, not just writes in them.           |
| `mandate_strategy` | `external_only` means spending limits are created and amended in **your** app. The agent never drafts one.                                                                        |
| `payout_route`     | `conversion_account_only` or `bank_only`. Constrains how money leaves.                                                                                                            |
| `payout_assets`    | What a payee may **receive**. Empty means unrestricted.                                                                                                                           |

**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.

```json theme={null}
{
  "detail": "client_policy has unknown key: payout_method — this server would silently ignore it, so it is rejected instead. Supported keys: payee_model, labels, mandate_strategy, payout_route, payout_assets"
}
```

## 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.

```bash theme={null}
curl -X PUT https://api.dakota.xyz/agentic-policy \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{"payee_model":"flat","mandate_strategy":"external_only"}'
```

**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.

| Field              | Values                                          | Default                            |
| ------------------ | ----------------------------------------------- | ---------------------------------- |
| `payee_model`      | `nested`, `flat`                                | `nested`                           |
| `mandate_strategy` | `external_only`                                 | unset — the agent may draft limits |
| `payout_route`     | `conversion_account_only`, `bank_only`          | unset — no restriction             |
| `payout_assets`    | asset codes, checked against the asset registry | `[]` — unrestricted                |
| `labels`           | keys `limit`, `payee`, `limit_unit`             | none — platform's own nouns        |

### `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:

<CodeGroup>
  ```text nested — one Acme, two ways to pay them theme={null}
  Recipients
  ──────────────────────────────────────────────
  Acme
    · Chase ••••4321 — ACH, Fedwire
    · 0x2Bd9…23b5 — Base
  ```

  ```text flat — two Acmes, one way each theme={null}
  Recipients
  ──────────────────────────────────────────────
  Acme          Chase ••••4321 — ACH, Fedwire
  Acme          0x2Bd9…23b5 — Base
  ```
</CodeGroup>

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:

<CodeGroup>
  ```json nested (default) theme={null}
  { "found": true,
    "recipient_id": "2YBd…",
    "name": "Acme",
    "has_postal_address": true,
    "destinations": [
      { "destination_id": "2YBf…", "kind": "bank",
        "bank_name": "Chase", "account_last4": "4321", "rails": ["ach", "fedwire"] },
      { "destination_id": "2YBg…", "kind": "crypto",
        "network_id": "base-mainnet", "address": "0x2Bd9…", "family": "evm" } ] }
  ```

  ```json flat theme={null}
  { "found": true,
    "entity_ref": "2YBd…",
    "name": "Acme",
    "has_postal_address": true,
    "payees": [
      { "payee_ref": "2YBf…", "entity_ref": "2YBd…", "name": "Acme",
        "paid_by": "bank account at Chase ****4321", "rails": ["ach", "fedwire"] },
      { "payee_ref": "2YBg…", "entity_ref": "2YBd…", "name": "Acme",
        "paid_by": "USDC on Base" } ] }
  ```
</CodeGroup>

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](#blockers); your app collects the signature in its own
  limit editor. `create_mandate` in a proposal is refused at accept:

```json theme={null}
{ "detail": "proposal 0: action 1 (create_mandate): spending limits are managed in the app, not drafted here — schedule under the customer's existing spending limit if it covers, otherwise tell the customer to set or raise their spending limit in the app and propose nothing" }
```

### `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`.

| Key          | Renames                                       | Charset                      |
| ------------ | --------------------------------------------- | ---------------------------- |
| `limit`      | the mandate                                   | `^[a-z][a-z0-9 _-]{0,30}$`   |
| `payee`      | the recipient                                 | same                         |
| `limit_unit` | *not a noun* — the unit amounts are quoted in | `^[A-Za-z][A-Za-z0-9]{0,9}$` |

Labels rewrite the **tool results**, so the model never learns platform's word in
the first place. With `{"limit": "spending limit"}`, `active_mandates` returns:

<CodeGroup>
  ```json no labels theme={null}
  { "mandates": [{
      "mandate_id": "2YBXZIXleX6FRXadUNswngpULtn",
      "rule": { "asset": "RD", "network_id": "base-mainnet",
                "target_type": "any", "max_per_tx": "1000", "window": "MONTHLY" } }] }
  ```

  ```json {"limit":"spending limit","limit_unit":"USD"} theme={null}
  { "spending_limit": [{
      "spending_limit_id": "2YBXZIXleX6FRXadUNswngpULtn",
      "amounts_in": "USD",
      "rule": { "asset": "RD", "network_id": "base-mainnet",
                "target_type": "any", "max_per_tx": "1000", "window": "MONTHLY" } }] }
  ```
</CodeGroup>

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.

<Note>
  `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.
</Note>

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.

```json theme={null}
{
  "proposals": [{
    "summary": "Add Priya Raman as a recipient with their Base Sepolia address.",
    "actions": [
      { "type": "create_recipient", "create_recipient": { "name": "Priya Raman" } },
      { "type": "create_crypto_destination", "create_crypto_destination": {
          "address": "0x2Bd9aA3f1e4C7b8f2A55949038A9610F50FB23b5",
          "network_id": "base-sepolia" } }
    ]
  }],
  "reply": "Priya isn't on your spending limit, so I can't schedule this yet — I've set her up so you can add her.",
  "blockers": [{
    "code": "mandate_does_not_cover_payee",
    "mandate_id": "3HGkONJUqqacVhatIqZkvndPQcX",
    "payee_name": "Priya Raman"
  }]
}
```

| Code                           | Meaning                                                      | What to do                                                                                          |
| ------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `mandate_does_not_cover_payee` | A limit is in force, but its rule does not reach this payee. | **Actionable.** Amend that limit to *add* the payee, collect the signature, and run the turn again. |
| `no_mandate`                   | No active limit at all.                                      | Nothing to amend — the customer establishes one first.                                              |

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.

<Info>
  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.
</Info>

### The full loop

```mermaid theme={null}
sequenceDiagram
    participant C as Customer
    participant A as Your app
    participant P as Dakota

    C->>A: "pay Priya 5 USDC"
    A->>P: POST /payment-agents/{id}/proposals
    P-->>A: proposal (create payee) + blocker
    A->>P: POST /instructions (accept the payee)
    P-->>A: recipient_id
    Note over A,C: your limit editor, prefilled with Priya
    C->>A: signs the amendment
    A->>P: POST /mandates/{id}/amend
    A->>P: POST .../proposals ("I've added Priya — go ahead")
    P-->>A: proposal WITH the payment, no blocker
    A->>P: POST /instructions
```

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](/documentation/agentic-payments/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:

```json theme={null}
{
  "payee_model": "flat",
  "labels": { "limit": "spending limit", "payee": "recipient", "limit_unit": "USD" },
  "mandate_strategy": "external_only",
  "payout_route": "conversion_account_only",
  "payout_assets": ["USDC", "USDT"]
}
```

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_bps` 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.

**By default `amount` is what the payee RECEIVES.** The server adds the fee on
top, so slightly more leaves the funding wallet:

```json theme={null}
{ "type": "create_scheduled_payments",
  "create_scheduled_payments": { "amount": "1.20", "asset": "RD", … } }
```

At 1000 bps that actuates as **1.333334** leaving the 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.

**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.

### Where the fee applies

Only where something actually converts. The server derives this; you do not
declare it:

| Shape                                                | Fee                                              |
| ---------------------------------------------------- | ------------------------------------------------ |
| `create_auto_account` with no `rail` — a swap        | your `developer_fee_bps`                         |
| `create_auto_account` with a `rail` — a bank offramp | **none**                                         |
| no `create_auto_account` — a direct payment          | **none**                                         |
| `fee_bps` set on the action                          | that value, **wins outright** — offramp included |

A bank payout carries no conversion fee because the fee *is* a conversion fee;
charging it would make the same payment cost more through an agent than by hand.
If you do charge for payouts, say so with the action's own `fee_bps` — it
overrides the rail rule, and the gross-up follows it.

**The agent is told the rate, 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 rate. 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.

```bash theme={null}
# try it
curl -X PUT https://api.dakota.xyz/agentic-policy \
  -H "X-API-Key: $DAKOTA_API_KEY" -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{"payee_model":"flat","labels":{"payee":"vendor"}}'

# ... talk to the agent ...

# back to platform defaults
curl -X PUT https://api.dakota.xyz/agentic-policy \
  -H "X-API-Key: $DAKOTA_API_KEY" -H "X-Idempotency-Key: $(uuidgen)" -d '{}'
```

A quick way to see the strictness is to register something wrong on purpose:

```bash theme={null}
curl -X PUT https://api.dakota.xyz/agentic-policy \
  -H "X-API-Key: $DAKOTA_API_KEY" -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{"payee_model":"totally_bogus"}'
```

```json theme={null}
{ "detail": "value is not one of the allowed values [\"\",\"nested\",\"flat\"]" }
```

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.
