CKB Action Links: a draft protocol for shareable CKB transaction URLs

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.

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:

  1. A dApp deployed somewhere
  2. The user opens the dApp
  3. The dApp prompts a wallet connect
  4. The dApp constructs a transaction
  5. 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:

  1. 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?
  2. Parameter type enum. §6.1’s example shows only number and select. Reference SDK adds text as a baseline. What’s the smallest normative set publishers actually need? (email, url, boolean, date, textarea are obvious candidates.)
  3. OTX binary encoding. Spec prose says encoding: "molecule" is “binary”. Reference SDK enforces 0x-prefixed hex on the wire because JSON can’t carry raw bytes. Should the spec pin this, or leave room for base64?
  4. 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?
  5. Fiber Network alongside L1. Should Fiber payments share ckb-action:, or get their own fiber-action: URL scheme? They aren’t on-chain transactions but channel state updates - meaningfully different.
  6. Batch actions - one URL, multiple transactions. Useful primitive, or scope creep?

Try it

Live demo - no setup required:

  1. Open https://ckb-actions.vercel.app
  2. 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
  3. 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.

11 Likes

this is a really cool interface for open transactions. It’s been clear for a long time that they represent a very different way of building dapps and really good to see how you’ve connected them with a design pattern that already has adoption and experimentation around it.

I can comment on a couple open questions

  1. Fiber operates in a more “synchronous” communication kind of context between peers, this scheme seems better suited toward asynchronous communication across unknown peers

  2. Batch actions, this scheme seems better suited toward batch actions inside a single transaction, rather than multiple transactions, but this could just be my opinion, in any event, the downstream design patterns would be needed, so it seems quite early to explore integration here

Do you have other use cases in mind? Simple swaps (CKB for NFT for example) have to me always seem like very low hanging fruit for OTX.

4 Likes

Fiber: agreed, and you put the split better than I had. An Action URL is fire-and-forget to a peer the publisher never learns the identity of, while Fiber is a live session between two peers who already share a channel. Cramming that into a stateless URL means smuggling session state through query params, which kills the point. If there’s ever a fiber-action it should be its own thing, not a flag on this one.

Batch actions: you changed my mind. I was conflating two cases. Multiple transactions per link needs orchestration patterns nobody’s built yet, so it’s premature. But multiple operations inside one transaction is just a richer OTX, which CoBuild already handles, so it needs no protocol change. I’ll drop the multi-tx variant and note the single-tx case as already supported.

Swaps: yeah, CKB-for-NFT is the cleanest one. The holder builds an OTX with their NFT as input and a CKB output to themselves; the buyer’s wallet adds the CKB input and the cell that receives the NFT, then signs. No oracle, no external state, the whole deal is legible before anyone signs. That’s the first use case that isn’t just “consumer funds an output” but two parties contributing to one agreed tx, which is really what OTX was built for. If I add a fourth example it’ll be that.

Other things on my list: UDT/xUDT claim links (tip jar with a type script), DAO deposit/withdraw as one-click links, and paywall unlocks gated on the callback.

2 Likes