What Is a Payment Mandate? Cryptographic Intent in Agentic Commerce

What Is a Payment Mandate? Cryptographic Intent in Agentic Commerce

Introduction

An AI shopping agent finds the cheapest flight, books it, and charges your card. A subscription bot pays your monthly streaming service. A travel agent negotiates a hotel rate and confirms the booking before you wake up. These flows all share the same hard problem: how does the payment network know the agent was actually authorized to spend your money, and how does it stop a compromised agent from spending more than you allowed?

The answer that AP2, Mastercard, and Visa have converged on is the payment mandate, a cryptographically signed artifact that encodes user intent. A mandate is small, signed, scoped, and verifiable. It says, in machine-readable form: "I, the user, authorize this specific agent to spend up to this amount at this merchant before this date." Every transaction the agent initiates must present a valid mandate, and every payment processor must validate it before settlement.

This article explains what payment mandates are, what fields they carry, how they get signed and validated, and which attacks target them. Then we build a real-time mandate validation pipeline in RisingWave using streaming SQL. The pipeline is verified end to end against RisingWave v2.8.0, with real query output included.

What Is a Payment Mandate?

A payment mandate is a cryptographically signed user-intent artifact. It is the agentic-commerce analogue of a check that you have pre-signed, with hard limits on who can cash it, where, how much, and until when.

The concept appears across the major agentic payment frameworks under different names:

  • AP2 (Agent Payments Protocol) calls them Mandates, the core trust primitive of the protocol. AP2 splits them further into Intent Mandates (authorizing the agent to act) and Cart Mandates (authorizing a specific transaction). See the AP2 specification.
  • Mastercard's Agent Pay uses the term Verifiable Intent, signed by the user's wallet and presented during the authorization request.
  • Visa Intelligent Commerce uses an analogous credential carried in network messages that proves the agent acted within user-granted scope.

The shared idea: the user signs intent once, the agent presents that signature on every spend, and the payment network validates the signature plus the scope before each transaction settles. The mandate is the source of truth for "did the user actually authorize this?" Compare this to traditional card-on-file flows, where a stored token simply lets the merchant charge again with no per-transaction proof of fresh user intent.

Mandates matter because agentic commerce removes the human from the per-transaction loop. With no human clicking "Pay," you need a different artifact to carry that consent forward in a way every party can independently verify.

Anatomy of a Mandate

A useful mandate has at minimum the following fields. Different specifications use slightly different names, but the substance is the same.

FieldPurpose
mandate_idUnique identifier, used for replay detection and revocation
issuerThe party signing the mandate (typically the user's wallet)
subject (agent_id)The agent authorized to act
user_idThe end user on whose behalf the agent acts
scope_merchantAllowed merchant or merchant category
max_amountPer-transaction or aggregate ceiling
max_usesNumber of times the mandate can be redeemed
valid_from / valid_toValidity window
signatureDigital signature over the canonicalized payload
statusCurrent state: ACTIVE, REVOKED, EXHAUSTED
created_atIssuance timestamp

Signing

The user's wallet hashes the canonical mandate body and signs the hash with the user's private key. Spec implementations typically lean on JSON Web Signatures (JWS), JSON Web Tokens, or W3C Verifiable Credentials as the wire format. Key custody usually rides on hardware-backed authenticators in the spirit of FIDO2/WebAuthn, so the signing key never leaves a secure element.

Scope

Scope is the most important and most attack-prone part of a mandate. A well-scoped mandate says exactly what the agent may do:

  • Merchant scope can be a single domain (booking.com), a category (grocery), or an allowlist.
  • Amount scope is usually a per-transaction cap, sometimes paired with a rolling spend ceiling.
  • Frequency scope caps the number of redemptions and may include cool-downs (no more than once per 24 hours).
  • Temporal scope is a validity window, often short for one-shot flows and longer for subscriptions.

Tightly scoped mandates contain blast radius. If an agent gets compromised, the attacker can only do what the mandate already permits.

Mandate-payment binding

A mandate authorizes intent, but the payment network still needs to bind that intent to a specific payment instrument. The standard pattern: the mandate references a payment method handle (a network token or a tokenized card credential), and the authorization request carries both the mandate and the payment token. The processor then checks that the mandate's subject matches the agent presenting it and that the merchant and amount fall inside scope before signing the authorization.

How Mandates Are Validated

When an agent initiates a payment, the processor (or, in many designs, a verifier service that sits in front of the processor) runs five checks before authorizing the charge. Each check has a specific failure mode it defends against.

  1. Signature verification. Recompute the canonical hash of the mandate body and verify the signature against the user's public key. Failure means the mandate is forged or has been tampered with after issuance.
  2. Scope check. Compare the live transaction's merchant, amount, and currency to the mandate's scope. Failure means scope escalation, which is the most common attack vector against agents whose context window has been poisoned.
  3. Expiry check. Confirm the current time falls inside [valid_from, valid_to] and that max_uses has not been exhausted. Failure means stale-mandate replay, often surfaced after a credential leak.
  4. Replay protection. Each mandate_id redemption should be tracked. A mandate marked single-use must be rejected on the second presentation. Even multi-use mandates are vulnerable to short-window replay if the attacker captures an in-flight request.
  5. Mandate-payment match. The agent presenting the mandate must match the subject, and the payment instrument must match the bound token. Failure means another agent has captured the mandate.

These checks need to happen on every single transaction, in milliseconds, at payment-network throughput. That is the constraint that makes streaming SQL a natural fit, which we get to below. For background on real-time financial verification at scale, see our walkthrough of payment fraud detection with streaming SQL and the broader pattern of real-time risk that BNPL providers like Atome rely on for sub-second decisions on millions of consumer credit assessments.

Common Mandate Attacks and How to Detect Them

Four categories of attacks recur across every agentic-payment threat model. Each one maps to a specific validation rule.

Replay attacks

The attacker captures a legitimately issued, validly signed mandate use request and re-submits it. Without per-mandate replay tracking, the same mandate can drain the user repeatedly until the validity window closes. The defense is a server-side ledger of (mandate_id, attempt_id) pairs and short-window deduplication on (mandate_id, agent_id, merchant, amount) to catch packet-level replay. Some specs additionally require a per-transaction nonce inside the mandate-bound payload.

Scope escalation

A compromised or prompt-injected agent attempts to spend at a merchant outside scope or above the amount cap. This is the dominant LLM-agent failure mode: the language model "misreads" instructions, often because an attacker has slipped untrusted content into a tool result, and tries to spend on behalf of the user in ways the user never authorized. The defense is strict server-side enforcement of merchant, amount, and currency scope. The agent's own claims about scope are not trusted, the mandate is.

Expired mandate use

An agent (or attacker who scraped the agent's storage) presents a mandate after valid_to. Defense is a hard timestamp check. This sounds trivial, but production systems get bitten when the mandate-issuance clock and the validation clock disagree, or when a long-running agent caches a mandate and tries to use it after a session boundary.

Mandate forgery

An attacker constructs a mandate from scratch or splices fields from valid mandates. Defense is signature verification rooted in a known public key, plus tying the subject field to the agent identity that actually shows up in the authorization request. The agent_mismatch failure flag in the validator below catches one form of this: a mandate issued to agent A being presented by agent B.

A defensive validator catches all four classes in the same materialized view because the rules compose: signature, scope, expiry, status, and per-mandate replay are all simple boolean predicates over the joined mandate-attempt stream.

Real-Time Mandate Validation with Streaming SQL

Validation has to happen on every payment attempt, in milliseconds. A streaming database is well suited to this: the mandate registry is a slowly changing table, the attempt log is a high-velocity stream, and validation is a join with a few CASE expressions. RisingWave maintains the result as a materialized view that updates incrementally as new attempts arrive.

All SQL below was executed against RisingWave v2.8.0. Object names are prefixed with aap05_ to keep the example sandboxed.

Define the schema

CREATE TABLE aap05_mandates (
    mandate_id      VARCHAR PRIMARY KEY,
    agent_id        VARCHAR NOT NULL,
    user_id         VARCHAR NOT NULL,
    scope_merchant  VARCHAR NOT NULL,
    max_amount      DECIMAL NOT NULL,
    max_uses        INT     NOT NULL,
    valid_from      TIMESTAMPTZ NOT NULL,
    valid_to        TIMESTAMPTZ NOT NULL,
    signature_hash  VARCHAR NOT NULL,
    status          VARCHAR NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL
);

CREATE TABLE aap05_mandate_use_attempts (
    attempt_id    VARCHAR PRIMARY KEY,
    mandate_id    VARCHAR NOT NULL,
    agent_id      VARCHAR NOT NULL,
    merchant      VARCHAR NOT NULL,
    amount        DECIMAL NOT NULL,
    attempt_time  TIMESTAMPTZ NOT NULL
);

In production, aap05_mandates would be a CDC source backed by your wallet service's database, and aap05_mandate_use_attempts would be a Kafka source ingesting the live stream of agent payment attempts. The signature_hash column stores the verified signature digest for audit.

Load mandates and attempts

We load five mandates that exercise the interesting cases (different merchants, amount caps, validity windows, and one revoked mandate), then 14 use attempts that include valid charges, scope violations, an expired mandate use, an agent mismatch, and a replay burst.

INSERT INTO aap05_mandates VALUES
    ('mnd_001', 'agt_shopper_a',  'usr_alice', 'amazon.com',     200.00, 3,
     '2026-05-01 00:00:00+00', '2026-05-31 23:59:59+00', 'sig_a1f9e2c0', 'ACTIVE',
     '2026-05-01 09:00:00+00'),
    ('mnd_002', 'agt_grocer_b',   'usr_bob',   'wholefoods.com', 150.00, 5,
     '2026-05-01 00:00:00+00', '2026-05-15 23:59:59+00', 'sig_b2e8d3a1', 'ACTIVE',
     '2026-05-01 10:00:00+00'),
    ('mnd_003', 'agt_travel_c',   'usr_carol', 'booking.com',   1200.00, 1,
     '2026-04-01 00:00:00+00', '2026-04-30 23:59:59+00', 'sig_c3d7e4b2', 'ACTIVE',
     '2026-04-01 11:00:00+00'),
    ('mnd_004', 'agt_subscribe_d','usr_dan',   'spotify.com',     11.99, 12,
     '2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00', 'sig_d4c6f5a3', 'ACTIVE',
     '2026-01-01 12:00:00+00'),
    ('mnd_005', 'agt_gift_e',     'usr_eve',   'etsy.com',        80.00, 2,
     '2026-05-05 00:00:00+00', '2026-05-10 23:59:59+00', 'sig_e5b5a6c4', 'REVOKED',
     '2026-05-04 13:00:00+00');

INSERT INTO aap05_mandate_use_attempts VALUES
    ('att_001', 'mnd_001', 'agt_shopper_a',  'amazon.com',      89.50, '2026-05-06 10:00:00+00'),
    ('att_002', 'mnd_001', 'agt_shopper_a',  'amazon.com',     142.00, '2026-05-06 14:30:00+00'),
    ('att_003', 'mnd_001', 'agt_shopper_a',  'ebay.com',        50.00, '2026-05-06 15:00:00+00'),
    ('att_004', 'mnd_001', 'agt_shopper_a',  'amazon.com',     250.00, '2026-05-06 16:00:00+00'),
    ('att_005', 'mnd_002', 'agt_grocer_b',   'wholefoods.com',  78.20, '2026-05-05 09:00:00+00'),
    ('att_006', 'mnd_002', 'agt_grocer_b',   'wholefoods.com',  92.10, '2026-05-06 09:00:00+00'),
    ('att_007', 'mnd_003', 'agt_travel_c',   'booking.com',    980.00, '2026-05-02 08:00:00+00'),
    ('att_008', 'mnd_004', 'agt_subscribe_d','spotify.com',     11.99, '2026-05-01 12:00:00+00'),
    ('att_009', 'mnd_004', 'agt_subscribe_d','spotify.com',     19.99, '2026-05-02 12:00:00+00'),
    ('att_010', 'mnd_001', 'agt_shopper_a',  'amazon.com',     142.00, '2026-05-06 14:30:02+00'),
    ('att_011', 'mnd_001', 'agt_shopper_a',  'amazon.com',     142.00, '2026-05-06 14:30:05+00'),
    ('att_012', 'mnd_005', 'agt_gift_e',     'etsy.com',        45.00, '2026-05-06 17:00:00+00'),
    ('att_013', 'mnd_002', 'agt_grocer_b',   'wholefoods.com',  30.00, '2026-04-29 09:00:00+00'),
    ('att_014', 'mnd_002', 'agt_shopper_a',  'wholefoods.com',  40.00, '2026-05-06 09:30:00+00');

Build the validation materialized view

This view joins each attempt to its mandate and runs the full validation cascade in a single CASE expression. The order of the WHEN branches matters: status, time, merchant, amount, agent. The reason column tells the caller exactly which rule rejected the attempt.

CREATE MATERIALIZED VIEW aap05_validation_results AS
SELECT
    a.attempt_id,
    a.mandate_id,
    a.agent_id     AS attempt_agent,
    m.agent_id     AS mandate_agent,
    a.merchant,
    m.scope_merchant,
    a.amount,
    m.max_amount,
    a.attempt_time,
    m.valid_from,
    m.valid_to,
    m.status       AS mandate_status,
    CASE
        WHEN m.mandate_id IS NULL                  THEN 'FAIL'
        WHEN m.status <> 'ACTIVE'                  THEN 'FAIL'
        WHEN a.attempt_time < m.valid_from         THEN 'FAIL'
        WHEN a.attempt_time > m.valid_to           THEN 'FAIL'
        WHEN a.merchant <> m.scope_merchant        THEN 'FAIL'
        WHEN a.amount   > m.max_amount             THEN 'FAIL'
        WHEN a.agent_id <> m.agent_id              THEN 'FAIL'
        ELSE 'PASS'
    END AS result,
    CASE
        WHEN m.mandate_id IS NULL                  THEN 'unknown_mandate'
        WHEN m.status <> 'ACTIVE'                  THEN 'mandate_not_active'
        WHEN a.attempt_time < m.valid_from         THEN 'before_valid_from'
        WHEN a.attempt_time > m.valid_to           THEN 'expired_mandate'
        WHEN a.merchant <> m.scope_merchant        THEN 'scope_violation_merchant'
        WHEN a.amount   > m.max_amount             THEN 'scope_violation_amount'
        WHEN a.agent_id <> m.agent_id              THEN 'agent_mismatch'
        ELSE 'ok'
    END AS reason
FROM aap05_mandate_use_attempts a
LEFT JOIN aap05_mandates m ON a.mandate_id = m.mandate_id;

Verified output

 attempt_id | mandate_id |    merchant    | amount | result |          reason
------------+------------+----------------+--------+--------+--------------------------
 att_001    | mnd_001    | amazon.com     |  89.50 | PASS   | ok
 att_002    | mnd_001    | amazon.com     | 142.00 | PASS   | ok
 att_003    | mnd_001    | ebay.com       |  50.00 | FAIL   | scope_violation_merchant
 att_004    | mnd_001    | amazon.com     | 250.00 | FAIL   | scope_violation_amount
 att_005    | mnd_002    | wholefoods.com |  78.20 | PASS   | ok
 att_006    | mnd_002    | wholefoods.com |  92.10 | PASS   | ok
 att_007    | mnd_003    | booking.com    | 980.00 | FAIL   | expired_mandate
 att_008    | mnd_004    | spotify.com    |  11.99 | PASS   | ok
 att_009    | mnd_004    | spotify.com    |  19.99 | FAIL   | scope_violation_amount
 att_010    | mnd_001    | amazon.com     | 142.00 | PASS   | ok
 att_011    | mnd_001    | amazon.com     | 142.00 | PASS   | ok
 att_012    | mnd_005    | etsy.com       |  45.00 | FAIL   | mandate_not_active
 att_013    | mnd_002    | wholefoods.com |  30.00 | FAIL   | before_valid_from
 att_014    | mnd_002    | wholefoods.com |  40.00 | FAIL   | agent_mismatch
(14 rows)

Seven attempts pass, seven fail, each with a precise reason. Notice that att_010 and att_011 pass scope validation (they are within mandate mnd_001's merchant and amount), so a single-rule validator misses them. They are still suspicious because they duplicate the amount of att_002 within seconds. That is replay, and we catch it in a second materialized view.

Build the anomalies view

aap05_anomalies unions two anomaly classes: hard validation failures, and replay candidates detected by self-joining the validation stream on (mandate_id, agent_id, merchant, amount) with a 60-second window.

CREATE MATERIALIZED VIEW aap05_anomalies AS
SELECT
    'validation_failure' AS anomaly_type,
    attempt_id, mandate_id, attempt_agent AS agent_id,
    merchant, amount, attempt_time,
    reason AS detail
FROM aap05_validation_results
WHERE result = 'FAIL'
UNION
SELECT
    'replay_candidate' AS anomaly_type,
    later.attempt_id, later.mandate_id, later.attempt_agent AS agent_id,
    later.merchant, later.amount, later.attempt_time,
    'duplicate_amount_within_60s' AS detail
FROM aap05_validation_results later
JOIN aap05_validation_results earlier
  ON  later.mandate_id    = earlier.mandate_id
  AND later.attempt_agent = earlier.attempt_agent
  AND later.merchant      = earlier.merchant
  AND later.amount        = earlier.amount
  AND later.attempt_id   <> earlier.attempt_id
  AND later.attempt_time  > earlier.attempt_time
  AND later.attempt_time - earlier.attempt_time <= INTERVAL '60 seconds';

The self-join compares each attempt to every earlier attempt with the same (mandate, agent, merchant, amount) and a time delta under one minute. RisingWave maintains this incrementally: when a new attempt lands, it joins only against recent matching rows already materialized.

Verified output

    anomaly_type    | attempt_id | mandate_id |    merchant    | amount |           detail
--------------------+------------+------------+----------------+--------+-----------------------------
 replay_candidate   | att_010    | mnd_001    | amazon.com     |    142 | duplicate_amount_within_60s
 replay_candidate   | att_011    | mnd_001    | amazon.com     |    142 | duplicate_amount_within_60s
 validation_failure | att_003    | mnd_001    | ebay.com       |     50 | scope_violation_merchant
 validation_failure | att_004    | mnd_001    | amazon.com     |    250 | scope_violation_amount
 validation_failure | att_007    | mnd_003    | booking.com    |    980 | expired_mandate
 validation_failure | att_009    | mnd_004    | spotify.com    |  19.99 | scope_violation_amount
 validation_failure | att_012    | mnd_005    | etsy.com       |     45 | mandate_not_active
 validation_failure | att_013    | mnd_002    | wholefoods.com |     30 | before_valid_from
 validation_failure | att_014    | mnd_002    | wholefoods.com |     40 | agent_mismatch
(9 rows)

The anomaly view surfaces nine items: seven distinct validation failures and two replay candidates. The replays (att_010, att_011) duplicate att_002's amount within five seconds, a near-certain signal of an in-flight capture-and-replay against mnd_001. None of the seven failures are replays, and none of the two replays would have been caught by the per-rule validator alone.

Read summary metrics

A small aggregation tells you the shape of incoming traffic at a glance, and is the kind of view an operations dashboard would subscribe to.

SELECT result, reason, COUNT(*) AS n
FROM aap05_validation_results
GROUP BY result, reason
ORDER BY result, reason;
 result |          reason          | n
--------+--------------------------+---
 FAIL   | agent_mismatch           | 1
 FAIL   | before_valid_from        | 1
 FAIL   | expired_mandate          | 1
 FAIL   | mandate_not_active       | 1
 FAIL   | scope_violation_amount   | 2
 FAIL   | scope_violation_merchant | 1
 PASS   | ok                       | 7
(7 rows)

Half of the simulated traffic was rejected, with scope violations as the most common failure mode. This is exactly what you would expect to see in early-stage agentic-commerce deployments where prompt-injection-style scope drift is common, and it is the kind of metric that should drive both alerting and per-agent risk scoring.

Wiring this into production

Three things change when you take this from the example above to a payment-network deployment:

  1. The aap05_mandates table becomes a CDC source from your wallet's mandate registry. Revocations propagate to the validator within milliseconds.
  2. The aap05_mandate_use_attempts table becomes a Kafka source fed by your authorization gateway.
  3. The aap05_anomalies view becomes a Kafka sink that pushes failures and replay candidates to a real-time risk service. High-severity failures (replay, agent mismatch) trigger an immediate decline; lower-severity failures (over-amount on a subscription mandate) might require step-up authentication.

The same pattern underlies how real-time risk teams at companies like Atome handle BNPL approvals at scale: an authoritative registry of customer commitments, joined to a live event stream, with materialized views that produce per-event verdicts in milliseconds.

FAQ

What is a payment mandate?

A payment mandate is a cryptographically signed artifact that captures a user's intent to authorize an AI agent to make payments on their behalf within a specific scope. It is a verifiable credential that includes the issuer (the user or their wallet), the subject (the agent), a scope (allowed merchant, maximum amount, frequency, validity window), a digital signature, and an expiry timestamp. The mandate accompanies every payment attempt so the network can independently confirm the user actually authorized this exact action.

How are payment mandates different from OAuth tokens?

OAuth tokens grant access to APIs and are typically scoped to read or write operations against a resource server. Payment mandates encode user intent for financial actions: who can spend, where, how much, and for how long. Mandates carry richer scope semantics, are cryptographically signed by the user-controlled wallet rather than an authorization server, and are designed to be presented to a payment network and validated end-to-end on every transaction. OAuth tokens authorize "this client may call this endpoint." Mandates authorize "this agent may move this much of this user's money under these specific conditions."

What attacks target payment mandates?

The four most common threats are mandate replay (re-submitting a captured valid mandate to charge the user twice), scope escalation (using a mandate at a merchant or amount outside what the user authorized), expired mandate use (presenting a mandate after its validity window has closed), and mandate forgery (an unauthorized agent presenting a mandate intended for a different agent). Replays are caught with mandate-id ledgers and short-window deduplication on the attempt stream. Scope escalation is caught with strict server-side scope checks. Expired use is caught with timestamp validation. Forgery is caught with signature verification rooted in the user's public key.

How can streaming SQL validate mandates in real time?

A streaming database joins the live stream of mandate use attempts to the mandate registry and applies signature, scope, expiry, and replay rules in materialized views that update incrementally. Each attempt receives a PASS or FAIL verdict in milliseconds, and a second materialized view aggregates failures and replay candidates for downstream alerting. Because the validator runs as SQL rather than as bespoke Java or Scala code, the rules are auditable, easy to extend, and share the same engine as the rest of your real-time risk stack.

Conclusion

Payment mandates are the trust primitive of agentic commerce. They turn user consent into a verifiable artifact that travels with every transaction, so a payment network can decide in milliseconds whether to honor an agent's request. The core of a working mandate system is small: a registry of signed mandates, a stream of use attempts, and a validator that joins them under five rules. The hard part is doing this at payment-network throughput with sub-second latency, which is exactly the workload streaming databases were built for.

If you are designing or operating an agentic-commerce stack, the mandate validator above is a working starting point. Drop your real schema into the same shape, swap in a CDC source for the mandate registry and a Kafka source for the attempt stream, and the rest of the pipeline is unchanged.

Ready to validate agentic mandates in real time? Try RisingWave Cloud free and stand up the validator on your own data in minutes.

Join our Slack community for help building agentic payment infrastructure.

Best-in-Class Event Streaming
for Agents, Apps, and Analytics
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.