IPFS pinning & retrieval service with x402 payments. TypeScript, Hono, Kubo IPFS, SQLite.
src/
index.ts # Entry point: wiring, config, process lifecycle
app.ts # Hono routes, middleware, request validation, A2A agent card
config.ts # Env-driven config with production validation
db.ts # SQLite setup (better-sqlite3)
types.ts # Shared types (Pin, PinStatus, etc.)
lib/errors.ts # Typed error classes (ValidationError, NotFoundError, etc.)
services/
pinning-service.ts # Core business logic: pin CRUD + upload + replication
ipfs-rpc-client.ts # Kubo RPC client (pin/add/cat/unpin)
x402.ts # x402 middleware, wallet extraction, price calculation
rate-limiter.ts # Per-wallet/IP rate limiting
content-cache.ts # In-memory LRU cache for gateway responses
content-type.ts # MIME type detection
logger.ts # Pino structured logging
repositories/
pin-repository.ts # SQLite persistence, query filtering, owner isolation
tests/
unit/ # Unit tests
integration/ # Integration tests
docs/
railway-deployment.md # Railway deployment runbook
deployment-smoke.md # x402 smoke test runbook
scripts/
backup-db.sh # SQLite backup script
- Runtime: Node.js 20+, TypeScript, pnpm
- Framework: Hono (
@hono/node-server) - IPFS: Kubo via HTTP RPC
- Database: SQLite via
better-sqlite3 - Payments: x402 protocol (
@x402/core,@x402/evm,@x402/hono), USDC on Taiko Alethia - Testing: Vitest
- Logging: Pino
- IPFS Pinning Service API spec — all pin endpoints conform to this
- x402 protocol — HTTP 402 machine-readable payment flow
- A2A (Agent-to-Agent) — agent card at
/.well-known/agent.json - EIP-3009 (
transferWithAuthorization) — used for x402 USDC settlement on Taiko
- Production: Railway (API + Kubo as separate services, persistent volumes)
- Config: Entirely env-driven. See
.env.example - Docker:
docker compose up --buildfor local full stack - Build:
pnpm buildcompiles todist/,node dist/index.jsto run
- Wallet = identity: There are no user accounts. The wallet that pays via x402 owns the pin. Owner endpoints enforce wallet isolation at the repository level.
- Production startup validation:
config.tsrejects placeholder EVM addresses and requiresX402_ENABLED=truewhenNODE_ENV=production. The app will crash on boot if misconfigured. - Price is dynamic:
x402.tscalculates price asbase + (size_mb * per_mb), capped atmax. The 402 response includes the exact amount. - Replication is best-effort: Primary pin must succeed. Replica failures are recorded in
PinStatus.info.replicationbut don't fail the request. - Gateway is optional paywall: Content retrieval is free by default; owners can set
meta.retrievalPriceto gate content behind x402. - SQLite single-writer: Only 1 API replica in production. No WAL mode tricks — just one process.
- Rate limiter is in-memory: Resets on restart. Keyed by wallet address (authenticated) or IP (unauthenticated).