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 ethersThe 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:
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:
mkdir -p ~/.claude/skills/chainkeep
curl -fsSL https://chainkeep.online/skill.md -o ~/.claude/skills/chainkeep/SKILL.mdThe 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.
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.mdstore 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
| Command | What it does |
|---|---|
networks | The 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
| Flag | Meaning |
|---|---|
--chain | Storage network chain ID. Default 42 (LUKSO). |
--public | Store unencrypted. Name and type become public metadata. |
--pay x402 | native | Pay in USDC over x402 (default, no gas) or in the storage chain’s coin. |
--network | x402 payment network, e.g. base. Defaults to the first one the server offers. |
--max-usdc | Pay only if the price is at most this much USDC. |
--yes | Pay whatever the quoted price is. |
--key-out | Also write the recovery key to this file (mode 600). |
--no-wait | Return once the server accepts the upload, before every chunk is written. |
--url | Server URL; overrides CHAINKEEP_URL. |
Environment
| Variable | Meaning |
|---|---|
CHAINKEEP_URL | The ChainKeep server, e.g. https://chainkeep.online. |
CHAINKEEP_PRIVATE_KEY | The 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:
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:
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:
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.ownerAPI 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:
| Step | Detail |
|---|---|
prepare | Encrypting and hashing the file. |
price | The x402 requirement or native quote that will be paid. |
confirm | Native only: the createFile transaction was sent ({ tx }). |
upload | Sending the bytes to the server. |
writing | Relayer progress ({ state, done, total }), while waiting. |
done | The 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.