Intents and mandates
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
Section titled “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
Section titled “Example: a monthly subscription”{ "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
Section titled “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
Section titled “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
Section titled “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)
Section titled “Changing a mandate (mandate.update)”{ "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
Section titled “Merchant charge requests”A payment with no user present. The service sends POST /merchant/charges to the gateway.
{ "mandate_id": "mnd_…", "charge_id": "sub_123-w1", "summary": "Monthly plan, month 2", "spend_okrw": 9900, "calls": [ … ] }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
Section titled “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 |