Building CKB PoP: a reusable participation primitive on CKB

Months ago, I joined CKBuilders and started learning the blockchain properly, trying to consume as much as I could from the handbook and the wider ecosystem. A lot of the time, I would even ask myself if I was moving at pace or not.

Then I came across DOBs on CKB.

At the moment, the one idea I had was simple: an SBT. A soulbound token. Something non-transferable, identity-bound, participation-bound, and meaningful because it stays with the person who actually earned it. From there, I started sketching out what an SBT meant in my head, not just as a collectible, but as a proof layer.

That was where CKB PoP came in.

CKB PoP started as a way to prove presence. But while building it, it became obvious to me that ā€œpresenceā€ is too narrow if the primitive is good enough. It should also be able to prove participation, completion, contribution, and all the other cases where someone should be able to earn a non-transferable badge on CKB without us pretending everything is just ā€œattendanceā€.

So I started scaffolding. And here we are today.

What CKB PoP is

CKB PoP is a scope-based participation system on CKB. By ā€œscopeā€, I mean the thing a badge is issued for. That scope might be:

  • a physical event
  • an online hackathon
  • a program
  • a course
  • a campaign
  • a bounty
  • a membership
  • or something custom

The core idea is that the on-chain primitive stays stable, while the way participation is proven can change depending on the use case. So instead of hardcoding one ritual, the system now supports different proof flows.

Right now, that includes:

  • dynamic QR for physical presence
  • signed claims for online completion
  • submission proofs for async or reviewed work
  • policy extensions for organizer attestations and submission review

So a conference check-in and an online hackathon completion can both lead to the same class of non-transferable badge, but through different proof paths.

The architecture

I’ve tried to keep the boundaries very explicit.

1. The package

I extracted the reusable part into an npm package called `ckb-pop-kit`. This is the reusable developer-facing layer. It handles things like:

  • scope modeling
  • proof driver registration
  • policy extension registration
  • claim token helpers
  • manifest shaping
  • CKB helper functions for args and cell data

The idea is that if another product wants to build on this, they should not need to depend on my frontend or hosted backend just to use the model.

2. The backend

The backend is a Rust service. It is non-authoritative. It does useful work, but it is not the final trust anchor.

It currently handles:

  • scope creation
  • HMAC encoded QR generation
  • signed claim issuance and verification
  • badge observation
  • manifest discovery
  • convenience APIs for the reference frontend

The backend can decide whether someone is eligible according to a given flow, but it still does not get to override the chain.

3. The contracts

The contracts are small on purpose. They enforce:

  • uniqueness
  • ownership
  • immutability

They do not know whether the badge came from a summit, a hackathon, or a course. They do not know what ā€œattendanceā€ means. They do not know what ā€œcompletionā€ means. They only know that a badge exists, it is unique for a given scope and address, and it belongs to that address.

That separation matters to me.

4. The reference frontend

There is also a reference app on top of all this. It now demonstrates:

  • physical verification flow
  • online claim flow
  • scope creation
  • badge vault/gallery
  • integration surface

I recently rebuilt the frontend shell and landing so it feels more intentional, and I also put docs for easy integrations.

One thing I cared about a lot

I did not want online participation to be hacked in as if it were just a weird event check-in. If someone completes an online hackathon, that should not have to masquerade as physical presence.

So the system now has explicit fields like:

  • `scope_kind`
  • `participation_mode`

That means the metadata can actually say what happened, instead of forcing everything into the vocabulary of attendance.

Why I think this matters on CKB

CKB is one of the few places where this kind of thing feels natural. The cell model makes it very clean to think in terms of unique owned artifacts. Type scripts let you enforce the invariants that should actually live on-chain, while keeping the rest off-chain and replaceable. That balance has become more and more important to me while building this. Not everything needs to be on-chain. But the things that matter should be enforceable there.

## Where it is now

At this point, CKB PoP has:

  • a published npm package: `ckb-pop-kit`
  • a reference backend
  • contracts for badge uniqueness and scope anchors (testnet)
  • physical QR flow
  • online signed-claim flow
  • docs and reference UI
  • a model that is broader than just events

So it is no longer just ā€œproof of physical presenceā€. It is closer to a reusable participation primitive on CKB.

Links

Where I need criticism

This is the part where I ask the community to really poke holes in it.

I want:

  • technical criticism
  • architectural criticism
  • product criticism
  • protocol criticism
  • integration ideas
  • security review
  • docs feedback
  • naming criticism too, honestly

And if anyone wants to contribute directly, I’m open to all sorts of contributions:

  • code
  • review
  • design
  • docs
  • protocol feedback
  • use-case suggestions
  • corrections where I’m thinking wrongly

I’m especially interested in hearing from community builders who might want to integrate something like this into their own products, whether for hackathons, programs, campaigns, guilds, or other contribution systems.

Appreciation

A special appreciation to the ever supporting Nervos Community, @xujiandong for mentoring me and @neon.bit . Another round to everyone who will be contributing to my growth and to CKB-PoP moving forwards. I have quite some more interesting builds we’d come back to but I am glad to share this, and quite ā€˜nervos’ as well.

Btw, If you read this and something feels wrong, say it.If you think the model is solid but incomplete, say what is missing. If you think there is a better way to structure this on CKB, I want to hear that too. And if you think this can be useful in something you’re building, I’d like to talk.

14 Likes

Really nice work.

This feels much more interesting as a participation primitive than as just a proof of presence app, and the architecture looks well thought out.

Since feedback was invited, the biggest question over time is probably how issuer trust and badge meaning carry across apps. But this already feels like a strong base to build on.

2 Likes

Thanks @Ophiuchus for contributing :wink:

If I get you right, you just pointed out ā€œif or how badge meaning stay legible across products?ā€

Right now, my focus has mainly been on getting the primitive itself into a reusable shape. But the next serious layer I’m thinking about is exactly around provenance, issuer identity, and how badge meaning should carry across products without becoming vague or easily abused.

I am currently thinking if provenance should be stronger for badges from other prod, and how much of it should be social trust/ protocol enforced.

I will definitely be putting some thought into it and coming harder with v2

4 Likes

Super idea :light_bulb: . Just to clarify, what about collections of DOBs and will this be like a immortal DOB?

2 Likes

for collections, yes, I think there should be a way for badges to be grouped by scope, issuer, program, hackathon, event series, community, etc. People should be able to see a participant’s earned history in context. I tried this on the reference app, there are some WIP by the way.

As for immortal DOBs, if you mean immutable, persistent and non-transferrable. yes, it is close in spirit. But if you mean consumability, since cells are by default consumable, in this design, the badge/anchor cells are meant to be non-transferable and effectively persistent, because the type script rejects attempts to reuse or mutate them in ways the protocol does not allow.

2 Likes