Project: AI-powered Readwise Reader inbox triage and organization
Completed: 2026-02-02
Status: ✅ Fully Functional
This is a remixable Val Town app template that lets users:
- Sync their Readwise Reader library to a local SQLite index
- Triage documents in their inbox (move, tag, annotate)
- Search through 10k+ docs with filters (location, category, tags, date, progress)
- AI-Powered Plans - Generate batch organization plans using Pollinations AI
- Projects - Create collections with automated AI rules
- Image Generation - Generate cover images for projects (optional)
Users remix this val, paste their API tokens, and instantly get the full experience.
- Runtime: Deno (Val Town)
- Router: Hono (HTTP framework)
- Database: SQLite (Val Town
std/sqlite@14-main) - AI Provider: Pollinations (OpenAI-compatible endpoint)
- Frontend: Vanilla JS SPA (dark theme)
readwise-mastra/
├── app/
│ └── http.ts # Main HTTP router (Hono)
├── lib/
│ ├── env.ts # Environment config + validation
│ ├── db.ts # SQLite schema + helpers
│ ├── readwise.ts # Readwise Reader API client
│ ├── sync.ts # Incremental sync runner
│ ├── llm_pollinations.ts # Pollinations wrapper + model discovery
│ └── agent.ts # AI plan generation + application
├── jobs/
│ └── sync/
│ └── interval.ts # 15-min sync + daily model refresh
├── web/
│ ├── index.html # SPA entry point
│ ├── app.js # Frontend JS
│ └── styles.css # Dark theme styles
├── plan.md # Original build spec
├── grounding.md # Pollinations API docs
├── keyupdates.md # Update plan details
├── tracker.md # Implementation progress tracker
└── IMPLEMENTATION_RECAP.md # This file
docs- Readwise documents (id, title, author, site_name, url, category, location, reading_progress, notes, summary, tags, etc.)tags- Tag definitions (key, name, type)doc_tags- Junction table for document-tag relationshipssync_state- Sync watermark + status (single row)
views- Saved search viewsprojects- Collections with AI rulesai_plans- Generated organization plansai_jobs- Plan execution logs
model_cache- Discovered Pollinations models (name, type, tools, reasoning, context_window, pricing)model_config- Selected primary/fallback models + last discovery timestamp
docs(location, updated_at)- Inbox sortingdocs(category),docs(site_name)- Filtersdoc_tags(tag_key)- Tag viewsdocs(saved_at)- Date sorting
GET /health- Health check (no auth)POST /api/sync/run- Trigger sync nowPOST /api/sync/reset- Reset sync state (full resync)GET /api/stats- Document counts + sync statusGET /api/auth/check- Validate Readwise token
GET /api/inbox/queue?location=new&limit=50- List inbox itemsPOST /api/docs/:id/triage- Move/tag/annotate documentGET /api/docs/:id- Get single document
GET /api/search?q=&tag=&category=&location=&from=&to=&progressMin=&progressMax=- Full searchPOST /api/views- Create saved viewGET /api/views- List saved viewsDELETE /api/views/:id- Delete view
POST /api/projects- Create projectGET /api/projects- List projectsPOST /api/projects/:id/apply- Run AI rule, return plan
POST /api/ai/plan- Generate organization planPOST /api/ai/apply- Execute planGET /api/ai/plans- List recent plansGET /api/ai/plans/:id- Get plan detailsGET /api/ai/models(NEW) - List cached models + configPOST /api/ai/discover(NEW) - Trigger model rediscovery
POST /api/image/generate- Generate image from prompt (stored in blob)GET /api/image/models- List available image models
POST /webhooks/readwise- Receive Readwise webhooks (triggers sync)
GET /web/:file- Serve SPA assets (index.html, app.js, styles.css)GET /*- SPA fallback (serves index.html)
READWISE_TOKEN=your_readwise_token APP_ADMIN_SECRET=your_secret_for_api_auth POLLINATIONS_TOKEN=your_pollinations_sk_token
POLLINATIONS_BASE_URL=https://gen.pollinations.ai POLLINATIONS_CHAT_PATH=/v1/chat/completions POLLINATIONS_PRIMARY_MODEL= # leave empty for auto-discovery READWISE_WEBHOOK_SECRET= # for webhook verification
Old (wrong): Used legacy endpoint https://text.pollinations.ai/openai with hardcoded "openai" model
New (correct):
- Canonical endpoint:
https://gen.pollinations.ai/v1/chat/completions - Model discovery: Fetches available models from
/v1/modelsand/text/models - Smart fallbacks: Auto-retries with fallback models on error
- Cache: Stores discovered models in SQLite for 24 hours
- Task-based selection: Uses cheap models for simple tasks, stronger models for complex planning
| Task Type | Model Used | Why |
|---|---|---|
| Simple triage | gemini | Cheap, fast, tools=true |
| Complex planning | gemini-large | Better reasoning, quality |
| Search/web features | gemini-search | Has google_search enabled |
| Fallback | openai → gemini-large | Broad compatibility |
- Auto-discovery: Runs daily via interval job (or manually via
/api/ai/discover) - Fallback on errors: If
geminifails, triesopenai, thengemini-large - Response metadata: Returns
{ text, modelUsed, fallbacksAttempted } - Image generation:
generateImage()stores images in Val Town blob storage
- Gemini code_execution: Can generate images/plots inline
- Gemini google_search: Can search the web (if using
gemini-search) - Content blocks: Handles
content_blocksin responses (text, image_url, thinking)
- Uses
updatedAfterwatermark to fetch only changed documents - Stores watermark in
sync_statetable - Runs automatically every 15 minutes via
jobs/sync/interval.ts - Manual trigger:
POST /api/sync/run
- Documents (title, author, url, category, location, progress, notes, summary, etc.)
- Tags (as key-value pairs)
- Document-tag relationships
- Get watermark from
sync_state.updated_after - Call Readwise
/api/v3/list/?updatedAfter=<watermark> - Paginate through all results (handles rate limits)
- Upsert each document + tags to SQLite
- Update watermark to newest
updated_at
- User provides a goal (e.g., "Archive all articles older than 30 days with 0% progress")
- Scope (optional): location, category, tag, limit
- Fetches matching docs from SQLite
- Sends to Pollinations with
complexTask: true(usesgemini-large) - Returns structured JSON plan:
{ "items": [ { "docId": "abc123", "patch": { "location": "archive", "tagsAdd": ["read"] }, "rationale": "Not started, over 30 days old" } ] }
- User reviews plan in UI
- User clicks "Apply" → executes patches via Readwise API
{
items: Array<{
docId: string;
patch: {
location?: "new" | "later" | "shortlist" | "archive" | "feed";
tagsAdd?: string[];
tagsRemove?: string[];
notes?: string;
summary?: string;
title?: string;
};
rationale: string;
}>
}
- Respects Readwise
Retry-Afterheader on 429 errors - Auto-retries with exponential backoff
- 200ms delay between patch applications
- Pure Vanilla JS (no framework)
- Dark theme (custom CSS)
- State management: Simple in-memory state object
- Auth: Bearer token stored in
localStorage
- Inbox - List by location (new, later, shortlist, archive, feed)
- Search - Full-text + filters
- AI Triage - Generate and apply plans
- Projects - Collections with AI rules
- Settings - Sync status, reset, logout
- Batch triage: Select multiple docs, move all at once
- Real-time stats: Document counts by location/category
- Toast notifications: Success/error feedback
- Modal dialogs: Create projects, review plans
- Responsive: Works on desktop and mobile
Runs every 15 minutes
- Readwise sync: Fetch new/updated documents
- Model discovery: Refresh model cache (once per 24 hours)
Next run: Check Val Town UI for interval status
- No authentication UI - Users must know their
APP_ADMIN_SECRET - No user management - Single-user per deployment
- Basic search - No full-text search index (SQLite LIKE only)
- No document content - Only metadata (Readwise API limitation)
- Blob storage URLs - Generated image URLs are Val Town-specific
- Better auth - OAuth or magic links
- Full-text search - Use SQLite FTS5
- Webhook processing - Handle specific events (tags_updated, etc.)
- Batch operations - Bulk move/tag in UI
- Saved filters - Reusable search queries
- Project dashboards - Stats per project
- Export - Download filtered docs as CSV/JSON
- Multi-provider AI - Support OpenAI, Anthropic, etc.
https://kamenxrider--2325eac8ffd511f0b0a842dde27851f2.web.val.run
-
Set environment variables in Val Town:
READWISE_TOKENAPP_ADMIN_SECRETPOLLINATIONS_TOKEN
-
Trigger initial sync:
curl -X POST https://your-val.web.val.run/api/sync/run \ -H "Authorization: Bearer YOUR_ADMIN_SECRET" -
Trigger model discovery:
curl -X POST https://your-val.web.val.run/api/ai/discover \ -H "Authorization: Bearer YOUR_ADMIN_SECRET" -
Open the UI and sign in with
APP_ADMIN_SECRET
- Sync status:
GET /api/statsshows last sync time + error - Model status:
GET /api/ai/modelsshows cached models + last discovery - Logs: Check Val Town execution logs for errors
- Sync fails: Check
READWISE_TOKENis valid - AI fails: Check
POLLINATIONS_TOKENis valid (sk_...) - Model errors: Run
POST /api/ai/discoverto refresh cache - UI won't load: Check
APP_ADMIN_SECRETin localStorage
- Health check:
GET /healthreturns{"ok":true} - Sync:
POST /api/sync/runstarts sync - Stats:
GET /api/statsreturns doc counts - Inbox:
GET /api/inbox/queue?location=newlists docs - UI: Open in browser, sign in with admin secret
- Model discovery:
POST /api/ai/discovercaches models - Model list:
GET /api/ai/modelsshows cached models - Plan generation:
POST /api/ai/planwith goal returns plan - Plan application:
POST /api/ai/applywith planId executes patches - Image generation:
POST /api/image/generatewith prompt returns blob URL
- Check interval settings: 15 minutes (900000ms)
- Check logs: Should run sync + model discovery
- Verify model discovery only runs once per day
app/http.ts- All HTTP routes (start here)jobs/sync/interval.ts- Background jobs
lib/sync.ts- Readwise sync algorithmlib/agent.ts- AI plan generation + applicationlib/llm_pollinations.ts- Pollinations API wrapper + discoverylib/readwise.ts- Readwise API client
lib/db.ts- SQLite schema + helperslib/env.ts- Environment config
web/app.js- SPA state + API callsweb/index.html- HTML entry pointweb/styles.css- Dark theme
plan.md- Original build specificationgrounding.md- Pollinations API documentationkeyupdates.md- Pollinations integration plantracker.md- Implementation progress (all phases complete)IMPLEMENTATION_RECAP.md- This file
✅ All planned features implemented
- Schema + sync ✓
- Inbox triage ✓
- AI plan generation ✓
- Image generation ✓
- Model discovery + fallbacks ✓
- SPA UI ✓
✅ Pollinations integration complete
- Correct canonical endpoint ✓
- Model discovery + caching ✓
- Smart fallback logic ✓
- Task-based model selection ✓
- Image generation with blob storage ✓
✅ Production ready
- Error handling ✓
- Rate limit handling ✓
- Authentication ✓
- Interval jobs ✓
- Logging ✓
- All code is commented - Read inline comments for context
- tracker.md shows progress - All 8 phases complete
- Environment variables are documented - See "Environment Variables" section above
- API is RESTful - Standard JSON responses
- Frontend is vanilla JS - Easy to understand/modify
- Check Val Town execution logs for errors
- Use
GET /api/statsto verify sync is working - Use
GET /api/ai/modelsto verify model discovery - Use
POST /api/ai/discoverto force model refresh - Check SQLite tables directly via Val Town's sqlite tool
- Add new endpoints: Edit
app/http.ts - Add new tables: Edit
lib/db.tsrunMigrations() - Add new AI features: Edit
lib/agent.ts - Change model logic: Edit
lib/llm_pollinations.ts - Update UI: Edit
web/app.jsandweb/styles.css
- Val Town Docs: https://docs.val.town
- Readwise Reader API: https://readwise.io/reader_api
- Pollinations API: https://github.com/pollinations/pollinations/blob/main/APIDOCS.md
- Val: https://www.val.town/x/kamenxrider/readwise-mastra
End of Implementation Recap
Last updated: 2026-02-02