Agent-friendly EFS write receipts for local development and Sepolia testnet work.
EFS Scribe gives agents a simple HTTP API for writing, reading, and removing public EFS files. The byte API pins supplied bytes to IPFS, writes an EFS record, returns a receipt, and reads bytes back by resolving the receipt, fetching the IPFS mirror, and verifying the payload hash. It also exposes the lower-level EFS plan/receipt endpoints for agents that want attestation detail.
It ships two modes: deterministic offline receipts for local development, and
configured sepolia writes/removals that submit real EAS transactions.
npm install
npm run devThe local default API key is local-scribe-key. The default local key is
delete-enabled; public hosted keys should usually omit allow_delete.
Set IPFS_API_URL to a Kubo-compatible API such as
http://127.0.0.1:5001/api/v0 when you want inline bytes pinned to IPFS by
default. EFS Scribe treats IPFS pinning as best-effort infrastructure: it
retries transient failures, limits service-side IPFS operations per
authenticated actor, rate-limits file writes, and keeps concurrent IPFS adds
low.
curl http://localhost:3000/health
curl http://localhost:3000/v1/capabilitiesFor most agents, write bytes to a path:
curl -X PUT 'http://localhost:3000/v1/files?path=%2Fagents%2Fdemo%2Fstatus.json' \
-H 'authorization: Bearer local-scribe-key' \
-H 'content-type: application/json' \
-H 'idempotency-key: demo-status-bytes-001' \
-H 'x-nanda-agent: agent:demo' \
--data-binary '{"ok":true}'Then read the bytes back:
curl 'http://localhost:3000/v1/files?path=%2Fagents%2Fdemo%2Fstatus.json'PUT /v1/files?path=... returns a compact JSON response with the receipt ID,
payload hash, receipt, and links. GET /v1/files?path=... returns verified raw
bytes. Byte writes default to IPFS-backed storage; if IPFS is unavailable, the
write fails instead of silently creating a non-retrievable record.
Preview the EFS plan for a write:
curl -X POST http://localhost:3000/v1/files/plan \
-H 'content-type: application/json' \
-H 'authorization: Bearer local-scribe-key' \
-d '{
"path": "/agents/demo/status.json",
"content": {
"mode": "inline_base64",
"content_base64": "eyJvayI6dHJ1ZX0=",
"content_type": "application/json"
},
"properties": { "name": "status.json" },
"agent": { "claimed_nanda_id": "agent:demo" },
"options": { "idempotency_key": "demo-status-001" }
}'The plan includes a preflight list naming Sepolia facts the chain writer
resolves first, such as rootAnchorUID, existing path anchors, and
/transports/<name> anchors. DATA itself is the EFS empty identity attestation,
with contentHash, contentType, and size bound as PROPERTYs. When
IPFS_API_URL is configured, inline_base64 input is pinned to IPFS and the
EFS record gets an ipfs://... MIRROR. Without IPFS pinning, inline bytes are
used only to compute content facts and are not retrievable from the EFS record.
Write a small JSON file in offline mode:
curl -X POST http://localhost:3000/v1/files \
-H 'content-type: application/json' \
-H 'authorization: Bearer local-scribe-key' \
-d '{
"path": "/agents/demo/status.json",
"content": {
"mode": "inline_base64",
"content_base64": "eyJvayI6dHJ1ZX0=",
"content_type": "application/json"
},
"properties": { "name": "status.json" },
"agent": { "claimed_nanda_id": "agent:demo" },
"options": { "idempotency_key": "demo-status-001" }
}'Then send the returned receipt to POST /v1/verify.
Remove the file from the authenticated agent lens:
curl -X POST http://localhost:3000/v1/files/delete \
-H 'content-type: application/json' \
-H 'authorization: Bearer local-scribe-key' \
-d '{
"path": "/agents/demo/status.json",
"agent": { "claimed_nanda_id": "agent:demo" },
"options": { "idempotency_key": "demo-status-delete-001" }
}'On Sepolia, removal revokes the caller's active placement PIN for that path. It does not erase earlier EAS attestations, chain history, mirrors, or IPFS pins.
For POST /v1/files/plan, IPFS is asked to calculate the CID without pinning.
For PUT /v1/files?path=... and POST /v1/files, inline bytes are pinned
before the EFS write.
To preview without storing a receipt, use POST /v1/files/plan.
Fetch the receipt again:
curl http://localhost:3000/v1/receipts/<receipt_id>Resolve the latest stored receipt for a path:
curl 'http://localhost:3000/v1/resolve?path=%2Fagents%2Fdemo%2Fstatus.json'Receipts and resolve responses include mirror metadata such as
{ "transport": "ipfs", "uri": "ipfs://..." } when retrievable bytes are
declared.
Byte reads are currently backed by the in-memory receipt index. If the service
restarts, keep the original receipt and mirror URI; the current MVP does not
yet include a Sepolia indexer that can rebuild /v1/files?path=... reads from
chain history alone.
offline: deterministic mock EFS receipts. No network or wallet required.sepolia: real Sepolia EFS writes/removals through EASmultiAttestandmultiRevoke. The derived per-agent wallet signs as the EAS attester, and the sponsor key can top that wallet up with Sepolia ETH.
Both modes use the same request and receipt shape.
Sepolia mode requires:
EFS_SCRIBE_MODE=sepolia
API_KEYS_JSON={"your-api-key":"api-key:your-agent"}
AGENT_KEY_DERIVATION_SECRET=<random deployment secret>
PUBLIC_BASE_URL=https://your-public-service.example
SEPOLIA_RPC_URL=<rpc url>
SERVICE_SPONSOR_PRIVATE_KEY=<private key with Sepolia ETH>
IPFS_API_URL=<kubo api url, recommended for retrievable files>
# Optional, only if the IPFS API is protected by a reverse proxy.
IPFS_API_AUTHORIZATION=<literal Authorization header value>Use an object grant when a key should be allowed to delete files:
API_KEYS_JSON={"your-private-key":{"subject":"api-key:your-agent","allow_delete":true}}String grants, such as {"public-key":"api-key:public-agent"}, can plan and
write but cannot delete. This is the safer shape for shared hackathon keys.
On Railway, set PUBLIC_BASE_URL to the public Railway domain so receipt links
point back to the hosted service instead of localhost.
If derived agent wallets are funded another way, set
SEPOLIA_AGENT_FUNDING_TARGET_WEI=0 and omit SERVICE_SPONSOR_PRIVATE_KEY.
The default automatic top-up target is 0.05 Sepolia ETH per derived agent wallet.
Positive non-zero targets below 0.05 Sepolia ETH are rounded up to that floor.
The sample local-scribe-key is rejected in Sepolia mode.
For monitoring, GET /health is a cheap liveness check and
GET /health/deep verifies Sepolia configuration, RPC chain id, the EFS root
anchor, and sponsor wallet balance. The deep health check returns 503 when
degraded and emits an efs_scribe.alert log. The default sponsor low-balance
threshold is 0.1 Sepolia ETH; override it with
SEPOLIA_SPONSOR_LOW_BALANCE_WEI.
Keep Railway pointed at /health for process liveness. Add a separate uptime
monitor for /health/deep and alert after a few consecutive failures or if the
JSON body does not report "mode": "sepolia" in production. Search Railway logs
for efs_scribe.alert to see failed check names and details such as
sepolia_sponsor_balance, balance, and threshold.
This repo also includes a scheduled GitHub Actions monitor:
.github/workflows/deep-health.yml. It runs npm run health:check against the
production /health/deep endpoint every 15 minutes and fails the workflow when
the service is degraded or not running in Sepolia mode. Enable GitHub Actions
email notifications on the repo/account to receive those failures by email.
Receipt lookup is currently memory-only. On-chain writes remain on Sepolia, but
GET /v1/receipts/:id and GET /v1/resolve only know receipts created since
the current service process started.
POST /v1/verify checks receipt shape and self-consistency. It does not yet
re-query Sepolia or act as an independent EAS indexer.
agent.claimed_nanda_id is a label supplied by the caller. The API key subject
is what controls the derived EFS attester lens.
- Inline request bodies are limited to 10 MB decoded bytes.
- Raw byte writes are limited to 10 MB.
- With
IPFS_API_URLconfigured, inline bytes default to anipfs://mirror. - Byte reads require an IPFS mirror and a configured or derived IPFS gateway.
- Service-side IPFS operations use a 10-request burst with a 5-request/minute refill per authenticated actor.
- File writes and removals also use a 10-request burst with a 5-request/minute refill per authenticated actor.
- Scribe runs at most 2 concurrent IPFS adds per service process.
- Each service-side IPFS operation may retry transient upstream failures up to
3 total attempts. Upstream
429responses are not retried. - Use
options.storage = "metadata_only"to skip service-side IPFS pinning. - Use
options.storage = "ipfs"to require service-side IPFS pinning and fail if it is not configured. - Use
hash_onlywhen you only want an EFS commitment to a hash. Hash-only is cheaper but does not make bytes retrievable. - Larger files should be pinned elsewhere and submitted as
external_mirror_onlywith one or more mirrors. - A request can include up to 8 mirrors and 32 properties.
Do not write secrets, private keys, personal data, or confidential URLs. EFS records are public attestations about file identity, retrieval, and metadata, not private storage.
npm test
npm run typecheck
npm run buildMIT