Integrate AIDID login into your site
Offline-testable todayWrite and self-test the RP-side integration today, entirely offline: read the wire contract, install the SDK, build a request, verify a response, and run the offline end-to-end demo. Production login is blocked until the chain-side resolve endpoint is authorised.
Journey statusOffline-testable today
Steps
1Five-minute quick start (1/4): install the SDK
Works todayThe SDK is served as a static file from this site: no npm account, no registry account and no backend of ours in the loop. The package is pure ESM.
Only the signature verifier imports the optional peer @msg-chain/sdk (the Dilithium-5 primitive). Message building, request and response verification, the callback reader and the drop-in button are dependency-free, so they also run inside an edge worker.
bash npm install https://aidid.im/download/aidid-login-0.1.0.tgz2Five-minute quick start (2/4): generate the request on your server
Works todaycreateSignInRequest fills type, version, an https uri default, a single-use nonce (32 CSPRNG bytes) and a 5-minute expirationTime. Keep the TTL at or below 5 minutes.
Store the request yourself: the response only echoes it back, and that echo proves nothing. The web entry point is https://aidid.im/authorize/?payload=... and that URL must not change.
js // server.js — generate a request and keep YOUR OWN copy of it. import { createSignInRequest, encodeSignInDeeplink } from '@aidid/login'; import { buildAuthorizeUrl } from '@aidid/login/widget'; const WALLET_ORIGIN = 'https://aidid.im'; // must be a bare https origin export const startSignIn = async (kv, domain) => { const request = createSignInRequest({ domain, // e.g. 'rp.example' (a port is allowed) chainId: 'msg-chain-1', // the chain the response must be bound to uri: `https://${domain}/aidid/callback`, // https callback on your origin statement: 'Sign in to my site.', resources: ['did:auth'], // add 'pay:address' only if you need the payout address }); // Store YOUR copy (TTL <= 5 minutes). Never trust the echo inside the response. await kv.put(`req:${request.nonce}`, JSON.stringify(request), { ttlSeconds: 300 }); return { authorizeUrl: buildAuthorizeUrl(request, WALLET_ORIGIN), // same device: redirect here deeplink: encodeSignInDeeplink(request), // cross device: turn this into a QR }; };3Five-minute quick start (3/4): verify the callback, fail-closed
Offline-testable todayThe eleven ordered checks run inside verifySignInResponse. It returns { ok: false, error } and never downgrades to allow.
Today it always fails closed at the resolve step with endpoint_unavailable, because the chain-side resolve endpoint is not authorised. That is the honest result, not a bug in your code. nonces must be a shared store (Redis, D1, a Durable Object), not createMemoryNonceStore, as soon as you run more than one instance.
js // callback.js — verify fail-closed against the request YOU stored. import { createAgentApiResolver, createCachedResolver, verifySignInResponse } from '@aidid/login'; import { createMsgChainVerifier } from '@aidid/login/verifiers/msgChainSdk'; export const onCallback = async (kv, nonces, response) => { const stored = await kv.get(`req:${response.nonce}`); // look up by the returned nonce if (stored === null) return { ok: false, error: 'nonce_unknown' }; return verifySignInResponse({ request: JSON.parse(stored), // rebuild the canonical message from YOUR copy, never the echo response, resolve: createCachedResolver({ resolver: createAgentApiResolver({ agentApiEndpoint: process.env.AGENT_API }), }), // 10 s TTL + single-flight by default; the API has no CORS, so this runs server-side nonces, // shared store: consume each nonce exactly once verifySignature: await createMsgChainVerifier(), chainId: 'msg-chain-1', }); };4Five-minute quick start (4/4): mount the drop-in button
Works todayThe widget renders a plain button with an inline SVG and no hosted asset, so there is nothing to style or fetch. On creation it reads #response= once (the wallet returns to request.uri#response=...), clears it from the address bar, then calls onResponse exactly once — or onError with a stable reason (no_fragment is not an error).
It does not verify anything and never approves silently, and it never puts /authorize in an iframe. Verification stays on your server.
js // app.js — the drop-in button (browser, ES module, no framework). import { mountSignInButton } from '@aidid/login/widget'; const button = mountSignInButton('#login', { request, // the request your server generated and stored walletOrigin: 'https://aidid.im', mode: 'auto', // 'redirect' same device | 'deeplink' cross device onResponse: (response) => post('/aidid/callback', { response }), // fires once, fail-closed onError: (error) => showError(error.reason), labels: { signIn: 'Sign in with AIDID' }, // override any subset, including Chinese }); // Cross device: render your own QR from button.getDeeplink().5Two transports: same device (top-level redirect) vs cross device (QR / deeplink)
Works todaymode: 'auto' picks redirect when a browser exists and the device looks like the one holding the DID (a mobile user agent, including iPadOS reporting a macOS UA), otherwise deeplink. The auto rule is specified in SPEC-LOGIN-v1 §9.2 (a user-agent / touch-point heuristic), and it stays a heuristic: pass mode explicitly when you know your audience better than the heuristic does.
Same device: a top-level navigation to the wallet. After the user approves, the wallet returns to your callback URL with #response=..., and the widget reads that fragment.
Cross device: the widget shows a web+aidid://auth?payload=... string and a copy button; render the QR with your own library, because the SDK ships no QR dependency. The phone that holds the DID approves and lands on your callback page — the browser on the other machine does not see the response by itself. Today the spec defines no server-side handoff, so the callback page must POST the fragment to your server and the originating browser must poll for the resulting session.
Not verified on hardware: whether an iOS PWA (display: standalone) returns to the RP automatically after approval has not been tested on a real device. Test it in your own target environment and offer a fallback (for example a link the user can open manually).
js // 'redirect' — same device: the SDK does a top-level navigation to // https://aidid.im/authorize/?payload=<encoded JSON> const url = button.getAuthorizeUrl(); // 'deeplink' — cross device: put this string in a QR code, or offer copy const deeplink = button.getDeeplink(); // web+aidid://auth?payload=...6Read the wire contract first
Works todayThe RP contract is four machine-readable schemas: signin-request (what you generate), signin-message (the canonical text that gets signed), signin-response (what the wallet returns) and login-error (the stable error strings you must handle).
The prose authority is the login specification, shipped in the specs tarball. Read section 0 (honest limits) and section 7 (the eleven ordered checks) before writing code.
7Run the offline end-to-end demo (real signatures, no chain)
Offline-testable todayThe fixture uses a fixed public test seed to derive a Dilithium-5 key pair and signs the canonical message for real. It exercises request, approve, verify and session with no network, no chain and no testnet.
Three runnable paths: the Node flow (examples/node/offline-demo.mjs from the tarball), the browser flow against the SDK's browser bundle, and a server-side example that shows verifySignInResponse plus an HTTP resolver, which fails closed with endpoint_unavailable when no public endpoint exists.
The demo does not check chain state, because a fixture resolver replaces canonical resolve, so it cannot prove that a DID exists on chain. Its test key must never be used for a real identity.
8Verify responses in eleven ordered steps, fail-closed
Offline-testable todayVerify in order and reject on the first failure: parse the response; compare nonce and domain with your stored request; rebuild the canonical message and compare byte-for-byte; check expiry; consume the nonce once; check the chain id; resolve the DID on the server; confirm the verification method is in authentication; allow only the Dilithium-5 proof type; verify the signature; then create the session.
Resolve must be server-side, because the agent API returns no CORS headers and a browser cannot call it. There is no built-in rate limit and no Retry-After, so cache with a 5 to 15 second TTL, use single-flight, back off on failure, and do not batch. Consume the nonce before verifying the signature.
The bridge proof type AididP384BridgeSignature2025 is demo-only; reject it unless you explicitly opt in, and label it non-quantum-safe if you do.
9Go-live checklist: do not ship before every line is true
Offline-testable todayBefore production, all of these must hold:
1. The chain-side resolve endpoint is authorised. It is not today (didLoginProduction), so every real verification fails closed with endpoint_unavailable. 2. The signing user actually holds a DID. Most users cannot create one today, because there is no end-user path to obtain MSG (userCreatesDid). 3. Your nonce store is shared across instances and consumes a nonce atomically; a per-instance memory store will accept replays. 4. Resolve runs server-side with a 5 to 15 second TTL cache and single-flight; the API has no CORS and no built-in rate limit. 5. You handle every stable error from login-error (did_not_found, did_deactivated, nonce_replayed, endpoint_unavailable, and the rest) and never fall back to an anonymous session. 6. Registering in the directory is optional and is only a declaration that you control your origin. It is not a security endorsement and it is not required to log in.
Until items 1 and 2 are true, ship nothing that claims to be a working sign-in.
10Production login is blocked (chain-side resolve not authorised)
Not available todayYou can finish and self-test the integration, but you cannot go live: GET {agent_api}/agent/v1/aidid/resolve is not authorised on the public network yet, so an RP cannot resolve any DID in production. This is the unavailable capability didLoginProduction in status.json.
Do not describe AIDID login as production-ready, and do not ship a flow that silently falls back to an anonymous session when resolution fails. The correct behaviour is fail-closed with endpoint_unavailable.
Why this is blocked today
didLoginProductionThe chain-side DID resolve endpoint (`GET {agent_api}/agent/v1/aidid/resolve`) is not authorized yet, so an RP cannot resolve any DID in production.
Reason taken from status.json#capabilities — not written by hand for this page.
11List your site in the registry (proof of origin, nothing more)
Works todayOnce your integration is written you can publish /.well-known/aidid-rp.txt on your origin and open a pull request against the public registry repository. A GitHub Action fetches your file and compares it with the registered challenge; a pull request that changes only the registry file and matches is merged automatically — including a pull request opened from your own fork, with no invitation or collaborator access needed. Registration is free and explicitly transitional until the on-chain registry exists.
There is no rebuild to wait for: the directory page reads a first-party cache that is refreshed on demand (a cache older than ten minutes triggers a refresh, rate-limited to once every ten minutes), and falls back to the snapshot built into the deployment when that read fails — so a merged entry normally shows up on your next visit or two.
Being listed proves only that you control the origin and declare the integration. It is not a security or reputation endorsement by AIDID.