PinataFS is a permissioned on-chain filesystem pattern for EVM chains.
This repo contains:
PermissionNFT(ERC-721 for permission ownership)PinataFS(path -> CID storage + permission checks)pinatafs-sdk(deploy/admin/read/write helpers)front-end-demo(Vite + React + Wagmi demo UI)
By the end of this workshop, participants can:
- Deploy
PermissionNFTandPinataFSto Base Sepolia. - Mint a permission NFT.
- Assign multiple writable prefixes to a token.
- Upload a file to Pinata and write its CID on-chain.
- Read that CID back from a path.
smart_contract- Solidity contracts + Foundry testssdk- TypeScript SDK (pinatafs-sdk)front-end-demo- Demo app
- Node.js
>=18(20+recommended) - pnpm
>=9 - Foundry (
forge) - A funded Base Sepolia wallet
- Pinata JWT + gateway domain (for demo uploads)
Install Foundry if needed:
curl -L https://foundry.paradigm.xyz | bash
foundryupIf your workshop wallet is unfunded, use Coinbase Developer Platform's faucet tools:
Suggested flow:
- Open the CDP portal and go to faucet/funding tools. This should be located under "wallets/faucet".
- Select
Base Sepolia. - Paste your workshop wallet address.
- Request testnet ETH, then confirm the balance in your wallet before deploying.
git clone <your-repo-url>
cd <repo-directory>
pnpm installpnpm build:contracts
pnpm test:contracts
pnpm --filter ./sdk build
pnpm --filter ./front-end-demo buildIf all commands pass, your local environment is ready.
Set deploy env vars in the same terminal session:
export RPC_URL="https://sepolia.base.org"
export PRIVATE_KEY="0x<your_private_key>"
export CHAIN_ID="84532"
export CHAIN_NAME="Base Sepolia"
export CHAIN_CURRENCY_SYMBOL="ETH"Set optional PermissionNFT name/symbol:
export PERMISSION_NFT_NAME="PinataFS Access"
export PERMISSION_NFT_SYMBOL="PFSA"Deploy both contracts:
pnpm deploy:stackYou should see output with:
Permission NFT Address: 0x...Filesystem Address: 0x...
Save both addresses.
You can also deploy separately:
pnpm deploy:permission-nft
pnpm deploy:filesystemCreate local env file:
cp front-end-demo/.env.example front-end-demo/.envUpdate at minimum:
VITE_RPC_URL=https://sepolia.base.org
VITE_CHAIN_ID=84532
VITE_CHAIN_NAME=Base Sepolia
VITE_CHAIN_CURRENCY_SYMBOL=ETH
VITE_BLOCK_EXPLORER_URL=https://sepolia.basescan.org
VITE_FILESYSTEM_ADDRESS=0x<PinataFS_address>
VITE_PERMISSION_NFT_ADDRESS=0x<PermissionNFT_address>
VITE_PINATA_JWT=<your_pinata_jwt>
VITE_PINATA_GATEWAY=<your_gateway>.mypinata.cloudFrom repo root:
pnpm devOpen the local URL printed by Vite.
- Connect wallet in the UI.
- Confirm chain is Base Sepolia.
- In
Admin: Mint Permission NFT, mint a token to your own wallet. - In
Admin: Upsert Prefix Permission, set:- NFT contract = demo contract (or custom)
- token id = minted token id
- prefixes, one per line (example:
/agent1and/shared/data) - click
Sync prefix set(single transaction)
- In
Write File:- choose a path under an allowed prefix (example
/agent1/files/manifest.json) - upload file to Pinata (or manually paste a CID)
- select/provide the NFT contract + token id
- click
Write CID to filesystem
- choose a path under an allowed prefix (example
- In
Read File, read the same path and verify the CID matches.
Write permission is keyed by:
nftContracttokenId
Caller must own ownerOf(tokenId) on that contract.
- Last write wins (
upsertonly). - Stores latest CID by hashed path key.
- Prefix authorization uses strict subtree matching.
- Owner can replace full prefix set in one transaction.
- Owner can revoke/unrevoke writes for a token.
- Owner can transfer ownership to
address(0)to permanently disable admin actions.
- Path must start with
/ - No duplicate slashes
- Cannot end with
/ - Allowed segment chars:
A-Z,a-z,0-9,-,_ .allowed only in final file segment (max one dot)- Prefixes cannot include
. - Strict subtree checks:
/shared/dataallows/shared/data/sub/file1/shared/datadoes not allow/shared/database/file1
import {
mintPermissionNft,
replaceTokenPrefixes,
writeFile,
readFile
} from "pinatafs-sdk";
await mintPermissionNft({
permissionNftAddress,
publicClient,
walletClient,
to: userAddress,
transferable: false
});
await replaceTokenPrefixes({
filesystemAddress,
publicClient,
walletClient,
nftContract: permissionNftAddress,
tokenId: 1n,
prefixes: ["/agent1", "/shared/data"]
});
await writeFile({
filesystemAddress,
publicClient,
walletClient,
nftContract: permissionNftAddress,
tokenId: 1n,
path: "/agent1/files/manifest.json",
cid: "bafy..."
});
const cid = await readFile({
filesystemAddress,
publicClient,
path: "/agent1/files/manifest.json"
});- Cause: stale or wrong artifact/ABI path.
- Fix:
pnpm build:contracts- re-run deploy
- ensure artifact points to
PinataFS.sol/PinataFS.jsonif overridden
- Cause: frontend SDK ABI does not match deployed contract.
- Fix:
- rebuild SDK/frontend
- verify
VITE_FILESYSTEM_ADDRESSpoints to a freshPinataFSdeployment
- Ensure frontend chain env is correct.
- Optionally set:
VITE_MULTICALL3_ADDRESS=0xcA11bde05977b3631167028862bE2a173976CA11VITE_MULTICALL3_BLOCK_CREATED=1059647
- Verify the connected wallet currently owns the token.
- Verify write path is under an allowed prefix.
- Verify token write revocation is not enabled.
- Only
PinataFS.owner()can replace prefixes or revoke token writes.
- Reads are public on public chains.
- CID format is not strictly enforced on-chain.
- Demo frontend exposes Pinata credentials via browser env vars.
- Do not use the demo credential model in production.
- Upsert-only writes (no delete)
- No on-chain
ls/pagination index yet - Chain history/events act as write history
smart_contract/contracts/PermissionNFT.solsmart_contract/contracts/PinataFS.solsmart_contract/test/PinataFS.t.solsdk/src/index.tsfront-end-demo/src/App.tsx