Scoped Mandate — one mandate, many purchases, one SCA

An AI agent shops on the customer's behalf. The customer grants the bank a limited mandate once, with Face ID; from then on the bank checks every payment against limits, merchants and validity, the merchant checks the product categories and signs an attestation, and the agent acts with its own identity and key — an identity the operator issues after the customer has identified there with the Konto Karte. The wallet never decides about payments; it shows the bank's state and can revoke.

Target picture: ARF, PaSO, SPAA   Interface Specification   API reference

At a glance

Three pictures, the whole idea: ① the customer allows once — that is the only approval. ② The agent shops without further prompts, within the limits. ③ Merchant and bank say no when something does not fit — and the customer always has the last word.

%%{init: {'look':'handDrawn','theme':'base','themeVariables':{
  'fontFamily':'"Comic Neue","Patrick Hand",cursive','fontSize':'17px',
  'primaryColor':'#ffffff','primaryBorderColor':'#3a3e45','primaryTextColor':'#2b2f36',
  'lineColor':'#3a3e45','clusterBkg':'#fbfbfc','clusterBorder':'#b4b9c1','edgeLabelBackground':'#ffffff','titleColor':'#2b2f36'}}}%%
flowchart TB
  subgraph P1["① Allow once"]
    direction LR
    N1["👤 Customer
“My agent may buy groceries:
up to €60 per purchase, €300 in total.”"] W1["📱 Wallet
shows everything —
Face ID, a single time"] B1["🏦 Bank
holds the mandate
and the limits"] N1 --> W1 --> B1 end subgraph P2["② The agent shops"] direction LR A2["🤖 Agent
“2 milk, bread, dish soap”"] H2["🧺 Kiezmarkt
checks the categories,
stamps the basket"] B2["🏦 Bank
checks the limits,
pays via SCT Inst"] R2["🧾 Receipt
to the customer"] A2 --> H2 --> B2 --> R2 end subgraph P3["③ Limits hold"] direction LR X3["🍺 “… and a beer?”"] -- “No”, says the merchant --> H3["🛒"] Y3["💸 More than €60?"] -- “No”, says the bank --> B3["🏦"] Z3["👤 “Revoke!”"] -- any time --> B4["🏦 no more payments"] end P1 ~~~ P2 ~~~ P3 classDef nutzer fill:#fff0f2,stroke:#e60013,stroke-width:2px classDef bank fill:#ffe5e8,stroke:#e60013,stroke-width:2px classDef wallet fill:#eef2ff,stroke:#3a3e45 classDef agent fill:#f3f4f6,stroke:#3a3e45 classDef haendler fill:#e6f6f0,stroke:#0f8b8d,stroke-width:2px classDef beleg fill:#fffbe6,stroke:#b26a00 class N1,Z3 nutzer class B1,B2,B3,B4 bank class W1 wallet class A2 agent class H2,H3 haendler class R2 beleg class X3,Y3 agent

In detail: the flow as a sequence diagram

Status 23 October 2026 (design revision v3, live since 22 October; categories as Google Product Taxonomy IDs since 23 October): flow A obtains the credentials via OpenID4VCI — the Konto Karte after logging in to online banking, the agent credential from the operator after the customer identified there with the Konto Karte (OpenID4VP). Flow B is the one SCA: the wallet presents the Konto Karte via OpenID4VP in the PaSO profile to the bank's verifier, with the mandate as transaction_data in the request; the bank then issues the approved mandate as a credential to the agent's key. Flow C is the purchase: the agent presents its agent credential and the bank's mandate credential to the merchant via OpenID4VP and hands over the checkout part in the clear; the basket hash is the transaction_data, the merchant verifies both credentials and recomputes hash C — no call to the bank. Flow D: rejection and revocation via the status list. Every decision lies with bank or merchant — the wallet shows and confirms, the agent executes. Numbers = message order; click to enlarge.

%%{init: {'theme':'base','themeVariables':{
  'fontFamily':'Sparkasse, system-ui, sans-serif','fontSize':'14px',
  'primaryColor':'#ffe5e8','primaryBorderColor':'#e60013','primaryTextColor':'#2b2f36',
  'actorBkg':'#f3f4f6','actorBorder':'#9aa0a8','actorTextColor':'#2b2f36',
  'signalColor':'#3a3e45','signalTextColor':'#3a3e45',
  'noteBkgColor':'#fff0f2','noteBorderColor':'#e60013','noteTextColor':'#2b2f36',
  'labelBoxBkgColor':'#f3f4f6','labelBoxBorderColor':'#9aa0a8','labelTextColor':'#2b2f36',
  'loopTextColor':'#515761','activationBkgColor':'#ffe5e8','activationBorderColor':'#e60013',
  'sequenceNumberColor':'#fff'}}}%%
sequenceDiagram
    autonumber
    actor N as Customer
    participant W as Wallet (EUDIPal)
    participant A as Agent (in EUDIPal)
    participant O as Operator — issuer (operator.s-you.me)
    participant I as Bank — issuer
    participant B as Bank — verifier + mandate service
    participant H as Merchant

    rect rgb(243, 244, 246)
    Note over N,I: Flow A — obtain credentials (OpenID4VCI, once)
    N->>W: fetch the Konto Karte (bank offer)
    W->>I: PAR + authorization code, login at online banking
    I-->>W: SD-JWT VC "Konto Karte" (cnf = wallet key, status list)
    W->>O: present the Konto Karte (OpenID4VP, key binding) — the customer identifies to the operator
    O-->>A: offer for the agent credential (pre-authorized code)
    A->>O: proof with the agent key
    O-->>A: SD-JWT VC "agent identity" (cnf = agent key, operator root)
    end

    rect rgb(255, 240, 242)
    Note over N,B: Flow B — one mandate, one SCA (OpenID4VP in the PaSO profile)
    N->>A: "buy milk, bread, dish soap"
    A->>A: build the checkout part (categories) → hash C
    A->>B: POST /agent-nonces
    A->>B: POST /mandate-requests (payment part, hash C, agent VC + KB-JWT)
    B->>B: verify agent VC (operator on trust list, status, cnf, nonce) · create verifier session
    B-->>A: transaction_data (scoped_mandate:2) + openid4vp_url
    A->>W: consent: payment part (bank) + checkout part (local)
    W->>N: show both halves, hash C verified
    N->>W: Face ID
    W->>B: GET /paso/request/{state} → JAR (DCQL, transaction_data)
    W->>W: JAR entry == displayed data? select the Konto Karte
    W->>B: POST /paso/response/{state} — JWE: Konto Karte + KB-JWT (nonce, aud, hash, 9 PaSO claims)
    B->>B: anchor, status list, cnf, hash over the entry, claims → mandate (sca_path b) + credential offer
    W->>B: GET /paso/sessions/{state} → mandate_id + credential_offer
    A->>I: OpenID4VCI pre-authorized code, proof with the agent key
    I-->>A: SD-JWT VC "mandate" (payment part, hash C, sca, cnf = agent key, status list)
    B-->>W: mandate (mirror: status, limits, hash C, credential ref)
    end

    rect rgb(243, 244, 246)
    Note over A,H: Flow C — purchase without a prompt (OpenID4VP at the merchant)
    A->>H: GET /catalog?q=…
    H-->>A: items, prices, categories
    A->>H: POST /checkout/sessions (basket, mandate_id)
    H-->>A: JAR: DCQL agent credential + mandate credential, transaction_data = basket hash + mandate
    A->>A: verify JAR signature, transaction_data == own basket?
    A->>H: POST …/response — agent VC + mandate VC, each with KB-JWT (transaction_data_hashes), checkout part in the clear, JWE
    H->>H: chains to operator root and bank root, status lists, key binding, sha256(checkout part) == hash C, categories
    H-->>A: signed attestation (agent_credential_ok, mandate_credential_ok, checkout_hash_ok)
    A->>B: POST /payments (attestation, amount, agent_proof)
    B->>B: merchant signature, hash C = SCA hash, limits, budget
    B-->>A: receipt — SCT Inst, remaining budget
    A-->>N: items, total, receipt
    end

    rect rgb(255, 240, 242)
    Note over N,H: Flow D — rejection and revocation
    alt merchant rejects (e.g. beer, category_excluded)
        H-->>A: 422 rejected_by: merchant
        A-->>N: "Merchant: alcohol is excluded by the mandate"
    else bank rejects (e.g. limit_exceeded)
        B-->>A: 422 rejected_by: bank
        A-->>N: "Bank: amount above the per-purchase limit"
    end
    N->>B: revoke (wallet, bank app, dashboard)
    B->>B: status bit of the mandate credential set
    B-->>N: mandate void — no further payment, merchant sees it on the status list
    end

As downloadable images: interaction across all four flows · sequence of the mandate approval. Step by step: Interface Specification §1.14 (mandate) and §2.5 (checkout).

The five parties

WalletEUDIPal on the iPhone: consent, SCA, chat with the agent
Bankholds the mandate, checks the payment scope, settles via SCT Inst (SPAA-DRP)
Merchantcatalog, category check, signed attestation — sees no bank data
Agentown Secure Enclave key, identity credential from the operator and mandate credential from the bank (nothing self-issued), signs every payment order
Operatoroperator.s-you.me: issues the agent's credential — only after the customer identified with the bank's Konto Karte (OpenID4VP); bank and merchant trust it via list and root

Status and outlook

The whole flow runs end to end, live, on OpenID4VC, with five parties: the bank (Sparkasse Agentenstadt) issues the customer's Konto Karte (SCA credential) via OpenID4VCI. With this card the customer identifies to the agent's operator (OpenID4VP), and only then does the operator issue the agent's identity credential — the app issues no agent identity itself, and without a Konto Karte there is no agent. The customer grants the bank one limited mandate by presenting the Konto Karte via OpenID4VP in the PaSO profile to the bank's verifier (Face ID as the factor); there is no other approval path. New since 21 October (v3, live since 22 October): the bank then issues the approved mandate as a credential to the agent's key — payment part in the clear, the categories only as a hash, revocation via the bank's status list. Since 23 October the categories are Google Product Taxonomy IDs, so agent, customer and merchant mean the same thing by a category. Afterwards the AI agent shops autonomously at Kiezmarkt in the chat; the bank checks every payment against the mandate, the merchant verifies the bank's mandate credential and the agent's credential, recomputes the category hash and checks the basket — without asking the bank — and every rejection names party and reason. Bank, merchant and operator each have their own screen; all interfaces are documented as a specification and as OpenAPI 3.1.

Interaction on OpenID4VC

TodayBank as issuer of the Konto Karte (OpenID4VCI, online-banking login, status list, own PKI) and as PaSO verifier (OpenID4VP, DCQL, KB-JWT with the PaSO claims) — the only approval path; operator service operator.s-you.me as verifier of the Konto Karte and issuer of the agent credential (own root; bank and merchant trust it via list and root); mandate in two parts (the bank reads only the payment part); merchant as OpenID4VP verifier (HAIP-encrypted); SPAA Dynamic Recurring Payment, also in Berlin Group form; SCT Inst; categories as Google Product Taxonomy IDs (since 23 Oct, matched by ancestry, exclusion wins)
Live (v3, since 22 Oct)The mandate becomes a Verifiable Credential of the bank, bound to the agent key: issued from the approval session via OpenID4VCI, presented to the merchant with the agent credential, checkout part in the clear with its hash in the credential, revocation on the status list. Replaces the customer-signed checkout credential, the separate principal key and the notary endpoint — nothing self-attested remains, and the EUDI wallet kit needs no change. Interface Specification §1.10
NextSeveral merchants (directory and merchant identity), then UCP as the agent ↔ merchant protocol with our presentation as the payment-handler step; SPAA end to end via the operator (agent → operator backend → bank with eIDAS certificate; today only the smoke test uses the façade). Who verifies what against which anchor: Interface Specification §4a
LaterAP2 for foreign agents, operator credential from the European Business Wallet instead of our operator service, EU trusted lists instead of configuration

Detailed status, open items with effort and decisions: internal area (sign in).

Documents

API reference

Machine-readable for partners: OpenAPI 3.1 rendered with Redoc; the raw YAML is here and at every service under /openapi.yaml.

Live systems

Publicly reachable, no login. Everything runs in Frankfurt (Google Cloud Run, europe-west3).

Quick start

curl -s https://bank.s-you.me/.well-known/mandate-service | jq .
curl -s 'https://shop.s-you.me/catalog?q=milch' | jq .

# full bank flow B/C/D with a real P-256 agent key and an operator-issued agent credential
uv run --python 3.12 --with 'cryptography==43.0.3' --with 'jwcrypto==1.5.6' \
  python tools/mock-bank/smoke_test.py https://bank.s-you.me https://operator.s-you.me

In the app (build ≥ 795): Settings → Labor → "Einkaufs-Agent (Test)", three steps — fetch the Konto Karte (demo login demo / 12345), set up the shopping agent (identify to the operator with the card), grant the mandate (consent sheet → Face ID; the bank issues the mandate credential). Then chat → "Kauf mir 2 Milch, Brot und Spülmittel" → receipt. "Und ein Bier dazu" → the merchant rejects, the agent says who and why. "Was hat der Agent gekauft?" → the bank's log. "Widerrufe das Mandat" → the bank flips the mandate credential's status bit and rejects every further payment.