Vellum: a reference dashboard and SDK for did:ckb

I’ve been building Vellum, a reference dashboard for did:ckb, the Decentralized Identifier method defined in WIP-01 and implemented in web5fans/did-ckb. The dashboard talks directly to the deployed Type Script on mainnet and testnet, and the reusable bits live in a draft @ckb-ccc/identity package on a fork of ckb-devrel/ccc.

Sharing now because I’d like feedback on (a) the UX choices around DIDs on CKB, (b) the profile convention I added, and (c) whether the package belongs in CCC upstream.

What’s working end to end

Every operation in WIP-01 is wired against the deployed contract, plus the WIP-02 migration:

  • Claim: builds a transaction creating a DID Metadata Cell with the type-id-style identifier args, fills a DiceBear pixel-art default avatar seeded on the new DID, signs and submits, polls for inclusion.

  • Resolve: paste any did:ckb, get the document, profile, handles, verification methods, services. Reads directly from the indexer, no API in between.

  • My DID: reverse-lookup by Lock Script, hero card with the holder’s avatar and display name, document body with editable fields, Lock Script card, and the full on-chain operation history (walks the cell chain backwards through tx inputs and classifies each as CREATE / UPDATE / MIGRATE).

  • Edit: full document editor with inline validation hints for AT Protocol / Nostr / did:key shapes.

  • Rotate: move control of a DID to a different CKB Lock without changing the identifier. Paste an address, the dashboard parses it via CCC’s Address.fromString, shows old vs new side by side, signs with the current Lock.

  • Deactivate: burn flow with a 24-hour UI cool-down per DID.

  • did:plc migration (WIP-02): fetches the source operation log from plc.directory, signs the CKB tx hash with one of the holder’s PLC rotation keys (kept in browser memory only, never transmitted), attaches the witness in WitnessArgs.output_type, and surfaces the 72-hour finalisation window.

Notable design calls

A few things in the implementation that are worth flagging for discussion:

  • Profile convention. The DID document carries the did:plc-compatible fields (verificationMethods, alsoKnownAs, services) plus a services.profile entry of type VellumProfile with displayName, avatar, bio inline. Any resolver picks it up. Question for the community: useful enough to standardise across CKB ecosystem apps so the data is portable, or stay app-specific?

  • Default avatar. When the holder doesn’t supply one, the SDK fills https://api.dicebear.com/9.x/pixel-art/png?seed=<did> into the document at create time. Deterministic, identifies the holder visually without onboarding friction.

  • Capacity reserve. Cell capacity is computed exactly and bumped by a 200 CKB reserve so the holder can grow the document later without re-funding. Most cells land between 300 and 600 CKB. Fully recoverable on deactivation.

  • History walk client-side. Implementing operation history without indexing infrastructure meant walking the cell chain backwards through tx inputs (client.getTransaction + previousOutput). Cheap for typical DIDs, capped at 50 steps for safety.

The SDK

The package mirrors the conventions of @ckb-ccc/spore, @ckb-ccc/udt, etc: @ckb-ccc/core as a dep, ESM + CJS builds, an identity namespace export, optional /plc subpath for the migration helpers.


import {

resolveDid,

buildCreateTx,

buildUpdateTx,

buildDeactivateTx,

buildMigrationTx,

getDidHistory,

listDidsByLock,

} from "@ckb-ccc/identity";

import { fetchPlcLog } from "@ckb-ccc/identity/plc";

Lives on a feat/identity-package branch of my fork: ccc/packages/identity at feat/identity-package · truthixify/ccc · GitHub. Released as GitHub Release tarballs on the fork for now, because publishing under @ckb-ccc/* isn’t mine to do.

26 unit tests cover base32, identifier round-trips, document encoding, DAG-CBOR round-trip, default-avatar semantics. All passing under vitest.

Open question for CCC maintainers: does this belong upstream? Happy to follow whichever conventions are preferred. The PR would be small, one new package, no changes to anything existing.

What’s next, realistically

Directions I think make sense to explore. None are promised, all worth a conversation:

  • Spore integration. A Spore can carry did:ckb:... in its metadata. Ownership stays with the Lock Script as today (Spores use Lock), but the issued-to binding can point to a DID, which means an NFT collection survives the holder’s wallet rotation. Worth prototyping.

  • SBT-style badge cells keyed on DID args. An on-chain badge Cell with args = DID identifier (20 bytes) is straightforward. Reputation systems (CKBoost is the obvious one) could anchor signals to a DID instead of an address, so reputation follows the user across key rotations.

  • W3C Verifiable Credentials. Signed by a key in the DID’s verificationMethods map, with the DID as subject. Stored off-chain by default, or pinned as a Cell when on-chain commitment matters. Standard pattern, just needs an issuance flow.

  • Lock rotation via wallet connect. Today the rotate page accepts a pasted address. A nicer flow would let the holder connect a second wallet inline.

  • Resolver coverage. Today only did:ckb resolves directly through Vellum. did:web and did:key are easy adds; did:plc is already reachable via the migration path.

I’m explicitly not committing to:

  • Merging the package into upstream CCC (depends on maintainers’ interest).

  • Publishing to npm under any official scope (would need permission first).

  • Spore / CKBoost / SBT integrations as part of this thread; flagging the direction, not promising the work.

Discussion prompts

Specific things I’d love community input on:

  1. services.profile convention. Worth standardising across CKB ecosystem apps so the data is portable, or fine app-specific?

  2. @ckb-ccc/identity upstream. Interest in merging this into ckb-devrel/ccc? Any API or scope changes maintainers would want before considering a PR?

  3. DID-anchored Spores / badges / reputation. Anyone interested in collaborating on a prototype? CKBoost folks especially.

  4. DID method coverage. Should the resolver page handle other methods (did:web, did:key, did:plc directly), or stay did:ckb-focused?

  5. WIP-04 Nostr profile. The dashboard doesn’t surface Nostr-specific fields yet (NIP-01 pubkey, NIP-05 handle, relay endpoints). Worth wiring as a first-class profile alongside services.profile?

Links:

Happy to answer anything in the thread.

9 Likes

Thanks for this @truthixify

I was able to create a DID without too much trouble.

Being able to destroy the DID and recover the CKB is a nice touch.. It seems plausible that a DID module like this could be used to earn social credentials over time (proof and history of participation, gathering of badges, verifiable interactions), which is an incentive against destroying and creating new DIDs. Perhaps that’s one answer to throwaway DIDs and governance questions.

I also like your demonstration of integrating a did:ckb module into CCC directly, which could create a social continuity across all apps that use it. Is this a feasible addition to CCC in your opinion @Hanssen?

@truthixify Regarding migrating an existing did:plc - is there something more needed on the user facing side? I’m assuming if there’s some kind of reason (campaign or incentive) for users to migrate, it may be their first interaction with CKB. Is there a way to reduce friction associated with acquiring the CKB needed for cell creation?

5 Likes

Thanks for trying it out.

The social-credentials framing is exactly the angle I’m most curious about too. Once a DID accumulates badges, attestations, or a participation history, churn starts working against the holder. Losing all of that costs more than the cell-capacity refund pays back. That’s also the cleanest answer to the throwaway-DID governance worry: weighting votes or eligibility by DID instead of by address means an actor can’t cheaply spawn new identities to dilute a signal, because the credentials they care about are pinned to the DID they already have. I think this makes the badge / credential layer the most valuable next step.

On migration friction: yes, real gap. The dashboard assumes the holder already has enough CKB on the connecting wallet, which for a first-time CKB user coming in from AT Protocol is a hard ask. A few directions I think are realistic:

  1. Sponsored migration. The migration transaction only requires a witness signed by one of the PLC rotation keys. Nothing in the contract requires the holder to provide the cell capacity, so a relayer (or sponsor) could add input cells, build the tx, hand the unsigned hash to the holder, take the rotation-key signature back, and submit. The new cell is locked to the holder, so the sponsor’s capacity stays locked there until the holder eventually deactivates (at which point the refund goes to the holder’s Lock, not the sponsor). Economically that’s the sponsor underwriting cell capacity per migration, which makes sense for funded campaigns and is straightforward to prototype behind a flag.
  2. Trimmed migration cell. At migration time the document only needs the bridged alsoKnownAs and a rotation key reference to be a valid did:ckb. Keeping the cell at the storage floor (around 300 CKB) reduces what a sponsor has to underwrite per migration. The holder can grow the document later when they have CKB of their own.

The first option is the most interesting because it removes the CKB-acquisition step entirely for the migrating user. If there’s interest from anyone running infra or sponsoring a campaign, happy to extend buildMigrationTx to accept an external funder and wire up a small relayer endpoint.

2 Likes

+1

I think yes because data portability is essential to an open ecosystem. But the standard should come with at least one application.

2 Likes