Creating a did:bio Identity
A did:bio DID names a dataset, a researcher, or a piece of research infrastructure. This walks through minting one and resolving it back.
1. The identifier is derived, not assigned
A did:bio identifier is the base58btc encoding of an Ed25519 public key - not a random UUID. That single design choice is what makes the method work offline:
did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc
▲ ▲
│ └─ base58btc(32-byte Ed25519 public key)
└─ network segment (devnet | testnet | localnet; absent = mainnet)Because the key is the identifier, anyone can verify a signature from the subject without fetching anything. The registry is only needed once you want to rotate keys, add services, or deactivate.
DIDs that differ only in their network segment are distinct DIDs resolved against distinct registries. No equivalence between them is implied.
2. Generate a keypair
The DID exists the moment the keypair does - creation is free and offline.
# Any Ed25519 keypair works.
openssl genpkey -algorithm ed25519 -out subject.pem3. Create the document
POST /api/did stores a DID document and pins it to IPFS.
curl -X POST https://api.ekayana.com/api/did \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"controller": "did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc",
"public_key": "z6MkfuN2vWAoHermh6vY6TgAJfwhBWZCApZb9XeQ9HwLqHfz",
"service_endpoints": [],
"metadata": {
"title": "Coral bleaching survey 2026",
"researchers": [{ "name": "Ada Lovelace", "role": "PI" }],
"keywords": ["coral", "bleaching"],
"data_type": "observational",
"license": "CC-BY-4.0",
"creation_date": "2026-07-24T00:00:00Z",
"last_modified": "2026-07-24T00:00:00Z"
}
}'public_key accepts a Multikey (z6Mk…) or a bare base58btc 32-byte key. The server decodes it, derives the identifier from the key, and rejects anything that is not a valid Ed25519 key - so a DID can never disagree with its own subject key.
4. What comes back
A W3C DID 1.0 conformant document:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://www.w3.org/ns/cid/v1"
],
"id": "did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc",
"verificationMethod": [{
"id": "did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc#default",
"type": "Multikey",
"controller": "did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc",
"publicKeyMultibase": "z6MkfuN2vWAoHermh6vY6TgAJfwhBWZCApZb9XeQ9HwLqHfz"
}],
"authentication": ["did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc#default"],
"assertionMethod": ["did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc#default"],
"service": [...]
}The #default fragment is reserved for the subject key.
5. Read it back
| Operation | Endpoint |
|---|---|
| Fetch the document | GET /api/did/{did} |
| Full resolution result | GET /api/did/resolve/{did} (public) |
| List your DIDs | GET /api/did |
| Update | PUT /api/did/{did} |
| Link a Dataverse DOI | POST /api/did/{did}/dataverse |
/api/did/resolve/ is the only DID route that needs no authentication - resolution is a public operation, as a DID method requires. It returns the document wrapped in resolution metadata:
{
"@context": "https://w3id.org/did-resolution/v1",
"didDocument": { ... },
"didResolutionMetadata": { "contentType": "application/did+ld+json" },
"didDocumentMetadata": { "versionId": "<cid>", "deactivated": false }
}Updates are restricted to the DID's owner; PUT from another account returns an authorization error.
Which DID goes in a UCAN?
Two identifier shapes appear in the platform, and mixing them up is the usual first stumble:
did:bio:…- a dataset or research subject, derived from an Ed25519 key.did:bio:user:{id}- an account, the audience of a share (returned by/api/users/search).
The sharing flow grants a capability over the first to the second. See Post-Quantum Security for how ML-DSA-87 keys are attached to a DID document as an additional verification method.