A keyless CLI for the Proof of Presence (PoP) protocol on Nervos CKB. Create events, open attendance windows with rotating QR codes, and mint soulbound badges — all without storing a private key locally.
- What It Does
- How It Works
- Installation
- Getting Help
- Initial Setup
- Command Reference
- Workflows
- On-Chain Contracts
- Signer Architecture
- Configuration
- Development
- Tech Stack
ckb-pop-cli is the organizer and attendee tool for ckb-pop.xyz, a Proof of Presence protocol built on Nervos CKB. It serves two roles:
For event organizers:
- Register an event with the ckb-pop backend and anchor it immutably on-chain.
- Open a timed attendance window that displays rotating QR codes in the terminal.
- Manually mint badges for specific attendees when needed.
For attendees:
- Scan an organizer's QR code and run a single command to verify attendance and mint a soulbound badge to your wallet address.
All signing is delegated to an external wallet (browser-based, Ledger, passkey, or WalletConnect). No private keys are ever stored by this tool.
Organizer Attendee
--------- --------
ckb-pop event create ckb-pop attend "<qr_data>"
└─ Signs creation proof via wallet └─ Parses QR payload
└─ POSTs to backend registry └─ Verifies HMAC freshness (60s)
└─ Builds event-anchor tx └─ Signs attendance proof via wallet
└─ Broadcasts on-chain └─ Builds dob-badge tx
└─ Backend activates event └─ Broadcasts on-chain
ckb-pop event window <event_id>
└─ Signs window-open proof
└─ Derives window secret (HMAC key)
└─ Displays rotating QR every 30s
└─ Each QR encodes: event_id|timestamp|hmac
The two on-chain type scripts (dob-badge and event-anchor) enforce uniqueness constraints at the protocol level. A badge cannot be minted twice for the same (event_id, address) pair, and no two anchors can exist for the same (event_id, creator) pair. The CKB chain is the source of truth; the backend provides event discovery and indexing.
Prerequisites: Rust toolchain (stable, 2021 edition).
git clone https://github.com/RobaireTH/ckb-pop-cli
cd ckb-pop-cli
cargo build --releaseInstall the binary to your PATH:
cargo install --path .Or run directly without installing:
cargo run -- [COMMAND] [OPTIONS]Every command and subcommand accepts --help (or -h) to print usage and available flags. The root command also accepts --version (or -V).
# Print version
ckb-pop --version
ckb-pop -V
# Top-level help: lists all commands and global flags
ckb-pop --help
ckb-pop -h
# Help for a command group
ckb-pop signer --help
ckb-pop event --help
ckb-pop badge --help
ckb-pop tx --help
# Help for a specific subcommand
ckb-pop signer set --help
ckb-pop signer connect --help
ckb-pop signer status --help
ckb-pop event create --help
ckb-pop event list --help
ckb-pop event show --help
ckb-pop event window --help
ckb-pop attend --help
ckb-pop badge mint --help
ckb-pop badge list --help
ckb-pop badge verify --help
ckb-pop tx status --helpckb-pop signer set --method browserThe browser signer is the default and requires no additional hardware. See Signer Architecture for other options.
ckb-pop signer connectThis opens a local signing page in your browser. Connect your CKB wallet (JoyID, MetaMask, UniSat, OKX, or any CCC-compatible wallet), and the CLI stores your address in ~/.ckb-pop/config.toml.
ckb-pop signer statusThese flags apply to all commands and override the values in ~/.ckb-pop/config.toml.
| Flag | Description | Default |
|---|---|---|
--network <NETWORK> |
Target network (testnet or mainnet) |
testnet |
--rpc-url <URL> |
Override the CKB RPC endpoint URL | From config |
--signer <METHOD> |
Override signing method (browser, ledger, passkey, walletconnect) |
From config |
--address <ADDRESS> |
Override the active CKB address | From config |
Set the default signing method.
ckb-pop signer set --method <METHOD>Options:
--method <METHOD>—browser,ledger,passkey, orwalletconnect
Open a browser signing page to authenticate your wallet and store your address in the config.
ckb-pop signer connectDisplay the current signing configuration: method, stored address, network, and RPC URL.
ckb-pop signer statusRegister a new event with the ckb-pop backend and create an immutable on-chain anchor.
ckb-pop event create \
--name "My Conference" \
--description "Annual tech conference." \
[--image-url <URL>] \
[--location <LOCATION>] \
[--start <ISO8601>] \
[--end <ISO8601>]Required:
--name <NAME>— Event name.--description <DESC>— Event description.
Optional:
--image-url <URL>— URL for the event image or badge art.--location <LOCATION>— Event location.--start <ISO8601>— Event start time (e.g.,2026-05-15T09:00:00Z).--end <ISO8601>— Event end time.
What happens:
- Prints your creator address so you can verify it matches your connected wallet before signing.
- Prompts your wallet to sign a creation proof.
- Posts the proof and metadata to the backend, which returns a canonical
event_id. - Builds and broadcasts an
event-anchortransaction on-chain. - Polls for confirmation (~90 seconds), then activates the event on the backend.
- Prints the
event_idand the event URL on ckb-pop.xyz.
List event anchors on-chain, optionally filtered by creator address.
ckb-pop event list [--creator <ADDRESS>]Show the details of a specific event anchor.
ckb-pop event show <EVENT_ID>Open a timed attendance window and display rotating QR codes in the terminal.
ckb-pop event window <EVENT_ID> [--duration <MINUTES>]Options:
--duration <MINUTES>— How long the window stays open. Default:60.
What happens:
- Prompts your wallet to sign a window-opening proof.
- Derives a window secret from the event ID, start time, and your signature.
- Clears the screen and displays a QR code that refreshes every 30 seconds.
- Each QR encodes
event_id|timestamp|hmacwhere the HMAC is derived from the window secret. - Attendees have a 60-second window to scan and use any given QR code.
- Exits when the duration expires or you press Ctrl-C.
Parse a QR code, verify it, sign an attendance proof, and mint a soulbound badge to your address.
ckb-pop attend "<QR_DATA>"Example:
ckb-pop attend "abc123def456...|1748000000|deadbeef01234567"What happens:
- Parses the QR payload:
event_id|timestamp|hmac. - Checks that the QR timestamp is within the last 60 seconds (freshness).
- Verifies the HMAC against the event's window secret.
- Prompts your wallet to sign an attendance proof.
- Builds a
dob-badgetransaction and broadcasts it on-chain. - Prints the badge transaction hash.
The QR data string is typically produced by scanning a terminal QR code. You can also paste it directly from the organizer.
Manually mint a badge for a specific recipient. This is an organizer action for cases where the attendee cannot run the CLI themselves.
ckb-pop badge mint <EVENT_ID> --to <ADDRESS>Options:
--to <ADDRESS>— The recipient's CKB address.
List all badges held by a given address.
ckb-pop badge list --address <ADDRESS>Check whether a specific badge exists on-chain for a given event and address.
ckb-pop badge verify <EVENT_ID> <ADDRESS>Query the confirmation status of a transaction by its hash.
ckb-pop tx status <TX_HASH># 1. Set up your signer (once)
ckb-pop signer set --method browser
ckb-pop signer connect
# 2. Create the event
ckb-pop event create \
--name "CKB Builders Day" \
--description "A day for CKB builders to meet and collaborate." \
--location "San Francisco" \
--start "2026-06-01T10:00:00Z" \
--end "2026-06-01T18:00:00Z"
# Output includes your event_id and URL on ckb-pop.xyz# Open a 90-minute window with rotating QR codes
ckb-pop event window <EVENT_ID> --duration 90
# Terminal shows a QR code that refreshes every 30 seconds.
# Attendees scan and run: ckb-pop attend "<qr_data>"# Run this after scanning the organizer's QR code
ckb-pop attend "abc123...|1748000000|deadbeef01234567"
# Your badge transaction hash is printed on success.
# The badge appears in your gallery on ckb-pop.xyz.# Check if a badge exists for any address and event
ckb-pop badge verify <EVENT_ID> <ADDRESS>
# List all badges for an address
ckb-pop badge list --address ckt1qzda...Both contracts are RISC-V type scripts deployed on CKB testnet. They use hash_type: "type" (the type ID pattern), which means their identity is stable across upgrades.
Deployment transaction: 0x40ca450088affcbc5f2d8a06545566717106e99caad306776704aab9f3127934
- Code hash:
0xb36ed7616c4c87c0779a6c1238e78a84ea68a2638173f25ed140650e0454fbb9 - Deploy index: 0
- Data hash:
0x6e550910a640a41f21614d97d1d7b8c1830cbf11cce5c868c76a6fd0f25ba7a9
Type script args (64 bytes): SHA256(event_id) || SHA256(recipient_address)
The script enforces one badge per (event_id, recipient_address) pair. It rejects any transaction that has a badge cell with matching args in both inputs and outputs, making badges soulbound — they cannot be transferred.
Cell data (34 bytes):
| Bytes | Content |
|---|---|
| 0 | Version (0x01) |
| 1 | Flags (0x01 = off-chain metadata present) |
| 2–33 | SHA256 hash of the off-chain content JSON |
- Code hash:
0xd565d738ad5ac99addddc59fd3af5e0d54469dc9834cf766260c7e0d23c70b37 - Deploy index: 1
- Data hash:
0x24dfb1d2a7aca1e967c405e60017204459f3f8fe80e2c21683c4288ad4f5befb
Type script args (64 bytes): SHA256(event_id) || SHA256(creator_address)
The script enforces one anchor per (event_id, creator_address) pair. Once created, the anchor cannot be destroyed or modified, creating an immutable on-chain record of the event's existence.
Cell data: JSON object containing event_id, creator_address, metadata_hash, and created_at_block.
Both contracts use the 64-byte args format so that the CKB indexer can find all badges or anchors for a given event using script_search_mode: "prefix". The first 32 bytes (SHA256(event_id)) serve as the prefix for discovery, and the second 32 bytes (SHA256(address)) narrow results to a specific holder.
Neither contract is deployed on mainnet yet.
The CLI defines a Signer trait that abstracts signing across all supported methods:
trait Signer: Send + Sync {
fn address(&self) -> &str;
async fn sign_message(&self, message: &str) -> Result<String>; // 65-byte hex signature
async fn sign_transaction(&self, tx: Transaction) -> Result<Transaction>;
}All commands follow the same pattern: build an unsigned transaction → route to the active signer → broadcast the signed transaction.
| Method | How It Works |
|---|---|
browser |
The CLI starts a local HTTP server on a random port and opens a bundled signing page in your browser. The page loads the CCC SDK (embedded in the binary), connects to your wallet, presents the signing request, and POSTs the result back to the local server. No external network calls are needed to load the page. |
ledger |
USB HID communication with a Ledger hardware wallet running the CKB Ledger app. |
passkey |
FIDO2 assertion via platform authenticator. CKB supports passkey-based lock scripts natively. |
walletconnect |
The CLI displays a WalletConnect v2 QR code in the terminal. A mobile wallet scans it and approves the signing request. |
The browser signer is the default because it supports the widest range of wallets with zero hardware dependencies.
The bundled signing page (src/signer/ccc-bundle.js, ~836 KB) is compiled into the binary at build time using include_bytes!(). When invoked, the CLI:
- Binds a TCP listener on a port in the 17500–17599 range.
- Serves the HTML signing page and CCC bundle from memory.
- Opens the page in the system's default browser.
- The page connects to your wallet and presents the signing request.
- On approval, the JavaScript converts the CCC SDK's camelCase output to snake_case (CKB RPC format) and POSTs it to
/callback. - The CLI receives the signed data and continues.
The config file is created automatically at ~/.ckb-pop/config.toml on first use.
[network]
default = "testnet"
testnet_rpc = "https://testnet.ckb.dev/rpc"
mainnet_rpc = "https://mainnet.ckb.dev/rpc"
[signer]
method = "browser" # browser | ledger | passkey | walletconnect
address = "ckt1qzda..." # Set by 'ckb-pop signer connect'All config values can be overridden per-command with the global flags.
No database or local cache is maintained beyond this config file. The chain is the source of truth for badges and anchors.
Unit tests are embedded in each module and cover cryptographic operations, config serialization, QR parsing, and transaction structure.
cargo test --libIntegration tests require a live testnet connection and a configured signer. They are marked #[ignore] and must be run explicitly.
cargo test --test integration -- --ignored --nocaptureThe integration test suite includes an end-to-end test (event_creation_and_badge_mint_e2e) that exercises the full workflow: event creation, anchor confirmation, badge minting, and on-chain verification.
src/
├── main.rs # Entry point
├── lib.rs # Module declarations
├── cli.rs # Command definitions (clap)
├── config.rs # Config file management
├── contracts.rs # On-chain contract addresses and cell deps
├── crypto.rs # SHA256, HMAC, QR generation and verification
├── rpc.rs # CKB RPC and indexer client
├── tx_builder.rs # Unsigned transaction construction
├── commands/
│ ├── mod.rs # Shared command helpers
│ ├── signer.rs # signer subcommands
│ ├── event.rs # event subcommands
│ ├── attend.rs # attend command
│ ├── badge.rs # badge subcommands
│ └── tx.rs # tx subcommands
└── signer/
├── mod.rs # Signer trait
├── browser.rs # Browser signer implementation
└── ccc-bundle.js # Pre-built CCC SDK (embedded asset)
tests/
└── integration.rs # Integration tests (require network)
docs/
└── plans/ # Design documents
| Crate | Purpose |
|---|---|
ckb-sdk 5.x |
CKB RPC client, transaction building, address handling |
ckb-types 1.x |
CKB data types (H256, Script, Cell, etc.) |
ckb-jsonrpc-types 1.x |
CKB RPC JSON serialization |
clap 4 |
CLI argument parsing |
tokio 1 |
Async runtime |
reqwest 0.12 |
HTTP client for browser signer callback and backend API |
serde / toml |
Config serialization |
sha2 / hmac |
Event ID generation and QR HMAC verification |
qrcode |
Terminal QR code display |
anyhow / thiserror |
Error handling |
chrono |
Timestamp handling |
opener |
Open URLs in the system browser |