← projects
Side project · shipping · salsa-collective.com

venmo-ledger — Email-to-Ledger for Venmo

Turn Venmo email receipts into a real transactions database — no API, no Plaid

Go 1.23Gmail OAuth2Postgres / SupabaseDockerLovable admin UI
// 01 — problem

Venmo has no public API for reading your own transactions. A small studio that takes class payments, dues, and event fees over Venmo is stuck copy-pasting from the inbox into a spreadsheet — and paid tools like Copilot or Lunch Money are aimed at personal finance, not at answering "who paid for classes this month?" I needed an open, self-hosted ledger fed by the one signal Venmo can't take away: the email receipt.

// 02 — approach
  • 01Go 1.23 worker polls a Gmail inbox over OAuth2 (read-only scope) and pulls every message from Venmo.
  • 02Regex-based parser extracts amount, counterparty, @handle, note, Venmo transaction ID, and date directly from the email body — no LLM needed because Venmo's receipts are predictable.
  • 03Each message writes atomically into two Postgres tables: `transactions` (typed payment_received / payment_sent / bank_transfer / expense) and `raw_emails` for dedup + re-parse. `gmail_message_id` is unique, so re-running is a no-op.
  • 04Same-shape stack as tanda: worker has no HTTP API. The Lovable admin UI in the Salsa Collective site talks to Supabase directly for the payments dashboard, filters, category tagging, and month-to-date income cards.
  • 05Runs anywhere: Docker for local dev, systemd unit on a $5 VPS or the same Raspberry Pi that runs Hermes and tanda for production.
// 03 — result

Every Venmo payment lands in the database within minutes of the email arriving. The Salsa Collective admin can filter by counterparty, type, and category, see income this month at a glance, and jump straight to the original Venmo transaction — with 134 historical payments backfilled from a 6-month Gmail lookback on first run.

// 04 — key decisions
Why parse emails instead of using a bank aggregator

Plaid and friends need bank credentials, cost real money per connection, and don't cover Venmo P2P cleanly. The email receipt is free, arrives reliably, and Venmo is unlikely to break its own template — so parsing it turns Gmail into a zero-cost, zero-lockin transaction feed.

Why regex, not an LLM

Venmo's email is a machine-generated template with maybe five variants. A regex-based parser is faster, deterministic, free to run, and easy to unit-test — using an LLM here would be paying tokens to solve a problem `grep` already solved.

Why worker-only, no HTTP API

Same reasoning as tanda: Lovable + Supabase already give me a UI and a query layer. Adding an HTTP surface on the worker would mean a new auth story, a new deploy target, and a new class of bugs — for zero user-visible benefit.

Why keep raw_emails around

The parser will get things wrong eventually — a new email format, an edge case, a bad regex. Keeping the raw email lets me re-parse historical data in place instead of asking Gmail for it again, and gives me an audit trail if a number ever looks off.

// 05 — further reading