Hi all - sharing an early draft of CKB Action Links, a protocol that lets anyone publish a CKB transaction intent as a URL. A user clicks the link, their wallet renders a preview, they sign, the transaction lands on chain. No dApp navigation, no separate wallet-to-dApp connection flow. The wallet is the dApp.
- Live client: https://ckb-actions.vercel.app
- Live action endpoint: https://ckb-actions.onrender.com (try
/actions/tip-jar?recipient=<your-testnet-address>) - Repo (spec + reference SDK / server / client + three example actions): GitHub - truthixify/ckb-actions: CKB Action Links: a protocol and reference implementation for sharing CKB transactions as URLs. · GitHub
Looking for protocol-level feedback before the spec stabilizes.
Why
Even a one-shot interaction like “tip an author” or “pay an invoice” today looks like:
- A dApp deployed somewhere
- The user opens the dApp
- The dApp prompts a wallet connect
- The dApp constructs a transaction
- The wallet signs
If the only thing actually needed is this transaction, signed by you, the dApp is overhead. A URL that any wallet-aware client can resolve into a transaction preview removes every step except (5).
Solana shipped exactly this idea as Actions and Blinks; it’s now embedded in Phantom, Dialect, Twitter/X unfurls and a long tail of publishers. CKB’s cell model needs a slightly different adaptation, but the user-facing pattern is identical.
How it works
1. Publisher hosts: https://example.com/tip/truth
2. Consumer encounters the link (chat, QR, unfurl, email…)
3. Client: GET https://example.com/tip/truth
→ Manifest JSON (title, icon, buttons, parameters)
4. Client renders a preview card with one button per action
5. Consumer selects an action, fills any parameters
6. Client: POST https://example.com/tip/truth?amount=100
body: { address: "ckb1…", params: {} }
→ Open Transaction (OTX) with outputs specified
7. Wallet completes the OTX (inputs + fee + change), signs, submits
8. Client optionally POSTs the tx hash to the publisher's callback URL
Steps 1-4 are identical to Solana Actions. Steps 5-7 diverge - that’s the CKB-shaped part of the spec.
Cell model vs account model: why OTX
In an account-based chain the publisher can compose a complete transaction once they have the consumer’s pubkey: “transfer 100 from ”. Sign and submit.
In CKB the publisher doesn’t know which cells the consumer will spend. The only things the publisher can commit to are the outputs they want and any inputs they own themselves (e.g. a publisher-signed cluster cell in a DOB mint).
So the POST response carries an Open Transaction (OTX) rather than a complete tx:
- Outputs - fully specified by the publisher
- Inputs - empty if the consumer pays for everything; pre-signed by the publisher if they contribute any
- Witnesses - placeholders for consumer signatures
The consumer wallet runs the equivalent of tx.completeInputsByCapacity(signer) + tx.completeFeeBy(signer) from CCC to add input cells, change, and fee, then signs and submits. The reference client wires this exact flow.
This is the same OTX shape the CoBuild RFC defines - Action Links just give it an HTTP transport and a manifest schema so wallets can render a meaningful preview before signing.
Spec at a glance
URL forms (§5):
ckb-action:https://example.com/actions/tip/truth
https://example.com/actions/tip/truth
The ckb-action: prefix is preferred for explicit handler dispatch. Bare HTTPS works wherever the endpoint serves the manifest content type.
GET response - Manifest (§6.1):
{
"type": "action",
"title": "Tip Truth",
"description": "Send a tip in CKB",
"icon": "https://example.com/icon.png",
"label": "Send tip",
"network": "mainnet",
"links": {
"actions": [
{ "label": "Tip 100 CKB", "href": "/tip/truth?amount=100" },
{
"label": "Tip custom",
"href": "/tip/truth?amount={amount}",
"parameters": [
{ "name": "amount", "label": "Amount", "type": "number", "required": true }
]
}
]
}
}
The response carries X-CKB-Action: true so clients can tell action endpoints apart from arbitrary JSON.
POST response - Transaction (§6.2):
{
"type": "transaction",
"otx": "0x…",
"encoding": "molecule",
"message": "Tip 100 CKB to Truth",
"callback": "https://example.com/tip/truth/confirm"
}
Errors (§6.3) - a small normative tag enum so client UX stays consistent across publishers:
INVALID_ADDRESS, INSUFFICIENT_CAPACITY, INVALID_PARAMS, UNSUPPORTED_NETWORK, EXPIRED, INTERNAL.
Full spec: ckb-actions/spec.md at main · truthixify/ckb-actions · GitHub
Reference implementation
A TypeScript monorepo, all on main:
| Package | What |
|---|---|
@ckb-actions/sdk |
Zod schemas, error taxonomy, manifest fetcher, OTX response parser, href templating |
@ckb-actions/server |
Express 5 reference Action Endpoint with the prescribed middleware stack and SDK→HTTP error mapping |
@ckb-actions/client |
Vite + React + Tailwind + CCC connector. Preview tab (consume any URL) and Create tab (publish a tip / invoice URL from your wallet) |
@ckb-actions/example-tip-jar |
§11.1 - dynamic recipient via query param, real CCC OTX |
@ckb-actions/example-invoice |
§11.3 - POST /create endpoint, real OTX, mark-paid callback hook |
@ckb-actions/example-dob-mint |
§11.2 - publisher-input + spore output pattern (placeholder spore type hashes; real spore-sdk values left as a publisher concern) |
132 tests across the workspace. Wallet sign / send is wired through CCC end-to-end, with confirmation polling via client.waitTransaction. Tx hashes link to pudge.explorer.nervos.org or explorer.nervos.org depending on manifest.network.
Open questions
These are the things I’d most appreciate input on - they’re also tracked in spec.md §12:
- Publisher identity. Trust is currently anchored to the HTTPS domain (§10.4). Worth standardizing a signed-Manifest format now, or wait for a concrete attack to motivate it?
- Parameter type enum. §6.1’s example shows only
numberandselect. Reference SDK addstextas a baseline. What’s the smallest normative set publishers actually need? (email,url,boolean,date,textareaare obvious candidates.) - OTX binary encoding. Spec prose says
encoding: "molecule"is “binary”. Reference SDK enforces0x-prefixed hex on the wire because JSON can’t carry raw bytes. Should the spec pin this, or leave room for base64? - Spore protocol code hashes. Should the spec list canonical Spore type-script code hashes per network for DOB-style actions, or defer to publishers and the Spore project’s own documentation?
- Fiber Network alongside L1. Should Fiber payments share
ckb-action:, or get their ownfiber-action:URL scheme? They aren’t on-chain transactions but channel state updates - meaningfully different. - Batch actions - one URL, multiple transactions. Useful primitive, or scope creep?
Try it
Live demo - no setup required:
- Open https://ckb-actions.vercel.app
- Create tab → connect a CCC-compatible testnet wallet → leave the Action server field as
https://ckb-actions.onrender.com→ “Create invoice” or copy the tip URL - Preview tab → paste the URL → Sign & send with wallet → testnet tx hash links to pudge.explorer.nervos.org
The Render free tier spins down after 15 min idle, so the first request after a quiet period takes ~30 s to cold-start. The client surfaces this as “Fetching manifest…” while it waits - give it a beat.
Locally:
git clone https://github.com/truthixify/ckb-actions
cd ckb-actions
pnpm install
pnpm --filter @ckb-actions/sdk build
pnpm --filter @ckb-actions/server dev # :3000
pnpm --filter @ckb-actions/client dev # :5173
Same flow, but point the Action server field at http://localhost:3000.
What I’d like from this thread
- Disagreement with any of the spec’s choices, especially the OTX / manifest shape
- Suggestions for the open questions above
- Pointers to prior CKB work in this space I might’ve missed
- Wallets or publishers interested in being early integrators
Thanks for reading.