How Cairn works,
explained properly.
The homepage is the elevator pitch. This page is the whiteboard. Same project, more details, fewer slogans. If you read all of it, you'll understand exactly what trust Cairn removes from your supply chain — and what it can't.
What's broken about installing code today
Every time you run npm install or pip install, you're trusting a long chain of strangers. The registry company. Their employees. Every maintainer of every package in your tree. The CDN their files happen to be on today. The DNS resolver pointing you there. Any one of them can lie, and you have basically no way to notice.
That trust gets abused, regularly, in three ways most developers now know by name:
- ·Typosquatting. Someone registers
colorssnext tocolors, ships malicious code, and waits for the typo. Names look identical to humans; installs don't care. - ·Versions that mutate. The bytes behind
foo@1.0.4the day you audited it are not necessarily the bytes behindfoo@1.0.4next Tuesday. Lockfiles help, but only if you trust the registry to honor them. - ·Stolen or sold accounts. A maintainer's session gets phished, or someone buys a burnt-out solo dev's GitHub for a few hundred dollars, and the next release of a library you've trusted for five years is suddenly hostile.
None of this is exotic. It happens in production every quarter, gets a CVE, a blog post, a couple of postmortems, and then the next one shows up. The reason it keeps happening isn't bad engineering — it's that the whole system runs on names you trust people to keep honest. And then agents showed up.
Agents install fast. An autonomous coding agent might pull in fifty dependencies in a workflow, look at none of them, and never raise an eyebrow when one of them is wrong. The threat model that humans tolerate falls apart when there's no human in the loop.
Cairn is a different shape of registry, designed so the three attacks above just don't work. Not “mitigated” — actually don't work. The rest of this page is how.
Cairn is four small things, stacked
The whole protocol is four parts. None of them is novel on its own — that's on purpose. Boring crypto, boring contracts, boring data structures. The interesting bit is how they compose.
A CID per release
Hash the bytes. The hash is the address. Same bytes, anywhere, any time, give the same CID. That's it.
A did:cairn identity
An ed25519 keypair. The public key, base32-encoded, is your identity. No accounts to create, nothing to sign up for.
A short manifest
Six fields of JSON tying name, version, CID, your DID, and the time together. You sign it once. Anyone can verify.
A tiny Base L2 contract
~200 lines of Solidity. Maps name@version → cid. Writes are permanent. Anyone can publish.
The mesh layer that actually delivers the bytes between people is a fifth thing, but it's deliberately not in the trust circle. You can swap it out for IPFS, Filecoin, raw HTTPS, a USB stick — and Cairn still works. We'll come back to that.
The address is the proof
A Content Identifier is a short string that you derive deterministically from a blob of bytes. Hash the bytes; encode the hash; that's the CID. Hand someone a CID, and they can verify whether any blob claiming to be it actually is, just by re-hashing locally.
Cairn uses CIDv1 with SHA-256 and the raw codec. It's a deliberately minimal pick. SHA-256 is everywhere, fast, and not collision-prone in practice. The raw codec means we treat your release as opaque bytes — no IPFS chunking, no IPLD wrapping, nothing to argue about. The hash is the file's identity.
import { CID } from "multiformats/cid";
import { sha256 } from "multiformats/hashes/sha2";
import * as raw from "multiformats/codecs/raw";
export async function bytesToCID(bytes: Uint8Array): Promise<string> {
const hash = await sha256.digest(bytes);
const cid = CID.createV1(raw.code, hash);
return cid.toString();
// → "bafkreihv3vjpx2c5qbb6...rfqp4"
}Forty-something characters of base32. Easy to log, easy to paste, URL-safe, case-insensitive in the bits you can mess up. The Cairn client never trusts the network to deliver the right bytes — it trusts arithmetic.
No accounts, just keys
An author is an ed25519 keypair. The public key, encoded in base32 and prefixed did:cairn:, is your identity. That's the whole sign-up flow.
did:cairn:z6MkuWqTNwHWxgY6JKaPxQYwL3rT8b9vKsRBL2nE7wQ4Anyone who knows the DID also knows the public key (it's literally encoded in the string), so anyone can verify a signature against it. There's no DID resolver to hit, no registry to consult, no account to be deactivated by some upstream company. The DID exists the moment your keypair exists, and stops existing the moment you forget about it.
Right now the dApp keeps your key in browser localStorage. That's fine for a demo, terrible for anything you care about. The proper CLI (on the roadmap) will move keys into your OS keychain and support hardware-backed signers — YubiKey, Secure Enclave, Ledger.
The manifest is six fields and a signature
Every release ships a tiny JSON file alongside the bytes. This is the thing your DID actually signs.
{
"spec": "cairn/v1",
"name": "pixel-cairn",
"version": "0.2.1",
"cid": "bafybeicairn0003pixelmascotreleaseccccccccccccccccccc",
"did": "did:cairn:z6Mki…7wQ4",
"publishedAt": "2026-05-22T14:31:07Z"
}The tricky bit is the canonicalization. A signature is over bytes, and JSON can be serialized 100 ways for the same logical object — different key order, different whitespace, different number formatting. So we define one canonical form and stick to it: explicit key order, no whitespace, no surprises. Two compliant implementations will produce the exact same bytes from the same manifest.
export function canonicalize(m: CairnManifest): Uint8Array {
// explicit key order — never trust Object.keys() insertion semantics
const ordered = {
spec: m.spec,
name: m.name,
version: m.version,
cid: m.cid,
did: m.did,
publishedAt: m.publishedAt,
};
return new TextEncoder().encode(JSON.stringify(ordered));
}The signature travels next to the manifest, not inside it. Keeps the manifest readable, and makes the signed bytes exactly the manifest bytes — no envelope to strip before verifying.
A contract that does one thing
CairnRegistry.sol lives on Base. It maintains an append-only map from name@version to cid, plus the author address and a timestamp. That's the whole contract. Anyone can call publish(), anyone can call resolve(), nobody can edit a release after the fact.
function publish(
string calldata name,
string calldata version,
string calldata cid
) external {
if (bytes(name).length == 0) revert EmptyName();
if (bytes(version).length == 0) revert EmptyVersion();
if (bytes(cid).length == 0) revert EmptyCid();
address currentOwner = _owners[name];
if (currentOwner == address(0)) {
// first publish — claim the name
_owners[name] = msg.sender;
_packages.push(name);
} else if (currentOwner != msg.sender) {
revert NameTaken(currentOwner);
}
Release storage existing = _releases[name][version];
if (existing.exists) revert VersionAlreadyPublished();
uint64 ts = uint64(block.timestamp);
_releases[name][version] = Release({
cid: cid, author: msg.sender, timestamp: ts, exists: true
});
_versions[name].push(version);
emit PackagePublished(name, version, cid, msg.sender, ts);
}If you remove all the comments, there are three real rules:
- ① First person to publish a name owns it from then on.
- ② Only the owner can publish further versions under that name.
- ③ A given
name@versioncan be written exactly once. Republishing the same pair reverts.
Read side, for the dApp and anyone who wants to index:
function resolve(string calldata name, string calldata version)
external view returns (string memory cid, address author, uint64 timestamp);
function ownerOf(string calldata name) external view returns (address);
function versionsOf(string calldata name) external view returns (string[] memory);
function packageCount() external view returns (uint256);
function packageAt(uint256 index) external view returns (string memory);And one event so off-chain indexers can keep up:
event PackagePublished(
string indexed name,
string version,
string cid,
address indexed author,
uint64 timestamp
);The mesh delivers; it doesn't decide
The registry tells you which bytes to fetch (by CID). It doesn't store the bytes themselves — that'd be expensive, slow, and pointless. The bytes live on a mesh.
In the alpha, the mesh is a local simulation. We label it as such everywhere it shows up in the UI (the “alpha · simulated telemetry” chip on the live mesh panel, the // STUB: markers in lib/cid.ts). In production the same interface plugs into any of:
- → IPFS or Filecoin via a pinning service (Pinata, web3.storage)
- → A libp2p DHT with Bitswap retrieval
- → Plain HTTPS gateways — basically a CDN
- → A self-hosted mirror for air-gapped or audit-sensitive setups
Five steps, in order
When you publish a release, here's what the client actually does:
- step 1 · hashWalk your build output, serialize it deterministically (tarball with sorted entries and zeroed mtimes), SHA-256 the result, encode as CIDv1.
- step 2 · manifestAssemble the v1 manifest — name, version, CID, your DID, UTC timestamp — and canonicalize it.
- step 3 · signed25519-sign the canonical bytes with your private key. The signature is detached and travels next to the manifest.
- step 4 · pinPush the bytes to the mesh. Wait for some number of mirrors to acknowledge they have the CID. (Alpha: this is simulated. Real: Pinata or libp2p.)
- step 5 · registerSend one transaction: publish(name, version, cid). On confirmation, the name → CID mapping is permanent on Base.
Steps 1 through 4 are local or peer-to-peer. Step 5 is the only one that touches a shared system, and it's permissionless — anyone with gas can publish.
Five steps, in reverse
When you (or an agent) run cairn install pixel-cairn@0.2.1:
- step 1 · lookupCall registry.resolve("pixel-cairn", "0.2.1") on Base. Get back (cid, author, timestamp). One read, often cached.
- step 2 · fetchAsk the mesh for the CID. Any node will do. Where the bytes come from doesn't matter — yet.
- step 3 · re-hashHash the bytes you received. If the CID doesn't match the one from the registry, drop them and try a different mirror.
- step 4 · verifyFetch the manifest (also content-addressed). Re-canonicalize. Check the ed25519 signature against the DID it claims to be signed by.
- step 5 · installOnly after all of the above does anything land on disk.
Every check above is local. The agent doesn't need to trust any server, any maintainer, or any mirror. It needs to trust the registry contract (open source, verifiable on Basescan), SHA-256, and ed25519. That's the entire trust base.
The honest split
Cairn is not a panacea. It shrinks the trust surface; it doesn't eliminate it. Here's the split, written plainly:
things that just stop working
- ✓ Typosquatting — a name maps to one CID, ever
- ✓ Silent version mutation — versions are write-once on-chain
- ✓ Compromised mirror serving bad bytes — caught on re-hash
- ✓ MITM in transit — same reason
- ✓ Registry server takedown — there is no registry server
- ✓ Platform-account hijack — there is no platform account
things you still have to worry about
- ✗ Someone steals your private key — they are now you
- ✗ Your wallet gets drained — the new owner now owns your names
- ✗ Maintainer ships intentionally bad code — Cairn proves who shipped it, not that it's safe
- ✗ Bugs in the contract — audit pending
- ✗ Base going down — reads keep working from cache; writes pause
The point is to make the right column small and recoverable, and the left column impossible. Today's registries have the same right column (or worse), plus a left column that includes “whoever happens to work at the registry company this week.” Removing that one entry is most of what Cairn is for.
A note about machines doing this at machine speed
Humans, mostly, don't actually audit their dependencies. We pin versions, we glance at the lockfile, we trust the maintainer's reputation, we move on. It works because the tempo is slow enough that catastrophic mistakes get caught eventually.
An agent does not have that tempo. Give an autonomous coding agent a task and it will happily pull fifty packages, require the wrong one, and never notice. There's no human to glance at anything.
Cairn gives an agent two things it couldn't otherwise have cheaply:
- 1.Reproducibility for free. Pin a CID and you get the same bytes forever, anywhere. Lockfile drift just stops happening.
- 2.Verification it can actually afford. Hashing fifty megabytes is microseconds. Verifying an ed25519 signature is microseconds. The agent can do both on every single install without you noticing in the bill.
On the roadmap there's an MCP server — cairn.resolve, cairn.verify, a couple more — so any MCP-capable agent (Claude, Cursor, your in-house orchestrator) gets all of this with no glue code.
The honesty note
We're at v0.2.1-alpha. The split below is the truth, so you know what you're looking at when you click around.
real, today
- · The Solidity registry, deployable to Base + Base Sepolia
- · Hardhat tests covering publish, resolve, immutability, ownership
- · CID hashing in the dApp via
multiformats - · ed25519 signing + verifying via
@noble/ed25519 - · Persistent
did:cairnidentity inlocalStorage - · Real on-chain publish using wagmi + viem + RainbowKit
simulated, alpha
- · The “mirrored to N nodes” mesh layer
- · Live node telemetry on the marketing page
- · Pinata / Filecoin / libp2p integration (stubbed)
- · The CLI binary and MCP server (not built yet)
Every stubbed call carries a // STUB: comment in the source with a description of what real infrastructure plugs in. The UI labels itself as alpha wherever the mesh is mocked.
FAQ
Why Base and not Ethereum mainnet?
Names are low-frequency, high-value writes — a handful per maintainer per month. L1 would be ~30× more expensive for identical guarantees. Off-chain alternatives like DNS or ENS push trust back to a registrar. Base hits the right tradeoff in 2026.
What stops me from publishing under someone else's name?
The contract does. ownerOf(name) is set on first publish; every subsequent publish must come from the same address, or it reverts with NameTaken. There's no admin override.
Can I delete a release?
No, and that's the whole point. Once a name@version is on-chain, its CID is set forever. If a release is broken or compromised, the move is to publish a fixed version — and let downstream consumers explicitly upgrade. Deletion would be a hole in the immutability guarantee, so we don't have one.
What if the bytes are lost from the mesh?
The on-chain CID is still there. Anyone who has the bytes can re-pin them and the package becomes installable again. Cairn separates naming (permanent) from delivery (replaceable) on purpose — losing bytes is annoying, but recoverable.
Isn't this just IPFS with extra steps?
IPFS solves byte delivery. It doesn't solve naming, ownership, or version commitment — and those are the parts that actually matter for a supply chain. Cairn layers an on-chain name registry with first-publisher-wins semantics and a signed-manifest convention on top of whatever delivery layer you like. You can run Cairn on IPFS. You can run it on Filecoin, libp2p, or HTTPS gateways. The trust story doesn't change.
What about npm compatibility?
A compatibility shim is on the Q3 2026 roadmap. The idea is a local proxy: npm install reads from Cairn, npm publish writes to Cairn, and the two namespaces map cleanly. You'd be able to migrate package-by-package without breaking anyone downstream.
Is this audited?
Not yet. The contract is small (~200 lines, no proxies, no upgradeability, no admin) which makes auditing tractable, but until a third party signs off, don't trust it with anything you can't afford to lose. We'll publish the audit report when there is one.

That's the whole protocol.
Four pieces, three rules in the contract, one promise. Everything else is just engineering.