Receipt tracker with no friction: receipts arrive by email and file themselves
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent β or use 1-click editor setup below.
π‘ Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
Expense is a receipt tracker with no friction: receipts arrive by email and file themselves, mileage prices itself from IRS rates, and an AI assistant does the data entry you'd rather skip β capture, log, and ask, all through MCP.


What it does:
Stack:
Auth & accounts (the recent work):
Login is email/password (scrypt-hashed) β the email is the login name, stored lowercase and unique, format-validated at signup/join. Every expense, report, category, and setting belongs to an account; everyone in an account shares them, and accounts are fully isolated from each other.
APP_EMAIL/APP_PASSWORD
when the database is empty; pre-email accounts get their login backfilled
from APP_EMAIL on first start (initStore).The public marketing pages double as the site's AI-search surface: when
someone asks an assistant for a free expense tracker, GPTBot / OAI-SearchBot /
ClaudeBot / PerplexityBot crawl and quote them. The copy is written as
standalone, quotable answers that name the app and its URL, and it lives in
ONE place β app/lib/seo-content.ts β which renders every surface:
| Page | Purpose |
|---|---|
/ | Landing page (SoftwareApplication JSON-LD) |
/about | Full feature/benefit list (AboutPage JSON-LD) |
/faq | 13 Q&As matching real AI queries (FAQPage JSON-LD) |
/alternatives | Expense vs Expensify comparison (WebPage + FAQPage JSON-LD) |
/llms.txt | The llmstxt.org file β the curated overview AI assistants read |
/about.md /faq.md /alternatives.md | Markdown mirrors per the llms.txt convention |
Supporting plumbing: public/robots.txt explicitly allows the AI crawlers
(GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot / Claude-SearchBot /
Claude-User, PerplexityBot / Perplexity-User, Google-Extended,
Applebot-Extended, meta-externalagent) while app routes stay blocked, and
public/sitemap.xml lists the public pages.
These routes are public (see the root loader in app/root.tsx); everything
else still requires a session.
YYYY-MM-DD_REPORT_FILE.ext).The app speaks the Model Context Protocol at https://expense.labnotes.org/mcp
(auth: OAuth 2.1 authorization-code + PKCE β sign in with your account and
approve the connection; no API keys). An assistant connected to your account
can:
Connect any MCP client:
The client opens your browser; you sign in and click Allow. Manage
connections (per-token delete, full disconnect) in Settings β Agents &
API (MCP). See docs/mcp.md for the full reference and
docs/mcp-directories.md for the directory
listings.
Storage is Postgres-only via Prisma (prisma/schema.prisma is the
single schema source of truth; the client is generated to
prisma/generated by pnpm build:prisma). DATABASE_URL is required at
startup (the app exits with a clear error otherwise). Receipt images live in
Postgres BYTEA (image_blobs) in prod and dev β no separate storage service.
| Data | Images |
|---|---|
accounts / users / | Postgres BYTEA (image_blobs, |
expenses / reports / | prod and dev) |
categories / settings / | |
mileage / image_blobs |
All reads/writes go through app/lib/store.server.ts (β
app/lib/database.ts, Prisma queries scoped by accountId); image storage
is behind app/lib/images.server.ts (Prisma imageBlob).
Keys are images/{accountId}/... pathnames on every backend β namespaced
per account so two accounts can never collide on the same filename.
Schema changes: edit prisma/schema.prisma, then prisma migrate dev --name β¦ locally and
pnpm db:push (or pnpm db:migrate) before deploying.
data/ β removedThe file-era migration source (data/*.csv + data/images/* and the
pnpm migrate-data one-off) was deleted in the Jul 2026 cleanup β data now
lives in the database, and importing from Expensify happens via
scripts/import-expensify.ts. Cloning prod uses scripts/clone
(prisma/backup.sql).
Load order: real process.env (Vercel dashboard, or inline) wins; a local
.env file fills the gaps. DATABASE_URL is required; .env is gitignored. if
(!hasDatabase()) {
dev / test β local .env:
On an empty database the first account + user are bootstrapped from
APP_EMAIL/APP_PASSWORD (fail-closed if missing); afterwards users are
created through the app's signup/join flow. SESSION_SECRET is always
required. APP_EMAIL/APP_PASSWORD can be removed from .env once you
have at least one user.
Accounts created before email login (username era) keep their old username
as the stored email until APP_EMAIL is set β initStore then backfills
that address onto the bootstrap (oldest) user, so the configured credentials
keep working.
Tests intentionally hardcode expense_test (Postgres incl. image blobs),
ignore the local database, and reset the schema from Prisma on each run
(pnpm test:db:push in the test setup).
prod β Vercel: set env vars in the project dashboard (Settings β
Environment Variables): DATABASE_URL (Supabase Supavisor pooled URL),
SESSION_SECRET,
and (only until the first user exists) APP_EMAIL / APP_PASSWORD.
Vercel injects them at runtime; .env never exists there.
Forward a receipt email to your inbox address and it's parsed and added automatically: the merchant, amount, and category are extracted, the receipt is stored as an image, and the expense date is the date of the email being forwarded. If something can't be processed, a reply email explains what happened.
How it decides what to import:
Vercel has no inbound email, so receipt emails are received by Resend
(receiving = parse email β POST webhook) and parsed by DeepSeek
(deepseek-v4-flash). Replies on failure go out through Resend too.
Create a Resend account and add a domain (e.g.
expense.labnotes.org) β you'll point MX/DKIM/SPF DNS records at Resend.
Resend β Receiving: add a receiving domain and an inbound route
(catch-all or receipts@β¦) that POSTs to
https://<your-app>/api/inbound-email.
On the webhook, copy the signing secret (whsec_β¦).
Create a DeepSeek API key.
Set env vars (dev .env, prod Vercel dashboard):
All are optional β without them the webhook returns 503 and receipts aren't imported.
In the app: Settings β Receipts by email β add each address you'll forward from (you can add several), then forward a receipt to your inbound address.
Notes:
RECEIPT_OCR_MODE=deepseek if/when the hosted model
accepts images (it tries vision first and falls back automatically on
auto).maxDuration) β enough for
attachment download + OCR + extraction.Prerequisites: Postgres running locally (brew services start postgresql@18).
Running the server without DATABASE_URL exits immediately with a clear
error β there is no file-based fallback.
Node 24+ and pnpm 11+ (developed/tested on Node 26).
Vercel (recommended): the app deploys with Vercel's zero-config React
Router support β no preset needed. (@vercel/react-router's vercelPreset()
is still pinned to React Router v7 as of this writing β track
vercel/vercel#16730; the
zero-config path builds one SSR function that serves every route.)
Push to GitHub (already configured: origin β assaf/expense).
In Vercel: Add New β Project β Import assaf/expense. Framework is
auto-detected as React Router; vercel.json pins the build command.
Set env vars in the project (Settings β Environment Variables):
DATABASE_URL β Supabase pooled URL (Supavisor transaction-mode
pooler). Schema is managed by Prisma migrations β apply prod schema
changes with ./scripts/migrate-prod (never runtime DDL).engines; the project runs on Node 26); pick Node 26 in
project settings if Vercel doesn't match automatically.One-time data import is done β the CSV source under data/ was deleted
in the Jul 2026 cleanup (data verified in the database: 306 expenses,
247 images). Import from Expensify now goes through
scripts/import-expensify.ts; cloning prod locally uses scripts/clone.
Deploy. Test the app is behind Deployment Protection or basic auth β the app has no built-in login (single-user personal tool).
Vercel has its own environment management (dashboard) β set DATABASE_URL
there directly (all images are stored in the database, so no other storage
env is needed).
Uses free OpenStreetMap services (Nominatim for geocoding, OSRM for routing, OSM raster tiles for the map). Rate-limited but fine for personal use. If OSRM is unavailable, distance falls back to straight-line (marked "approx.").
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/expense)<a href="https://allmcps.com/mcp/expense"><img src="https://allmcps.com/api/badge/expense?style=directory" alt="Expense on AllMCPs" /></a>