Tutorial

did:bio identifiers

A did:bio DID names a researcher, a dataset, a paper, or a piece of research infrastructure. This page covers what the identifier is made of, the two kinds of subject it can name, how the platform creates and anchors one, and how to resolve it back, from the API or from the command line.

1. The identifier is derived

A did:bio identifier is the base58btc encoding of 32 bytes called the subject. It is derived rather than assigned.

schemedidmethodbionetwork (omit = mainnet)devnetbase58btc(32 byte subject: key or wallet owned PDA)2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tcA key subject is its own verification key, and an owned subject is named by the wallet that created it.
Anatomy of a did:bio identifier. It has the did scheme, the bio method, an optional network segment that is omitted on mainnet, and finally the base58btc encoding of the 32 byte subject, which is an Ed25519 key or a wallet owned program derived address.

Those 32 bytes come in two forms, and the difference matters for everything below.

A key subject is an Ed25519 public key. The DID exists the moment the keypair does, needs no transaction, and resolves to a deterministic generative document until something is written on-chain. Your own identity is one of these. It is your wallet's address.

An owned subject is a program derived address that the registry computes from a wallet and a nonce. It lies off the Ed25519 curve, so no keypair exists for it and there is nothing to lose. The wallet that created it is its first verification method, so one signature names a dataset the wallet owns and pays for. Every asset DID on the platform is one of these.

DIDs that differ only in their network segment are distinct DIDs resolved against distinct registries. No equivalence between them is implied.

2. Your identity is your wallet

The platform holds no signing key for anyone. Your did:bio is did:bio:<network>:<wallet address>, and it appears on your profile once a wallet is linked.

bash
curl https://api.ekayana.io/api/users/me -H "Authorization: Bearer $TOKEN"
json
{ "id": 42, "username": "ada", "email": "ada@lab.example",
  "did": "did:bio:devnet:HP3Hj9en...", "wallet": "HP3Hj9en..." }

Linking is a Sign In With Solana challenge. POST /api/wallet/nonce returns a message to sign, and POST /api/wallet/verify takes the signature. Until a wallet is linked did is null, and any call that needs an identity answers with a 409 asking for one. The app does all of this behind one button.

3. Create an asset DID

POST /api/did creates the record for a dataset, or, with kind, for any short lowercase word such as paper or claim. The API derives an owned subject from your wallet and a fresh nonce and stores the metadata. Nothing is on-chain yet.

bash
curl -X POST https://api.ekayana.io/api/did \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "dataset",
    "metadata": {
      "title": "Coral bleaching survey 2026",
      "researchers": [{ "name": "Ada Lovelace", "orcid": "0000-0001-2345-6789", "role": "PI" }],
      "keywords": ["coral", "bleaching"],
      "license": "CC-BY-4.0"
    }
  }'

The response is the record plus, when the registry has one, the resolved document.

json
{
  "did": "did:bio:devnet:7Qm3...",
  "kind": "dataset",
  "owner_wallet": "HP3Hj9en...",
  "anchored": false,
  "deactivated": false,
  "metadata": { "...": "..." },
  "metadata_cid": "bafy...",
  "pending_sync": true
}

Metadata is published to IPFS as a public document and never contains an email. Until the DID is anchored, resolving it returns notFound, because an owned subject has no generative document.

4. Anchor it with your wallet

Writing to the registry is a transaction your wallet signs and pays for. The API plans it, the wallet sends it, and the API confirms by reading the chain.

bash
curl -X POST https://api.ekayana.io/api/workflows \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "did": "did:bio:devnet:7Qm3...", "kind": "anchor" }'
json
{
  "id": "0d2c...",
  "kind": "anchor",
  "status": "planned",
  "plan": {
    "cluster": "devnet",
    "program_id": "H1gnV4GjNT3UV7AgGNUCkSaciuVVtM7hKb8JhPV3Xxy6",
    "fee_payer": "HP3Hj9en...",
    "did_account": "9xLd...",
    "instructions": [
      { "program_id": "H1gnV4Gj...", "accounts": [ { "pubkey": "HP3Hj9en...", "is_signer": true, "is_writable": true } ], "data": "<base64>" },
      { "...": "one entry per instruction" }
    ],
    "summary": [
      "Create did:bio:devnet:7Qm3... in the registry, controlled by HP3Hj9en...",
      "Add service #metadata (BioMetadata) -> ipfs://bafy...",
      "Name did:bio:devnet:HP3Hj9en... as controller"
    ],
    "rent_delta_lamports": 2247840,
    "fee_lamports": 5000
  }
}

The plan lists every instruction with its accounts and data, the rent it will lock and the fee. The app builds a transaction from it, the wallet signs and sends, and the client posts the signature.

bash
curl -X POST https://api.ekayana.io/api/workflows/0d2c.../confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "signature": "5Kd9..." }'

The API looks the transaction up at confirmed commitment, rereads the account, and only then marks the record anchored. A plan expires if it is not confirmed in time, because the record may have moved on.

Two more workflow kinds cover the rest of the lifecycle. sync computes the difference between the record's services and the document on-chain and plans exactly the adds and removes needed. deactivate plans the permanent tombstone. Both are wallet signed the same way.

5. Resolve it

bash
curl https://api.ekayana.io/api/did/resolve/did:bio:devnet:7Qm3...

Resolution is public, as a DID method requires, and always goes to the chain. The API never fabricates lifecycle state. The result is wrapped in resolution metadata.

json
{
  "@context": "https://w3id.org/did-resolution/v1",
  "didDocument": {
    "@context": ["https://www.w3.org/ns/did/v1", "https://www.w3.org/ns/cid/v1"],
    "id": "did:bio:devnet:7Qm3...",
    "verificationMethod": [{
      "id": "did:bio:devnet:7Qm3...#default",
      "type": "Multikey",
      "controller": "did:bio:devnet:7Qm3...",
      "publicKeyMultibase": "z6Mk..."
    }],
    "authentication": ["did:bio:devnet:7Qm3...#default"],
    "assertionMethod": ["did:bio:devnet:7Qm3...#default"],
    "capabilityInvocation": ["did:bio:devnet:7Qm3...#default"],
    "controller": ["did:bio:devnet:HP3Hj9en..."],
    "service": [{
      "id": "did:bio:devnet:7Qm3...#metadata",
      "type": "BioMetadata",
      "serviceEndpoint": "ipfs://bafy..."
    }]
  },
  "didResolutionMetadata": { "contentType": "application/did+ld+json" },
  "didDocumentMetadata": { "versionId": "3", "updated": "2026-09-16T10:41:07Z", "deactivated": false }
}

versionId is the registry's monotonic counter, so two reads that disagree tell you which one is stale. The #default method is the wallet's key, so the wallet can keep changing the document.

OperationEndpoint
Full resolution resultGET /api/did/resolve/{did} (public)
Record with documentGET /api/did/{did}
List your DIDsGET /api/did
Update metadataPUT /api/did/{did}
Link a Dataverse DOIPOST /api/did/{did}/dataverse
Plan a chain changePOST /api/workflows with anchor, sync or deactivate

6. From the command line

The same registry is open to anyone with a keypair, and no account is needed. bio-did-resolver resolves any DID and drives every instruction except update_service.

bash
cargo install bio-did-resolver

# Any DID, any cluster. The network segment picks the RPC endpoint.
bio-did-resolver resolve did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc

# Your keypair's own DID. Register it and publish a metadata service.
bio-did-resolver init
bio-did-resolver add-service metadata BioMetadata ipfs://<cid>

# An asset the keypair owns. The same nonce always names the same DID.
bio-did-resolver init-owned 42
bio-did-resolver add-service metadata BioMetadata ipfs://<cid> did:bio:devnet:<OWNED>

# A post-quantum assertion key, uploaded through a key buffer in five transactions.
bio-did-resolver add-key pq --type ml-dsa-87 --key-file pq.pub --flags assertion

--dry-run simulates and prints the program logs instead of sending. Requests the program would refuse fail before they leave the machine, with a reason.

Which DID goes where

  • Your wallet's DID is who you are. It is the audience of every share made to you and the issuer of every share you make.
  • An asset's DID is what a share is about. It carries the metadata service, names your wallet's DID as controller, and can be linked to a DOI without giving up either identifier.
  • Naming your wallet's DID as controller grants authority on-chain. Once your wallet's DID has a registry entry, a key you add to it can update every asset that names it, so a new device key reaches all of your assets with one change.

The sharing tutorial grants a capability over a file to a wallet DID. Post Quantum Security covers attaching an ML-DSA-87 key to either kind of document.