N°07

Developer guide

Build with the SDK.

Everything on fileonchain.org runs on the open-source @fileonchain packages. Anchor CIDs from your own app with a wallet, through the hosted API with credits, or from an AI agent over MCP.

Getting started

There are three ways to anchor a CID, depending on who signs the transaction:

@fileonchain/sdk

Self-signed anchoring from your own app. Your users' wallets (or your server's keys) sign; you talk to the chain directly. One umbrella package, twelve chain families.

@fileonchain/api

Hosted anchoring with account credits. FileOnChain's workers sign and send — no wallet, no chain SDK, just an fok_ API key from the dashboard.

@fileonchain/mcp

Anchoring for AI agents. A stdio MCP server exposing registry lookups, CID validation, and API-backed anchoring as tools.

terminal
pnpm add @fileonchain/sdk
# EVM chains additionally need:       pnpm add viem
# Substrate chains additionally need: pnpm add @polkadot/api
# Solana chains additionally need:    pnpm add @solana/web3.js
# The other nine families are dependency-free.

@fileonchain/sdk — the umbrella

Source & README →

The package most apps should install. The root entry re-exports the entire dependency-free core (chain registry, CID validation, anchor payloads) plus the EVM contract ABIs. Each chain family lives on its own subpath — @fileonchain/sdk/evm, …/substrate, …/solana, and so on through all twelve — and …/api re-exports the hosted API client. Heavy chain dependencies stay opt-in: only the EVM, Substrate, and Solana subpaths have (optional) peer dependencies.

registry.ts
import { CHAINS, getChain, getChainsByFamily, buildTxUrl, isValidCID } from "@fileonchain/sdk";

const base = getChain("evm:8453");
base?.registryContract; // FileRegistry address on Base (null / zero until deployed)

getChainsByFamily("substrate").map((c) => c.name);
isValidCID("bafybeig..."); // CIDv1 base32 check

buildTxUrl(base!, "0xabc..."); // explorer link for a transaction

Every family exports the same anchorChunkedFile with an identical progress and receipt shape — chunk anchors first, the file-level anchor last (indexers rely on that ordering). A folder anchors exactly like a file: anchor the CID of its DAG root. The file-level anchor is a free registry event, and its receipt slots straight into an evidence package — the protocol page explains the v1 anchor flow and how anyone verifies an evidence package.

anchor.ts
import { anchorChunkedFile } from "@fileonchain/sdk/substrate";

const receipt = await anchorChunkedFile(api, {
  chainId: "substrate:autonomys-mainnet",
  address: "5F...",
  signer: injectedSigner,
  fileCid: "bafybeig...",
  chunks: [{ cid: "bafk...", index: 0, nextCid: undefined, data: bytes }],
  onProgress: ({ stage, chunksAnchored, chunksTotal }) => {
    // "connecting" | "signing" | "submitting" | "confirming" | "confirmed"
  },
});

receipt.txHashes; // every transaction sent
receipt.txHash;   // the file-level anchor (always sent last)

@fileonchain/utils — the core

Source & README →

The dependency-free foundation everything else builds on — the umbrella's root entry re-exports all of it, so you only depend on it directly when you want chain metadata without any anchoring code. It is the single source of truth for:

  • The chain registry — CHAINS, getChain(id), getChainsByFamily(family), MAINNET_CHAINS / TESTNET_CHAINS, and explorer helpers buildTxUrl / buildAddressUrl. Contract addresses live on the chain entries themselves.
  • CID validation — isValidCID and validateOrError (CIDv1 base32).
  • The anchor vocabulary — buildFileAnchorPayload, buildChunkAnchorPayload, and parseAnchorPayload: the versioned JSON payloads written identically on every family.
  • Orchestration helpers — buildChunkedAnchorPayloads, assertPayloadFits, batchByBytes / batchByCount, and runSequentialChunkedAnchor — the shared machinery behind every family client.

Family clients — @fileonchain/sdk-*

One anchor client per chain family, all published standalone and re-exported by the umbrella. Nine of the twelve are fully dependency-free: the SDK owns payload building, ordering, batching, and size validation, while a minimal structural signer interface owns transport — adapt an injected wallet in the browser or the chain's own SDK on a server.

PackageHow it anchorsPeer dependencySigner
@fileonchain/sdk-evmFileRegistry.anchorCID contract call per chunk + fileviemviem WalletClient
@fileonchain/sdk-substratesystem.remarkWithEvent batched via utility.batchAll@polkadot/apipolkadot.js signer
@fileonchain/sdk-solanaSPL Memo program (native — always provisioned)@solana/web3.jsSolanaAnchorSigner
@fileonchain/sdk-aptosMove module file_registry::anchor_ciddependency-freeAptosAnchorSigner
@fileonchain/sdk-cosmosTransaction memo, one payload per txdependency-freeCosmosAnchorSigner
@fileonchain/sdk-suiMove calls batched into programmable transaction blocksdependency-freeSuiAnchorSigner
@fileonchain/sdk-starknetanchor_cid multicalls on the Cairo FileRegistrydependency-freeStarknetAnchorSigner
@fileonchain/sdk-nearanchor_cid on the WASM registry contractdependency-freeNearAnchorSigner
@fileonchain/sdk-tronTransaction data/memo field, one payload per txdependency-freeTronAnchorSigner
@fileonchain/sdk-cardanoCIP-20 transaction metadata (label 674)dependency-freeCardanoAnchorSigner
@fileonchain/sdk-tonText comment on a minimal self-transferdependency-freeTonAnchorSigner
@fileonchain/sdk-hederaConsensus Service message on the registry topicdependency-freeHederaAnchorSigner

Every package is also importable as @fileonchain/sdk/<family>. Besides anchorChunkedFile, each exposes a file-level entrypoint — anchorCID on the contract families, anchorCIDWithRemark / anchorCIDWithMemo / anchorCIDWithMetadata / anchorCIDWithComment / anchorCIDWithMessage on the remark, memo, metadata, comment, and consensus-message families.

EVM — contract calls with viem

evm.ts
import { anchorCID, getCIDRecord } from "@fileonchain/sdk/evm";
import { createWalletClient, custom } from "viem";

const walletClient = createWalletClient({
  account: "0x...",
  transport: custom(window.ethereum),
});

// The file-level anchor is a free registry event; the receipt slots
// straight into an evidence package as its settlement receipt.
const receipt = await anchorCID(walletClient, {
  chainId: "evm:8453",
  cid: "bafybeig...",
  uri: "ipfs://bafybeig...",
});
receipt.txHash;
receipt.blockNumber;
receipt.blockHash;
receipt.submitter;

// Later — anyone can read the first-write record without a wallet:
const record = await getCIDRecord("evm:8453", "bafybeig...");
record?.submitter;

Substrate — remarks with @polkadot/api

substrate.ts
import { anchorCIDWithRemark } from "@fileonchain/sdk/substrate";
import { ApiPromise, WsProvider } from "@polkadot/api";
import { getChain } from "@fileonchain/sdk";

const chain = getChain("substrate:autonomys-mainnet")!;
const api = await ApiPromise.create({ provider: new WsProvider(chain.rpcUrl) });

const receipt = await anchorCIDWithRemark(api, {
  chainId: "substrate:autonomys-mainnet",
  address: "5F...",
  signer: injectedSigner, // e.g. from @polkadot/extension-dapp
  cid: "bafybeig...",
});

Solana — SPL Memo with @solana/web3.js

solana.ts
import { anchorCIDWithMemo } from "@fileonchain/sdk/solana";
import { Connection } from "@solana/web3.js";

const connection = new Connection(rpcUrl);

// `wallet` is any SolanaAnchorSigner — Phantom, Solflare, and
// wallet-standard adapters match the interface out of the box.
const { signature, slot, memo } = await anchorCIDWithMemo(connection, wallet, {
  chainId: "solana:mainnet",
  cid: "bafybeig...",
});

The dependency-free nine — bring a signer

Aptos, Cosmos, Sui, Starknet, NEAR, TRON, Cardano, TON, and Hedera follow the same pattern: the client takes a small signer object instead of a chain SDK. Injected wallets (Petra, Keplr, Argent, TronLink, …) match the interfaces directly; on a server, wrap the chain's SDK in a few lines.

aptos.ts
import { anchorChunkedFile } from "@fileonchain/sdk/aptos";

// The dependency-free families take a minimal structural signer —
// adapt an injected wallet (browser) or the chain's SDK (server).
const receipt = await anchorChunkedFile(petraWallet, {
  chainId: "aptos:mainnet",
  fileCid: "bafybeig...",
  chunks: [{ cid: "bafk...", index: 0 }],
  onProgress: ({ stage, chunksAnchored, chunksTotal }) => {},
});

@fileonchain/api — hosted anchoring

Source & README →

A zero-dependency typed client for the hosted /api/v1/* endpoints. FileOnChain's workers sign and send the transactions, paid with account credits — useful when you don't want wallets or chain SDKs anywhere near your code. Create an API key under Dashboard → API keys.

hosted.ts
import { FileOnChainClient, FileOnChainApiError } from "@fileonchain/api";

const client = new FileOnChainClient({
  apiKey: process.env.FILEONCHAIN_API_KEY!, // fok_… from /dashboard/keys
});

const job = await client.anchor({
  cid: "bafybeig...",
  fileName: "data.bin",
  fileSizeBytes: 150_000,
  chunkCount: 3,
  chainIds: ["evm:870"], // must be status: "active" chains
  paymentMethod: "credits",
});

job.txHashes;                    // one { chainId, txHash, blockNumber } per chain
await client.waitForJob(job.id); // poll until complete/failed
await client.getCredits();       // { balanceMicroUsdc, balanceUsdc }

Errors surface as FileOnChainApiError with a .status: 401 bad key, 402 insufficient credits, 503 hosted signer not configured, 502 on-chain send failed (anchoring failures refund credits).

@fileonchain/mcp — for AI agents

Source & README →

A stdio MCP (Model Context Protocol) server. Five read-only tools serve the chain registry with no configuration (list_chains, get_chain, validate_cid, parse_anchor_payload, build_explorer_url); three API-backed tools (anchor_cid, get_anchor_job, get_credits) spend account credits through the hosted API — the server never holds private keys.

mcp-config.json
{
  "mcpServers": {
    "fileonchain": {
      "command": "npx",
      "args": ["-y", "@fileonchain/mcp"],
      "env": { "FILEONCHAIN_API_KEY": "${FILEONCHAIN_API_KEY}" }
    }
  }
}

Or with the Claude Code CLI:

terminal
claude mcp add fileonchain -- npx -y @fileonchain/mcp

FILEONCHAIN_API_KEY is required only for the anchoring tools; FILEONCHAIN_API_URL optionally overrides the API origin.

Host an envelope at any public URL and two links follow from it. The report page runs every check in the reader's browser; the badge is an SVG whose colour and text are the offline verifier's status for the envelope at that URL — valid, valid, warnings, incomplete, or invalid — and grey unreachable / unknown when it could not run. Neither ever says “verified”.

links
# Report page — the reader's browser fetches the envelope and runs every check locally
https://fileonchain.org/verify?url=https://example.com/releases/v1.2.0/evidence.json

# Status badge — re-runs the offline verifier on the envelope at that URL each time it is fetched
https://fileonchain.org/api/badge?url=https://example.com/releases/v1.2.0/evidence.json

The Markdown that drops the badge into a README or release note, with the report page as its click-through (the /verify page offers it under any report loaded from a URL). A /verify?url= link also carries a social card painted from the same result: status stamp, receipts, settlement systems, and artifact vs envelope signer counts.

README.md
[![FileOnChain evidence: valid](https://fileonchain.org/api/badge?url=https%3A%2F%2Fexample.com%2Freleases%2Fv1.2.0%2Fevidence.json)](https://fileonchain.org/verify?url=https%3A%2F%2Fexample.com%2Freleases%2Fv1.2.0%2Fevidence.json)

Seal a GitHub Release. A starter composite action hashes the release artifacts into a manifest, seals the manifest with the reference SDK (@fileonchain/sdk/evidence, no new protocol behaviour), verifies it, attaches fileonchain-evidence.json and the manifest to the release, and appends the badge to the notes. Without a signing key the envelope proves integrity and time only; with an ed25519 seed the workflow signs both the artifact and the envelope. Status: starter — it runs on this repository's own releases and installs the SDK from the monorepo because the packages are not on npm yet.

.github/workflows/release.yml
on:
  release:
    types: [published]

permissions:
  contents: write

jobs:
  seal:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # …build your artifacts…
      - uses: FileOnchain/fileonchain-org/.github/actions/seal-release@main
        with:
          files: |
            dist/*.tgz
          signing-key: ${{ secrets.FILEONCHAIN_SIGNING_KEY }}   # optional ed25519 seed
          api-key: ${{ secrets.FILEONCHAIN_API_KEY }}           # optional: store in FileOnChain Cloud

Shared concepts

One payload vocabulary, twelve runtimes

Every family writes the same versioned JSON payloads ({ p: "fileonchain", v: 1, op: "anchor" | "chunk", … }) — whether they land in a contract call, a remark, a memo, or a consensus message. Any indexer can read anchors back with parseAnchorPayload, regardless of chain.

One receipt shape

Every anchorChunkedFile resolves to the same ChunkedAnchorReceipt — txHashes for every transaction sent and txHash for the file-level anchor — and reports the same AnchorProgress stages, so one progress UI covers all twelve families.

On-chain storage

Every anchorChunkedFile accepts includeData — chunk bytes ride inside the anchor payloads (base64 d field), turning the chain into the file's home. Chunk sizes must fit each chain's per-transaction budget — getChunkDataBudget(chain) / isStorageCapable(chain). Anchors on other chains point at the stored copy via a fileonchain://<chainId>/<cid> storage URI (buildStorageUri).

Provisioning

A chain is "provisioned" when its registry entry carries a deployed contract, module, program, or topic. Anchoring on an unprovisioned chain throws ChainNotProvisionedError; check first with isChainProvisioned(chain).

provisioning.ts
import { getChain, isChainProvisioned, ChainNotProvisionedError } from "@fileonchain/sdk";

const chain = getChain("aptos:mainnet")!;
if (!isChainProvisioned(chain)) {
  // anchorCID / anchorChunkedFile would throw ChainNotProvisionedError here —
  // nothing is deployed on this chain yet, so fall back or pick another chain.
}

Each package ships its own README with the full API surface — browse them on GitHub. Anchoring something? The uploader runs on these exact packages, and the explorer shows what lands onchain. Watching for protocol version or fixture changes? They are called out on the changelog, which also has an RSS feed.