EVMFS.app

How it works

  1. Build your site — Create or import files in the workspace below. Any static site works: HTML, CSS, JS, images, fonts.
  2. Upload to EVMFS — Files are zipped client-side (DEFLATE), split into 20 KB chunks, and each chunk stored on-chain via Block.sol. The sequence is registered in EVMFS.sol.
  3. Get a CID — On-chain rolling SHA-256 produces a 32-byte Content ID — your permanent, tamper-proof address on Ethereum.
  4. Register a name — Bind your CID to <name>.evmfs.app via EVMFSRegistry.sol. Tiered pricing: 7+ chars = 0.015 ETH.
  5. Zero-server serving — Visitors open <name>.evmfs.app. A Service Worker fetches all chunks from chain, verifies the CID hash, decompresses the ZIP, caches files in IndexedDB, and intercepts every request — no servers, forever.
Gateway & Service Worker architecture
Browser → visits <name>.evmfs.app
↓ served by nginx (static)
Bootloader (index.html)
↓ reads label from subdomain
↓ eth_call → EVMFSRegistry.getCid(label) on Sepolia
↓ navigator.serviceWorker.register(sw.js?cid=...&chain=...)
Service Worker (Virtual Disk Controller)
↓ receives LOAD_SITE message
↓ raw JSON-RPC eth_call → EVMFS.getFileByCID(cid)
↓ fetch all blocks → reassemble byte-array
↓ recompute rolling SHA-256 → verify CID
↓ decompress ZIP (JSZip) → extract files
↓ store in IndexedDB (path → content + MIME)
✓ intercepts fetch() → serve from IndexedDB forever
Folder structure lives inside the ZIP — any static layout works
Non-file paths → SPA fallback to index.html
Works offline after first load (full IndexedDB cache)
Select a file to edit
Compresses workspace → uploads to Sepolia

Builds, zips, uploads to EVMFS, and optionally registers an app name. Uses our deployed contracts on Sepolia.

Shell
ETH_PRIVATE_KEY=0xYOUR_KEY \
BLOCK_ADDR=0x4571D19Ada5Ec6853bd7Bef5fBe9F0f94238419c \
EVMFS_ADDR=0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A \
node scripts/evmfs-build-and-deploy.mjs \
  --network sepolia \
  --skip-deploy \
  --register-app --app-name myapp

Flags

--skip-deployUse existing contracts (default for most users)
--skip-buildReuse existing dist/
--skip-ipfsUpload to EVMFS only, skip Pinata pin
--ipfs-onlyPin only (dist-evmfs/ must exist)
--loader-onlyRegenerate loader HTML and re-pin
--force-staticsRe-upload framework JS to IPFS
--register-appRegister app name in EVMFSRegistry after upload
--update-cidUpdate CID for an existing registered name
--app-name <n>App name for registry (required with register/update)
--registry-addrOverride EVMFSRegistry address
Tamper-proof — The CID is computed from blockchain data using rolling SHA-256. Even a malicious RPC node cannot inject code — hash mismatch triggers rejection.
Tamper-proof — The CID is computed from blockchain data using rolling SHA-256. Even a malicious RPC node cannot inject code — hash mismatch triggers rejection.

What is EVMFS

A two-contract system — Block.sol (chunk store) and EVMFS.sol (file registry) — that turns any EVM chain into a permanent content-addressed file store.

Files are split into fixed-size chunks, each stored in its own Block. The sequence of blocks is registered as a File, identified by a deterministic 32-byte CID. No gateways, no off-chain relay, no expiry.

Storage
Every byte in EVM storage, replicated across all nodes.
Integrity
CID = rolling SHA-256 over chunks. Tamper → different CID.
Permanence
Finalized files cannot be deleted or modified.
Composable
Any contract can look up a CID and read raw bytes.

Architecture

Block.sol — raw chunk store
createBlock(bytes data) → blockId
getBlock(blockId) → { id, data }

EVMFS.sol — file registry (depends on Block.sol)
createFile(bytes chunk, bool end) → fileId start
upload(fileId, bytes chunk, bool end) append
getFileByCID(bytes cid) → FileData
getFileBlocks(fileId) → BlockData[]

EVMFSRegistry.sol — name → CID mapping (evmfs.app gateway)
registerApp(name, cid) payable claim
updateCid(name, cid) update
getCid(name) → bytes

Virtual filesystem & ZIP layout

EVMFS stores a single ZIP per CID — not individual files. Every folder and file lives inside that ZIP as standard directory entries. The gateway decompresses it and maps each path to an IndexedDB key.

my-app.zip (uploaded as one EVMFS file)
├─ index.html entry point
├─ style.css
├─ app.js
├─ assets/
│ ├─ logo.svg
│ └─ font.woff2
└─ data/
└─ config.json

Serving rules (VDC service worker):
/index.html → text/html
/assets/logo.svg → image/svg+xml
/app/dashboard → SPA fallback → /index.html
any other origin → network (not intercepted)

Any static site works: plain HTML, React/Vite builds, frameworks, WASM binaries. As long as it can be served from a flat file tree, it can live on EVMFS.

Service Worker lifecycle

Install
SW registered with CID as URL param. self.skipWaiting() activates immediately.
Activate
Claims all clients instantly. Cleans up old IndexedDB caches from stale CIDs.
Load site
On LOAD_SITE message: fetch blocks from chain, verify CID, decompress ZIP, store all files.
Fetch intercept
Same-origin requests → IndexedDB lookup → Response with correct MIME + CSP headers.
CID integrity
Rolling SHA-256 recomputed client-side. Hash mismatch → rejected. Tamper-proof even against malicious RPC.
Offline
After first load, site is fully cached in IndexedDB. Works permanently without network.
No ethers.js in the Service Worker — The VDC uses raw eth_call JSON-RPC with manually-encoded calldata. Zero external dependencies in the SW means zero supply-chain risk.

CID computation

A 32-byte value computed by rolling SHA-256 as chunks are uploaded on-chain. Uses the EVM’s native sha256() precompile.

h₁ = SHA-256(chunk₁)
h₂ = SHA-256(h₁chunk₂)
hₙ = SHA-256(hₙ₋₁chunkₙ) CID

CID = "0x" + hex(hₙ) → 32 bytes, 64 hex chars

SHA-256 is hardware-accelerated via crypto.subtle.digest in browsers, so clients can recompute the CID locally after download and verify against the on-chain value — trustless integrity even against a malicious RPC node.

Contracts

ContractAddressNetwork
Block.sol0x4571D19Ada5Ec6853bd7Bef5fBe9F0f94238419cSepolia
EVMFS.sol0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2ASepolia
EVMFSRegistry.sol0x1DC0fe8Ad09F4FB6f36c5DEC81f4975fb3a85999Sepolia
EVMFSSubscription.sol0x21BfAAfC8eb807Cf695F3D4e3c99eA6D43bE8600Sepolia
Block.sol0x32DB8E8Eeb4A8Fec859fDAC5A6222608D847DB7FTaiko Hoodi
EVMFS.sol0x36bF7216C9F23dc9a2433B75B7be7971d3f78b47Taiko Hoodi
EVMFSRegistry.sol0xc4aca772da000649003951Fd3E9FF65b5001C008Taiko Hoodi
EVMFSSubscription.sol0xA427B3A109d95013c7990E08654FAaE722afe8f3Taiko Hoodi

IPFS framework assets

Permanent, content-addressed. Served via <CID>.ipfs.dweb.link subdomains.

AssetIPFS CID
ethers.umd.min.js v6bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u
evmfs-crypto.jsbafkreib3mqg77ixf42t6nzz4p2mocqk2fnv22rhrw2zx34epencepfnduu
evmfs-web-loader.jsbafkreieizmmxatd63zg6i2ven46hyxd7sze3iglrtd33tjp3zu7h4svzye
evmfs-zip-processor.jsbafkreih36uv3yimcjqua5ddjvsehsvzf3dvzwbhgfux6qjjevcmqakf7xy
jszip.min.jsbafkreifmy7sbivnia5s3l7m4p3q3qb4knulaxo6kivnov2cu3zs4sr6vty

Protocol reference

Two contracts, one CID. Everything you need to integrate EVMFS at the protocol level.

Single-chunk upload

Files under ~20 KB — one createFile(data, true) call.

JavaScript · Node.js
import { ethers } from "ethers";

const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const ABI   = [
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256 fileId)",
];

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const signer   = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider);
const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

const content = new TextEncoder().encode("Hello, EVMFS!");
const tx  = await evmfs.createFile(content, true);
const rec = await tx.wait();

const log = rec.logs
  .map(l => { try { return evmfs.interface.parseLog(l); } catch {} })
  .find(e => e?.name === "FileFinalized");
const cid = ethers.hexlify(log.args.cid);
// cid → "0x<64 hex chars>" — permanent on Sepolia
TypeScript · Node.js
import { ethers, type ContractTransactionResponse } from "ethers";

const EVMFS: string = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const ABI: string[] = [
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256 fileId)",
];

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const signer   = new ethers.Wallet(process.env.ETH_PRIVATE_KEY!, provider);
const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

const content: Uint8Array = new TextEncoder().encode("Hello, EVMFS!");
const tx: ContractTransactionResponse = await evmfs.createFile(content, true);
const rec = await tx.wait();

const log = rec!.logs
  .map((l: any) => { try { return evmfs.interface.parseLog(l); } catch {} })
  .find((e: any) => e?.name === "FileFinalized");
const cid: string = ethers.hexlify(log!.args.cid);
// cid → "0x<64 hex chars>" — permanent on Sepolia
HTML · Classic
<!-- ethers v6 UMD -->
<script src="https://bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u.ipfs.dweb.link"></script>
<script>
(async () => {
  const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
  const ABI   = [
    "event FileFinalized(uint256 indexed fileId, bytes cid)",
    "function createFile(bytes calldata data, bool end) returns (uint256 fileId)",
  ];
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer   = await provider.getSigner();
  const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

  const content = new TextEncoder().encode("Hello, EVMFS!");
  const tx  = await evmfs.createFile(content, true);
  const rec = await tx.wait();
  const log = rec.logs
    .map(l => { try { return evmfs.interface.parseLog(l); } catch {} })
    .find(e => e?.name === "FileFinalized");
  console.log("CID:", ethers.hexlify(log.args.cid));
})();
</script>
JavaScript · ES Modules
<script type="module">
import { ethers } from "https://esm.sh/ethers@6";

const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const ABI   = [
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256 fileId)",
];

const provider = new ethers.BrowserProvider(window.ethereum);
const signer   = await provider.getSigner();
const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

const content = new TextEncoder().encode("Hello, EVMFS!");
const tx  = await evmfs.createFile(content, true);
const rec = await tx.wait();
const log = rec.logs
  .map(l => { try { return evmfs.interface.parseLog(l); } catch {} })
  .find(e => e?.name === "FileFinalized");
console.log("CID:", ethers.hexlify(log.args.cid));
</script>

Multi-chunk upload

Files larger than 20 KB — split, upload first chunk with createFile, append the rest with upload.

JavaScript · Node.js
import { ethers }      from "ethers";
import { readFileSync } from "fs";
import { createHash }   from "crypto";

const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CHUNK = 20 * 1024;
const ABI   = [
  "event FileCreated(uint256 indexed fileId, address indexed owner)",
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256)",
  "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)",
];

function computeCID(chunks) {
  let h = createHash("sha256").update(chunks[0]).digest();
  for (let i = 1; i < chunks.length; i++)
    h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest();
  return "0x" + h.toString("hex");
}

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const signer   = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider);
const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

const raw    = readFileSync("./my-file.zip");
const chunks = [];
for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK));
console.log("Expected CID:", computeCID(chunks));

let nonce = await provider.getTransactionCount(signer.address, "latest");
const r = await (await evmfs.createFile(chunks[0], chunks.length === 1, { nonce: nonce++ })).wait();
const fileId = evmfs.interface.parseLog(r.logs[0]).args.fileId;

for (let i = 1; i < chunks.length; i++)
  await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1, { nonce: nonce++ })).wait();
TypeScript · Node.js
import { ethers }      from "ethers";
import { readFileSync } from "fs";
import { createHash }   from "crypto";

const EVMFS: string = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CHUNK: number = 20 * 1024;
const ABI: string[] = [
  "event FileCreated(uint256 indexed fileId, address indexed owner)",
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256)",
  "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)",
];

function computeCID(chunks: Buffer[]): string {
  let h = createHash("sha256").update(chunks[0]).digest();
  for (let i = 1; i < chunks.length; i++)
    h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest();
  return "0x" + h.toString("hex");
}

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const signer   = new ethers.Wallet(process.env.ETH_PRIVATE_KEY!, provider);
const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

const raw    = readFileSync("./my-file.zip");
const chunks: Buffer[] = [];
for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK));
console.log("Expected CID:", computeCID(chunks));

let nonce = await provider.getTransactionCount(signer.address, "latest");
const r = await (await evmfs.createFile(chunks[0], chunks.length === 1, { nonce: nonce++ })).wait();
const fileId: bigint = evmfs.interface.parseLog(r!.logs[0]).args.fileId;

for (let i = 1; i < chunks.length; i++)
  await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1, { nonce: nonce++ })).wait();
HTML · Classic
<script src="https://bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u.ipfs.dweb.link"></script>
<input type="file" id="file" />
<script>
const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CHUNK = 20 * 1024;
const ABI   = [
  "event FileCreated(uint256 indexed fileId, address indexed owner)",
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256)",
  "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)",
];

document.getElementById("file").addEventListener("change", async (e) => {
  const raw    = new Uint8Array(await e.target.files[0].arrayBuffer());
  const chunks = [];
  for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK));

  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer   = await provider.getSigner();
  const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

  const r = await (await evmfs.createFile(chunks[0], chunks.length === 1)).wait();
  const fileId = evmfs.interface.parseLog(r.logs[0]).args.fileId;
  for (let i = 1; i < chunks.length; i++)
    await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1)).wait();
  console.log("Upload complete, fileId:", fileId.toString());
});
</script>
JavaScript · ES Modules
<input type="file" id="file" />
<script type="module">
import { ethers } from "https://esm.sh/ethers@6";

const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CHUNK = 20 * 1024;
const ABI   = [
  "event FileCreated(uint256 indexed fileId, address indexed owner)",
  "event FileFinalized(uint256 indexed fileId, bytes cid)",
  "function createFile(bytes calldata data, bool end) returns (uint256)",
  "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)",
];

document.getElementById("file").addEventListener("change", async (e) => {
  const raw    = new Uint8Array(await e.target.files[0].arrayBuffer());
  const chunks = [];
  for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK));

  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer   = await provider.getSigner();
  const evmfs    = new ethers.Contract(EVMFS, ABI, signer);

  const r = await (await evmfs.createFile(chunks[0], chunks.length === 1)).wait();
  const fileId = evmfs.interface.parseLog(r.logs[0]).args.fileId;
  for (let i = 1; i < chunks.length; i++)
    await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1)).wait();
  console.log("Upload complete, fileId:", fileId.toString());
});
</script>

Read a file by CID

Read-only — no signer needed. Any public RPC node works.

JavaScript · Node.js
import { ethers } from "ethers";

const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CID   = "0x<your-cid-here>";
const ABI   = [
  "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))",
  "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])",
];

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const evmfs    = new ethers.Contract(EVMFS, ABI, provider);

const meta   = await evmfs.getFileByCID(CID);
const blocks = await evmfs.getFileBlocks(meta.id);

// Reassemble
const chunks = blocks.map(b => ethers.getBytes(b.data));
const total  = chunks.reduce((s, c) => s + c.length, 0);
const buf    = new Uint8Array(total);
let   off    = 0;
for (const c of chunks) { buf.set(c, off); off += c.length; }

console.log("Downloaded", total, "bytes");
TypeScript · Node.js
import { ethers } from "ethers";

const EVMFS: string = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CID: string   = "0x<your-cid-here>";
const ABI: string[] = [
  "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))",
  "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])",
];

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const evmfs    = new ethers.Contract(EVMFS, ABI, provider);

const meta   = await evmfs.getFileByCID(CID);
const blocks = await evmfs.getFileBlocks(meta.id);

// Reassemble
const chunks: Uint8Array[] = blocks.map((b: any) => ethers.getBytes(b.data));
const total: number  = chunks.reduce((s, c) => s + c.length, 0);
const buf    = new Uint8Array(total);
let   off    = 0;
for (const c of chunks) { buf.set(c, off); off += c.length; }

console.log("Downloaded", total, "bytes");
HTML · Classic
<script src="https://bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u.ipfs.dweb.link"></script>
<script>
(async () => {
  const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
  const CID   = "0x<your-cid-here>";
  const ABI   = [
    "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))",
    "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])",
  ];

  const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
  const evmfs    = new ethers.Contract(EVMFS, ABI, provider);

  const meta   = await evmfs.getFileByCID(CID);
  const blocks = await evmfs.getFileBlocks(meta.id);

  const chunks = blocks.map(b => ethers.getBytes(b.data));
  const total  = chunks.reduce((s, c) => s + c.length, 0);
  const buf    = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) { buf.set(c, off); off += c.length; }
  console.log("Downloaded", total, "bytes");
})();
</script>
JavaScript · ES Modules
<script type="module">
import { ethers } from "https://esm.sh/ethers@6";

const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A";
const CID   = "0x<your-cid-here>";
const ABI   = [
  "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))",
  "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])",
];

const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const evmfs    = new ethers.Contract(EVMFS, ABI, provider);

const meta   = await evmfs.getFileByCID(CID);
const blocks = await evmfs.getFileBlocks(meta.id);

const chunks = blocks.map(b => ethers.getBytes(b.data));
const total  = chunks.reduce((s, c) => s + c.length, 0);
const buf    = new Uint8Array(total);
let off = 0;
for (const c of chunks) { buf.set(c, off); off += c.length; }
console.log("Downloaded", total, "bytes");
</script>

Verify CID

Recompute the rolling SHA-256 client-side and compare — trustless integrity check.

JavaScript · Node.js
import { createHash } from "crypto";

function verifyCID(chunks, expected) {
  let h = createHash("sha256").update(chunks[0]).digest();
  for (let i = 1; i < chunks.length; i++)
    h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest();
  const computed = "0x" + h.toString("hex");
  return computed.toLowerCase() === expected.toLowerCase();
}

// chunks = array of Buffer/Uint8Array from getFileBlocks
console.log(verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
TypeScript · Node.js
import { createHash } from "crypto";

function verifyCID(chunks: Buffer[], expected: string): boolean {
  let h = createHash("sha256").update(chunks[0]).digest();
  for (let i = 1; i < chunks.length; i++)
    h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest();
  const computed: string = "0x" + h.toString("hex");
  return computed.toLowerCase() === expected.toLowerCase();
}

// chunks = array of Buffer from getFileBlocks
console.log(verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
JavaScript · Browser (SubtleCrypto)
async function verifyCID(chunks, expected) {
  let h = new Uint8Array(await crypto.subtle.digest("SHA-256", chunks[0]));
  for (let i = 1; i < chunks.length; i++) {
    const joined = new Uint8Array(h.length + chunks[i].length);
    joined.set(h); joined.set(chunks[i], h.length);
    h = new Uint8Array(await crypto.subtle.digest("SHA-256", joined));
  }
  const computed = "0x" + [...h].map(x => x.toString(16).padStart(2,"0")).join("");
  return computed.toLowerCase() === expected.toLowerCase();
}

// chunks = array of Uint8Array from getFileBlocks
console.log(await verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
JavaScript · Browser (SubtleCrypto)
// SubtleCrypto is built-in — no import needed.
async function verifyCID(chunks, expected) {
  let h = new Uint8Array(await crypto.subtle.digest("SHA-256", chunks[0]));
  for (let i = 1; i < chunks.length; i++) {
    const joined = new Uint8Array(h.length + chunks[i].length);
    joined.set(h); joined.set(chunks[i], h.length);
    h = new Uint8Array(await crypto.subtle.digest("SHA-256", joined));
  }
  const computed = "0x" + [...h].map(x => x.toString(16).padStart(2,"0")).join("");
  return computed.toLowerCase() === expected.toLowerCase();
}

// chunks = array of Uint8Array from getFileBlocks
console.log(await verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");

Subscription Plans

EVMFS uploads require a small per-block premium (0.00005 ETH) unless you hold an active subscription NFT. Plans are ERC-1155 tokens β€” fully on-chain, no admin keys, no middlemen.

Connect to see your NFT inventory and subscription status.

Monthly

0.002 ETH
30 days of free uploads
Best Value

Yearly

0.02 ETH
365 days of free uploads

Lifetime

0.2 ETH
Unlimited free uploads forever

How it works

  • With subscription: createFile() and upload() cost only gas (no premium).
  • Without subscription: each block write costs 0.00005 ETH, forwarded to the protocol beneficiary.
  • Subscriptions stack: buying two monthly plans gives you 60 days.
  • ERC-1155 tokens are minted as proof of purchase and remain in your wallet permanently.
  • addExistingBlock(), createFileEmpty(), and extendFile() are always free (no new blocks created).
  • Fully permissionless: no admin keys, no pause function. ETH goes directly to the immutable beneficiary.