Use DID login for payments (payout address)

Offline-testable today

After a verified login you can resolve the DID on your server and read the payout address from the canonical record. You can write and self-test that logic offline today; reading a real address in production is blocked with the rest of production login. pay:address is a declaration the user sees and approves, not a permission.

Journey statusOffline-testable today

Steps

  1. 1Where the payout address actually comes from

    Works today

    A login response carries a did, a canonical message and a signature — no account address. To get the address you resolve the DID and read the canonical record on the server.

    The record field owner is the 44-character MSG account address (msg + 40 hex + one checksum digit). The document's controller field is different: it lists controller DIDs, not addresses, so never send MSG to a did:msg:... string. Check deactivated before you use the address, and do not cache it longer than your resolve TTL (5 to 15 seconds); a name-to-address mapping can change between DID updates.

    js
    // After a verified login you have `did` (result.did) — never an address.
    import { createAgentApiResolver, createCachedResolver } from '@aidid/login';
    
    const resolve = createCachedResolver({
      resolver: createAgentApiResolver({ agentApiEndpoint: process.env.AGENT_API }),
    });
    
    const record = await resolve.resolve(result.did); // canonical record, 10 s TTL by default
    if (record.deactivated) throw new Error('did_deactivated'); // refuse a retired identity first
    
    const payoutAddress = record.owner; // 'msg' + 40 hex + 1 checksum = 44 chars, an ACCOUNT address
    // record.document.controller is a list of controller DIDs — NOT a payment address.
  2. 2resources: ['pay:address'] is a declaration, not a grant

    Works today

    Adding pay:address to resources puts a statement into the canonical message that the user signs, and the wallet shows it on the approval page: "this site will obtain your payout address". It is a declaration, and the protocol says so explicitly.

    It is not a permission, it grants no on-chain right, and it does not mean the user authorised a payment. The response still contains no address, so you must resolve the DID yourself. If you do not request pay:address, do not derive, display or use the address for payment at all.

    js
    import { createSignInRequest } from '@aidid/login';
    
    const request = createSignInRequest({
      domain: 'shop.example',
      chainId: 'msg-chain-1',
      uri: 'https://shop.example/aidid/callback',
      statement: 'Sign in to shop.example.',
      // A declaration the user sees and approves. It is NOT a permission and the
      // response carries no address, so you still resolve the DID yourself.
      resources: ['did:auth', 'pay:address'],
    });
    // result of the callback: { ok: true, did: 'did:msg:...' } — no address field, by design.
  3. 3Binding the address to the DID: keys, devices and sub-DIDs

    Offline-testable today

    The address belongs to the DID, so the questions that matter are about the DID and where it lives.

    New device or restored backup: a restore brings back the same DID document, so the same owner address still applies. A newly created DID gets a new address, so never hard-code an address against a user account.

    Key rotation or a DID update: re-resolve before every payout; the record carries a monotonic version and an updated block time, and the owner can change across updates.

    Sub-DIDs: a sub-DID is its own did:msg with its own resolve record. Pay the sub-DID's owner only when that sub-DID is the identity you accepted, and remember that a sub-DID grant has no on-chain revocation channel today — a revoked grant may still resolve until the document is updated.

    Deactivated DIDs: refuse before reading an address (did_deactivated), and keep your own deny list, because the chain has no blacklist.

  4. 4Today: offline self-test yes, production address no

    Not available today

    Everything above can be written and self-tested offline today, with a fixture resolver in place of canonical resolve: the request, the approval declaration, the callback, the resolve step and the address read.

    What you cannot do today is read a real address in production. Resolve is not authorised on the public network, so the address step fails closed with endpoint_unavailable for every DID. Do not ship a checkout or payout flow that depends on it; the honest status is fail-closed until the chain side authorises the endpoint and ordinary users can actually hold a DID.

    Why this is blocked today

    • didLoginProduction The 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.