# M401 ↔ NDID mapping

M401 reimplements the [NDID platform](https://ndidplatform.github.io/docs/technical-overview) roles and guarantees on ThaiFi chain 17, replacing Tendermint nodes + message queues with the existing on-chain key layer and a small registry contract.

## Role mapping

| NDID | M401 | Notes |
|---|---|---|
| Platform (NDID nodes, Tendermint) | chain 17: AccountKeychain precompile `0xAAAA…0000` + `M401Registry` | NDID's DLT holds participant registry + request hashes; M401's chain holds key authorization (precompile) + prove/consent events (registry). Both store **no PII on-chain**. |
| RP node | the API/MCP server the agent calls | issues the challenge (NDID's RP-initiated request), verifies, records |
| IdP node | wallet.thaifi.com (ThaiFi Wallet) | identity proofing happened at account creation (email OTP / LINE Login) + at pairing (passkey/PIN → AAL 2.1/2.2); re-authenticates the owner at L2 consent |
| AS node | phase 1: the wallet itself (its D1 is the authoritative source for email/LINE/aal). phase 2 (L3): third parties (bank, gov) | NDID separates IdP (authentication) from AS (data); M401 collapses them for phase 1 because the wallet already holds its users' data |
| Message queue (encrypted node-to-node) | **the agent is the courier** | NDID needs a queue because nodes are institutions that can't trust direct HTTP; M401 moves signed artifacts (envelope, JWS) over plain HTTPS and lets every party verify signatures itself |
| On-chain request record (`request_id`, hashed params) | the RP challenge (nonce) + `ProofRecorded(proofHash, …)` | `proofHash = keccak256(nonce ‖ signature)` — unique per prove event, unforgeable without the agent key |
| Accessor signature (IdP signs request_message_hash) | the agent's keychain-envelope signature over `m401-auth-v1` hash | proves "the holder of access key K consented to this exact request" |
| IAL / AAL | `aal` claim: `2.1` = PIN-guard, `2.2` = passkey (+ user verification) — from `agent_pairings.aal_level`, refreshed at every L2 consent ceremony | same semantics as NDID's min_ial/min_aal negotiation: the RP requests a floor in `scope`, the wallet answers with what it can attest |
| Consent (IdP response status) | L2 `/m401` approval screen + `ConsentGranted` event + D1 `m401_consents` audit row | consent is bound to (pseudonym, realmFp, scopeHash) — the chain proves *that* consent happened without revealing *what* was released |
| reference_group_code (hashed identifier) | pairwise pseudonym `keccak256(userId ‖ realm)[0:20]` | cross-RP unlinkability, identical purpose |
| AS data signature (on data + salt) | IdP JWS (ES256) over the assertion incl. claims | RP verifies offline against JWKS at `/.well-known/m401.json` (`kid` rotation ≈ NDID node key rotation) |

## NDID sequence → M401 sequence

```
NDID                                              M401
──────────────────────────────────────────────    ──────────────────────────────────────────────
RP → platform: create request                     RP → agent: 401 WWW-Authenticate: M401 (challenge, scope)
platform → IdP: encrypted request                 agent → IdP: POST /api/m401/requests (TAA auth)
IdP → user: authenticate + consent                wallet UI /m401: passkey/PIN approval (L2)
IdP → platform: signed response                   wallet: JWS assertion + ConsentGranted on registry
platform → RP: callback                           agent → RP: Authorization: M401 (credential + assertion)
RP ↔ AS: data via queue + signature check         (phase 1) claims already in the assertion
                                                  (phase 3/L3) RP ↔ third-party AS: data request keyed by consent_id
```

## What M401 deliberately drops

- **Participant registry / governance board** — NDID is a closed club of approved nodes. M401 is permissionless: any server can be an RP (just implement the challenge), and the IdP set is discoverable via `.well-known` (the RP chooses which IdPs it accepts, exactly like choosing which OAuth providers to support).
- **Message queue** — replaced by the agent-as-courier + signature verification at every hop. Trade-off: the IdP doesn't learn *when* the assertion is consumed by the RP (acceptable; consent receipts live on-chain).
- **min_ial / min_idp multi-IdP consensus** — phase 1 has one IdP per user account (their wallet). Multi-IdP returns at L3.

## Path to L3 (third-party AS)

1. AS registers in a follow-up on-chain listing (or an mpp-style directory) with `service_id`, endpoint, and its own signing key.
2. L2 consent gains `data_request_list` (NDID's shape): `[{service_id, params_hash}]`.
3. RP (or IdP) calls the AS with `consent_id`; AS verifies the on-chain `ConsentGranted` + IdP assertion, releases data signed to the RP, and records a data-hash event on the registry.
4. The AccountKeychain pattern (hash-verified, permissionless, PII-free) repeats one layer up.

## Why not Proof's x401 (W3C VC / OID4VP)?

| | Proof x401 | M401 |
|---|---|---|
| Credential issuer | Proof's Certificate Authority | none needed — the user's on-chain `authorizeKey` tx **is** the attestation |
| Trust anchor | CA + trust list governed by Proof | chain 17 (precompile) + wallet `.well-known` key |
| Transport | Digital Credentials API / OID4VP (browser-centric) | plain HTTP headers — headless CLI/agents work natively |
| Revocation | IdP-dependent | instant, on-chain (`verifyHash` false after revoke) |
| Audit | Proof API | public registry events on chain 17 (explorer-browsable) |
| Payments (nice-to-have synergy) | x402 per-presentation | MPP already in the same stack; M401 + MPP share challenge/retry ergonomics |

An OID4VP/VC bridge (mapping M401 assertions to Verifiable Presentations for ecosystems that demand W3C VC) is future work, deliberately out of the 1.0 draft.
