Intent mandates

An intent mandate is the user’s cryptographically signed instruction: “Buy X under conditions Y, up to amount Z, valid until T.” Agents fulfill mandates; Lipafy validates against the signed terms.

Mandates exist because “the agent should know what I want” is a bad authorization model. Mandates make the user’s permission explicit, conditional, time-bound, and signed.

The shape

1{
2 "natural_language_description": "Order dinner from any Nairobi restaurant under 1500 KES",
3 "capability_slug_patterns": ["eats.*"],
4 "allowed_merchants": ["mama-oliech", "java-house", "kfc"],
5 "max_amount_per_fulfillment": "1500",
6 "total_budget": "3000",
7 "max_fulfillments": 2,
8 "currency_code": "KES",
9 "expires_at": "2026-05-23T18:00:00Z"
10}

Two flavors:

  • Single-shot (max_fulfillments: 1) — “buy this one thing when the conditions match.” Equivalent to AP2’s “Closed” state once fulfilled.
  • Multi-shot — “I’ll let you fulfill this up to N times, total budget Z.” Useful for monitoring (price-drop alerts), recurring buys, or budget-pooled background purchases.

The two-phase signing flow

To prevent server↔client text-format disagreements, signing is a round-trip:

Phase 1 — challenge

$POST /v1/intent-mandates/challenge
1{
2 "natural_language_description": "...",
3 "capability_slug_patterns": ["eats.*"],
4 "max_amount_per_fulfillment": "1500",
5 "total_budget": "3000",
6 "currency_code": "KES",
7 "max_fulfillments": 2,
8 "expires_at": "2026-05-23T18:00:00Z"
9}

The server responds with the exact canonical text to sign, plus a nonce:

app.lipafy.xyz is asking you to authorize an agent purchase mandate.
Intent: Order dinner from any Nairobi restaurant under 1500 KES
Capability patterns: eats.*
Allowed merchants: any registered Lipafy capability provider
Max per fulfillment: 1500 KES
Total budget: 3000 KES
Max fulfillments: 2
Expires: 2026-05-23T18:00:00Z
Nonce: 7c4f8...
Signing this message authorises agents to spend within these limits
on your behalf via Lipafy. It does not move funds on-chain.
URI: https://app.lipafy.xyz/intents/mandates
Version: 1

Phase 2 — create

The user signs that text with their wallet (personal_sign). The client posts the same parameters plus the signature and nonce:

$POST /v1/intent-mandates

The server reconstructs the exact same text from the parameters + nonce, then recoverMessageAddress-es the signature. If the recovered address is linked to the account via wallet_addresses, the mandate is stored.

This shape means the signature commits to every parameter — change a single character of the intent or pattern and the signature won’t recover correctly.

Fulfilling

Anyone with the mandate id (including the agent itself via MCP) can call:

$POST /v1/intent-mandates/<id>/fulfill
1{
2 "capability_slug": "eats.mama-oliech.order",
3 "amount": "1200",
4 "body": { "items": ["ribeye"] }
5}

Lipafy validates every condition against the stored, signed mandate:

CheckReject code
Status is open (not fulfilled, cancelled, exhausted, expired)mandate_not_open
Now < expires_atmandate_expired
currency_code matchesmandate_currency_mismatch
Amount ≤ max_amount_per_fulfillmentmandate_amount_exceeded
fulfillment_count < max_fulfillments AND consumed_amount + amount ≤ total_budgetmandate_exhausted
Slug matches a pattern in capability_slug_patternsmandate_slug_not_allowed
If allowed_merchants set: slug’s first segment is in the listmandate_merchant_not_allowed

All pass → call dispatches through the gateway like any normal capability call. The mandate’s consumed budget atomically advances inside a transaction with SELECT FOR UPDATE, so concurrent fulfillments serialise.

Where this matches Google’s AP2

AP2 termLipafy equivalent
IntentMandateintent_mandates row + signed canonical text
user_cart_confirmation_required: truemax_fulfillments: 1 (single-shot)
merchants allow-listallowed_merchants text array
intent_expiryexpires_at
Open / Closed statesopenfulfilled / exhausted
Verifiable presentationsecp256k1 wallet signature stored alongside

Lipafy adds:

  • Concrete budget arithmetic (per-fulfillment cap, total budget) so the mandate is enforceable without AI judgment
  • Multi-shot lifecycle (a single mandate can serve N fulfillments)
  • Per-account wallet binding — the signature has to come from a wallet linked to the owning account, so even a leaked mandate id isn’t enough to act

Cancelling

The user can always DELETE /v1/intent-mandates/<id> while it’s open. Once cancelled, no further fulfillments are accepted (existing capability_requests don’t reverse — past spend is past spend, not undone by cancelling future authorization).