ChainKeepBeta

Developers

Store files onchain from code

The chainkeep npm package stores files permanently onchain from a script, a server or an AI agent. Pay over x402 in USDC (two signatures, no gas) or in the storage chain’s native coin. Files are encrypted on your machine by default and verified byte for byte when read back.

Install

Node 20 or later. The library also runs in Deno, Bun and the browser: anywhere with fetch, Web Crypto and an ethers signer.

npm install chainkeep ethers

The CLI needs no install: npx -y chainkeep runs it. Releases are listed on npm.

Agent skill

Giving an AI agent ChainKeep needs no code. /skill.md is a skill file: it tells the agent when to use ChainKeep, how to store and read files with the CLI or plain HTTP, and the rules it must follow, such as asking before paying and never logging the recovery key.

Paste this into any agent that can read the web:

Prompt
Read https://chainkeep.online/skill.md and store this file permanently with ChainKeep.

Or install it as a skill, so the agent picks it up whenever a user asks to back up or permanently keep a file. For Claude Code:

Shell
mkdir -p ~/.claude/skills/chainkeep
curl -fsSL https://chainkeep.online/skill.md -o ~/.claude/skills/chainkeep/SKILL.md

The agent then runs the CLI with its own wallet, set in CHAINKEEP_PRIVATE_KEY. Give it a dedicated wallet holding only what it needs.

CLI

Set the server and a wallet, then quote, store and read. Output is JSON on stdout, progress on stderr, so it pipes cleanly into another program.

Shell
export CHAINKEEP_URL=https://chainkeep.online
export CHAINKEEP_PRIVATE_KEY=0x…   # a dedicated agent wallet
npx -y chainkeep quote notes.md
npx -y chainkeep store notes.md --max-usdc 0.50
npx -y chainkeep get 42 0x<fileId> --key <recovery key> --out notes.md

store never pays without --yes or a --max-usdc ceiling. Without either, it prints the price and exits with code 2, so an agent can ask its user first.

Commands

CommandWhat it does
networksThe networks this server stores on, with their vault addresses.
quote <file> [--chain 42]The price in USDC (x402) and in the native coin. Pays nothing.
store <file> [options]Encrypts (unless --public), pays and stores the file. Needs --yes or --max-usdc.
get <chainId> <fileId> [--key K | --key-file f] [--out path]Reads, verifies and decrypts a file to disk.
list [--chain 42] [--owner 0x…]Files owned by the agent wallet, or by --owner.

Store options

FlagMeaning
--chainStorage network chain ID. Default 42 (LUKSO).
--publicStore unencrypted. Name and type become public metadata.
--pay x402 | nativePay in USDC over x402 (default, no gas) or in the storage chain’s coin.
--networkx402 payment network, e.g. base. Defaults to the first one the server offers.
--max-usdcPay only if the price is at most this much USDC.
--yesPay whatever the quoted price is.
--key-outAlso write the recovery key to this file (mode 600).
--no-waitReturn once the server accepts the upload, before every chunk is written.
--urlServer URL; overrides CHAINKEEP_URL.

Environment

VariableMeaning
CHAINKEEP_URLThe ChainKeep server, e.g. https://chainkeep.online.
CHAINKEEP_PRIVATE_KEYThe agent’s own wallet. x402 needs USDC on the payment network; native payment needs the storage chain’s coin.

Library

Every function takes one options object. Pay in USDC over x402, with no gas and no provider:

JavaScript
import { Wallet } from 'ethers'
import { readFile } from 'node:fs/promises'
import { storeWithX402 } from 'chainkeep'

const stored = await storeWithX402({
  baseUrl: 'https://chainkeep.online',
  signer: new Wallet(process.env.CHAINKEEP_PRIVATE_KEY),
  bytes: new Uint8Array(await readFile('memory.json')),
  name: 'memory.json',
  type: 'application/json',
  chainId: 42,
  maxUsdc: 500_000n, // 0.50 USDC, in atomic units (6 decimals)
})
// stored.fileId, stored.key (shown once: hand it to your user)

Or in the storage chain’s native coin, with the signer connected to that chain:

JavaScript
import { JsonRpcProvider, Wallet } from 'ethers'
import { storeWithNative } from 'chainkeep'

const lukso = new JsonRpcProvider('https://rpc.mainnet.lukso.network')
const stored = await storeWithNative({
  baseUrl: 'https://chainkeep.online',
  signer: new Wallet(process.env.CHAINKEEP_PRIVATE_KEY, lukso),
  chainId: 42,
  bytes,
  name: 'memory.json',
})

Read a file back, verified against the chain and decrypted:

JavaScript
import { JsonRpcProvider } from 'ethers'
import { readStored } from 'chainkeep'

const file = await readStored({
  baseUrl: 'https://chainkeep.online',
  provider: new JsonRpcProvider('https://rpc.mainnet.lukso.network'),
  chainId: 42,
  fileId: '0x…',
  key: recoveryKey, // omit for public files
})
// file.bytes, file.name, file.type, file.encrypted, file.owner

API reference

Exports of chainkeep. Every network call accepts an optional fetch to swap in your own.

storeWithX402(options)

Stores a file, paying over x402. The signer signs two messages and needs USDC on the payment network, no gas.

Options baseUrl, signer, bytes, name, chainId, type?, encrypted? (true), network?, maxUsdc? (bigint, atomic USDC), wait? (true), fetch?, onStep?

storeWithNative(options)

Stores a file, paying in the storage chain’s native coin. The signer must be connected to that chain and hold the quoted amount plus gas for one transaction.

Options baseUrl, signer, bytes, name, chainId, type?, encrypted? (true), wait? (true), fetch?, onStep?

readStored(options)

Reads a file back from the chain, verified against its onchain Merkle root and SHA-256, and decrypts it with key. Falls back to the server’s mirror, held to the same commitments.

Options baseUrl, provider, chainId, fileId, key?, fetch?

quote(options)

The price of storing size bytes, without paying: { usdc: [x402 requirement], native: { amount, relayer, token } | null }.

Options baseUrl, chainId, size, fetch?

storageNetworks(options)

The networks the server uploads to: [{ chainId, vault, relayer, feeBps, minFee }].

Options baseUrl, fetch?

prepareForStorage(options)

The exact bytes that would be stored, encrypted with a fresh recovery key unless encrypted is false. Useful for a quote on the encrypted size.

Options bytes, name, chainId, type?, encrypted? (true)

waitForUpload(options)

Polls an upload until the relayer has written every chunk. Throws if the upload fails.

Options baseUrl, chainId, fileId, onProgress?, fetch?

x402PaymentHeader(options)

The X-PAYMENT header for an x402 “exact” requirement: a signed EIP-3009 USDC authorization.

Options signer, requirement

Results and progress

Both store functions resolve to { chainId, fileId, owner, key, encrypted, size, paid }; x402 adds settlement, the payment transaction. key is the base64 recovery key, or null for a public file. The file’s page is /file/<chainId>/<fileId>.

Pass onStep(step, detail) to follow along:

StepDetail
prepareEncrypting and hashing the file.
priceThe x402 requirement or native quote that will be paid.
confirmNative only: the createFile transaction was sent ({ tx }).
uploadSending the bytes to the server.
writingRelayer progress ({ state, done, total }), while waiting.
doneThe final result.

Rules

  • Get approval before paying. Show the user the price and never pay more than they approved.
  • Encrypt by default. Public files are readable by anyone, forever.
  • The recovery key is shown once. Hand it to the user; never log it. Without it an encrypted file can never be opened.
  • Use a dedicated wallet holding only what it needs. Never ask a user for their private key.
  • Nothing can be deleted. Don’t store anything that may need removing later.

HTTP API

Working in another language? The full recipe, from the 402 price response to the EIP-712 file request, the chunk format, the Merkle root and the encryption envelope, is in the agent guide.

Read /skill.md Or store a file in the browser