On-chain storytelling protocol with a local-first AI writing assistant. Writers collaborate with Claude to brainstorm, outline, and write fiction stories, then publish them on plotlink.xyz where every storyline becomes a tradable token.
See AGENTS.md for the full writing workflow. Quick summary:
- Start
claudein this directory - Say "let's write a story" — brainstorm genre, tone, characters
- Claude creates files in
stories/your-story-name/ - Review, iterate, refine with Claude
- Publish via the OWS app when ready
Stories follow this structure:
stories/{story-name}/
.story.json # Content type metadata (fiction | cartoon)
structure.md # Outline, characters, arc
genesis.md # Synopsis hook (~1000 chars)
plot-01.md # Chapter 1 (max 10K chars)
...
See stories/_example/ for a complete reference.
- Web App: Next.js 16 (App Router), TypeScript, Tailwind CSS v4, Supabase
- Local Writer App: Hono + React 19 + Vite, SQLite + Prisma, OWS wallet
- Storage: Filebase (IPFS)
- Chain: Base (L2)
The local writer app runs on http://localhost:7777 (configurable via APP_PORT). All endpoints except auth use Authorization: Bearer {token} headers.
The OWS passphrase is stored in plaintext in ~/.plotlink-ows/.env as OWS_PASSPHRASE. It is used to decrypt and sign with the OWS wallet. For login verification, the passphrase is hashed with HMAC-SHA256 and compared against the stored hash in the database.
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/auth/status |
GET | No | Check if passphrase is configured |
/api/auth/setup |
POST | No | First-run passphrase setup (≥4 chars) → returns { token } |
/api/auth/login |
POST | No | Login with passphrase → returns { token } (24h TTL) |
/api/auth/verify |
GET | Bearer | Check token validity |
/api/auth/reset-passphrase |
POST | Bearer | Update passphrase |
| Endpoint | Method | Purpose |
|---|---|---|
/api/publish/preflight |
GET | Check wallet balance vs. creation fee (uploads go through the PlotLink API) |
/api/publish/file |
POST | Publish story on-chain (SSE stream of progress events) |
/api/publish/retry-index |
POST | Retry indexing for a published file |
/api/publish/upload-cover |
POST | Upload cover image — FormData file field, WebP or JPEG only, max 1MB → returns { cid } |
/api/publish/upload-plot-image |
POST | Upload plot illustration — FormData file field, WebP or JPEG only, max 1MB → returns { cid, url } |
/api/publish/update-storyline |
POST | Update storyline metadata (coverCid, genre, language, isNsfw) |
Publish flow: Upload to IPFS → estimate gas → sign with OWS wallet → broadcast → confirm → index on plotlink.xyz (8s delay + 10 retries × 30s). Genesis files call createStoryline, plot files (plot-*.md) call chainPlot. Content limit: 10K chars.
Cover update workflow:
POST /api/publish/upload-coverwith image file → getcidPOST /api/publish/update-storylinewith{ storylineId, coverCid: cid }→ updates on plotlink.xyz
Metadata update workflow:
POST /api/publish/update-storylinewith{ storylineId, genre?, language?, isNsfw? }
Both upload-cover and update-storyline sign messages with the OWS wallet (message format: PlotLink: Upload cover image\nTimestamp: {ts} and PlotLink: Update storyline #{id}\nTimestamp: {ts}).
| Endpoint | Method | Purpose |
|---|---|---|
/api/stories |
GET | List all stories |
/api/stories/archived |
GET | List archived stories |
/api/stories/archive |
POST | Archive a story { name } |
/api/stories/restore |
POST | Restore archived story { name } |
/api/stories/:name |
GET | Story detail with file contents |
/api/stories/:name/:file |
GET | Single file content and publish status |
/api/stories/:name/:file |
PUT | Update file content { content } |
/api/stories/:name/:file/publish-status |
POST | Record publish result (txHash, storylineId, etc.) |
/api/stories/:name/metadata |
POST | Write story metadata { contentType } |
/api/stories/:name/:file/mark-not-indexed |
POST | Mark file as not indexed { indexError? } |
| Endpoint | Method | Purpose |
|---|---|---|
/api/terminal/spawn |
POST | Spawn Claude CLI session for a story { storyName?, resume? } |
/api/terminal/session/:storyName |
GET | Get stored session ID for a story |
/api/terminal/status |
GET | List all active terminal sessions |
/api/terminal/rename |
POST | Rename session { oldName, newName } |
/api/terminal/stop |
POST | Kill default PTY (legacy) |
/api/terminal/:storyName |
DELETE | Kill a story's PTY |
/api/terminal/:storyName/discard |
DELETE | Kill PTY and clean metadata |
/ws/terminal |
WebSocket | Live PTY relay ?token={token}&story={name}&resume={bool} |
| Endpoint | Method | Purpose |
|---|---|---|
/api/wallet |
GET | Wallet info and balances (ETH, USDC, PLOT) |
/api/wallet/create |
POST | Create OWS wallet if not exists |
/api/dashboard |
GET | Writer dashboard stats (stories, costs, royalties) |
/api/settings/register-agent |
POST | Register wallet on ERC-8004 |
/api/settings/generate-binding |
POST | Generate wallet binding proof |
/api/settings/link-status |
GET | Check ERC-8004 registration status |
Defined in lib/genres.ts:
Genres (21): Romance, Fantasy, Science Fiction, Mystery, Thriller, Horror, Adventure, Historical Fiction, Contemporary Lit, Humor, Poetry, Non-Fiction, Fanfiction, Short Story, Paranormal, Werewolf, LGBTQ+, New Adult, Teen Fiction, Diverse Lit, Others
Languages (11): English, Chinese, Korean, Japanese, Spanish, French, Hindi, Arabic, Portuguese, Russian, Others
# Next.js web app
npm run dev # Start dev server
npm run build # Production build
npm run lint # ESLint
npm run typecheck # TypeScript type-check
# Local writer app
npm run app:dev # Start local writer (Hono + Vite dev)
npm run app:build # Build frontend
npm run app:start # Serve production build
# CLI
npx plotlink-ows init # Guided setup
npx plotlink-ows # Start appVersion format: X.Y.Z (e.g., 1.0.0, 1.11.23). Each digit can go beyond 9 (e.g., 1.2.15).
| Digit | Meaning | Who can bump |
|---|---|---|
| 3rd (Z) | Minor updates, bug fixes | T3 autonomously |
| 2nd (Y) | Major updates, new features | T3 autonomously |
| 1st (X) | Operator (T1) permission only | Never bump without asking |
When making a PR, bump the 3rd digit for bug fixes, the 2nd digit for feature work. Never bump the 1st digit without explicit T1 approval.
PR CI runs lint-and-typecheck only. Visual regression is manual-only — trigger via gh workflow run update-snapshots.yml when changes may affect visual output.
See .env.example for all required environment variables.