# Intents and mandates

The payment intent a service builds, and the mandate a user approves once.

**Testnet demo setup**
This page covers the agent-pay testnet service integration. To use the hosted wallet and bundled shopping tools, add the single public URL in [Connect an AI agent](/en/docs/agents/). The format below is for developers building intents for their own registered service. Arrange registry access and testnet setup with [business@pouchi.app](mailto:business@pouchi.app) before deploying.

An **intent** is the JSON a service hands to the wallet: the calls to run now, what they spend, and the **mandate** they run under.
The user approves the mandate once. Later intents and merchant charges inside it run without a prompt. The structure follows AP2 intents and mandates.

## Three kinds

| `action` | Mandate | Example |
|---|---|---|
| `purchase` | One run. Item and price pinned with `eq`. No later charges | One cinema ticket |
| `mandate` | Many runs. Per-run, total, runs, validity, period limits | Subscriptions, reservations, metered search, savings |
| `mandate.update` | Raises limits, runs or days of the same mandate. Scope unchanged | Renewing a subscription |

## Example: a monthly subscription

```json
{
  "type": "agentpay.intent",
  "version": 1,
  "id": "int_8cVd0Hq3pTzR1b2N",
  "service": "acme-news",
  "action": "mandate",
  "summary": "Monthly plan 9,900 KRW, up to 12 months",
  "spend_okrw": 9900,
  "prefund_okrw": 9900,
  "expires_at": 1792051200,
  "calls": [
    { "target": "0x…token", "value": "0", "data": "0x095ea7b3…" },
    { "target": "0x…merchant", "value": "0", "data": "0x…" }
  ],
  "mandate": {
    "key": "acme-news:plan",
    "label": "Acme News monthly",
    "total_okrw": 118800,
    "per_okrw": 9900,
    "approve_cap_okrw": 9900,
    "calls": 12,
    "days": 360,
    "merchant_charges": true,
    "period": { "seconds": 2592000, "max_runs": 1 },
    "permissions": [{
      "target": "0x…merchant",
      "signature": "pay(bytes32,uint256,uint256)",
      "predicates": [{ "arg": 1, "op": "eq", "uint": "2" }, { "arg": 2, "op": "lte", "okrw": 9900 }]
    }]
  }
}
```

## Intent fields

| Field | Type | Rule |
|---|---|---|
| `type`, `version` | `"agentpay.intent"`, `1` | Fixed |
| `id` | `int_` + 8 to 64 chars | Set by the kit |
| `service` | Service id | Must match the service of the origin it was fetched from |
| `summary` | String | Up to 160 chars. Shown on approval and in history |
| `spend_okrw` | Integer | What the calls spend now. At most `per_okrw` |
| `prefund_okrw` | Integer, optional | Balance the wallet should hold for this mandate (top-up hint) |
| `expires_at` | Unix seconds | Within one hour. The kit uses 15 minutes |
| `calls` | 0 to 8 | `{ target, value: "0", data }`. All must fit the mandate |

Amounts (`*_okrw`) are whole payment-token units; the wallet scales them by the token `decimals`.

## Mandate fields

| Field | Default | Rule |
|---|---|---|
| `key` | | Starts with `<service id>:`. A live mandate with the same key is reused |
| `label` | | Up to 80 chars. The approval title |
| `total_okrw`, `per_okrw` | | Total and per-run limit. `per_okrw ≤ total_okrw` |
| `approve_cap_okrw` | | Most one payment-token `approve` may allow. At most `total_okrw` |
| `calls` | 100 | Runs, 1 to 1000 |
| `days` | 30 | Validity, 1 to 365 days |
| `valid_after` | Approval time | Start time (reservations). At most 300 days ahead |
| `merchant_charges` | `false` | The service may charge later. Only for services allowed in the registry |
| `period` | None | `{ seconds, max_runs?, max_spend_okrw? }`. Fixed windows from `validAfter` |
| `permissions` | | 1 to 16. Only functions on the service's own registered contracts |

### Predicates

`permissions[].predicates` constrain one 32-byte argument word each, up to 4 per permission.

| Field | Meaning |
|---|---|
| `arg` | Zero-based argument index |
| `op` | `eq` or `lte` |
| One value | `okrw` (token units, scaled by decimals), `uint`, `bytes32` or `address` |

A `purchase` must pin its item with at least one `eq` predicate.

## Changing a mandate (`mandate.update`)

```json
{ "action": "mandate.update", "spend_okrw": 0, "calls": [],
  "mandate": { "key": "acme-news:plan" },
  "update": { "add_total_okrw": 29700, "add_calls": 3, "add_days": 90 } }
```

No calls run during this update. `add_total_okrw`, `add_calls` and `add_days` increase the corresponding limits; optional `per_okrw` raises the per-run limit. Target contracts, functions, argument predicates and period limits do not change, and the current period usage is not reset. A different scope or period needs a new mandate. The user reviews and approves changes with `change_mandate`.

## Merchant charge requests

A payment with no user present. The service sends `POST /merchant/charges` to the gateway.

```json
{ "mandate_id": "mnd_…", "charge_id": "sub_123-w1", "summary": "Monthly plan, month 2",
  "spend_okrw": 9900, "calls": [ … ] }
```

```http
Authorization: AgentPay-Mandate-HMAC id=mcr_…,ts=1792051200,sig=…
```

The signature is HMAC-SHA256 over `METHOD\npath\nts\nsha256(body)`, and the request timestamp may differ from the verification time by at most 300 seconds (five minutes) in either direction. `kit.merchant.charge` does this for you.

## Validation errors

| Code | Meaning |
|---|---|
| `INTENT_SERVICE_MISMATCH` | The intent's `service` differs from the origin's service |
| `CONTRACT_NOT_REGISTERED` | A permission targets a contract not registered to the service |
| `INTENT_CALL_OUTSIDE_MANDATE` | A call to run now is outside the mandate |
| `MERCHANT_CHARGES_NOT_ALLOWED` | The service may not charge later |
| `INTENT_EXPIRED` | Expired, or set to live longer than an hour |
| `OUTSIDE_MANDATE`, `OVER_LIMIT`, `PERIOD_LIMIT` | A charge exceeds the scope, limits or window |
| `MANDATE_REVOKED`, `MANDATE_RELEASED`, `MANDATE_EXPIRED` | The mandate is off or over |
| `CHARGE_ID_REUSED` | The same `charge_id` was sent with different content |

Run the kit's `validateIntent` before serving an intent: it applies the same rules as the gateway.
