Automation / VisionLive

Receipts in, structured rows out.

Telegram bot plus Claude Vision plus n8n plus Supabase. Snap a receipt, get a clean accounting row in under five seconds.

This one runs every day. I take photos of receipts on the road, forward them to a private Telegram bot, and by the time I am back in the car the row is in Supabase: vendor, date, gross, VAT, net, category, and the original image URL. The accountant exports a CSV at month end. Nobody types a thing.

01 / 03

What it does

A Telegram bot waits for photos. When one arrives, n8n picks up the webhook, downloads the file, and uploads it to a Supabase storage bucket. The image URL goes to Claude with a strict JSON schema: vendor, currency, gross, VAT, net, line items if visible, date, payment method, suggested category.

Claude returns clean JSON. n8n validates it with a Zod-style schema, writes a row to a receipts table, and posts a one-line confirmation back into the Telegram chat. If anything fails validation, the workflow flips the row to needs_review and pings me. There is no other branch.

It has been running for four months without intervention. Cost is roughly half a penny per receipt and three minutes a week of looking at the needs_review queue.

02 / 03

The problem it solves

I was paying for a receipt-capture SaaS that did three things: take photos, OCR them, and export to my accountant. It cost more per month than my Postgres bill. The OCR was good but the categorisation was rubbish, and exports needed cleaning every time.

Replacing it with a workflow that lives in n8n and Supabase took an evening. The categorisation is now better than the SaaS because Claude actually reads context like the line items and the vendor name, instead of running a fixed model trained two years ago.

03 / 03

Architecture

Five nodes in n8n, one Claude API call, one Supabase table. The whole workflow is a JSON file you can import.

  • Telegram trigger node listening on private bot updates.
  • HTTP request node downloading the photo via Telegram getFile.
  • Supabase storage node uploading to a receipts bucket with a signed URL.
  • Claude node with vision input and a strict JSON-only system prompt.
  • Supabase insert node writing the parsed row, tagged with the chat user.
  • A reply node sending a one-line confirmation and the row id back to Telegram.
Code

The interesting bits.

text·system prompt for Claude
You are a receipt parser. Given an image of a receipt,
return ONLY valid JSON matching this schema:

{
  "vendor": string,
  "country": string,           // ISO 3166-1 alpha-2
  "currency": string,          // ISO 4217
  "date": string,              // YYYY-MM-DD
  "gross": number,             // total paid, in major units
  "vat": number | null,        // VAT amount if shown
  "net": number | null,
  "payment_method": "card" | "cash" | "bank" | "unknown",
  "category": "travel" | "meals" | "supplies" | "software" | "fuel" | "other",
  "line_items": [{ "label": string, "amount": number }] | null,
  "confidence": number         // 0..1
}

Rules:
- If a field is not legible, use null. Do not guess.
- gross is always present. If you cannot read it, return confidence: 0.
- No prose. No markdown. No code fences. Just the JSON object.
typescript·n8n function node: validate.ts
import { z } from 'zod'

const Receipt = z.object({
  vendor: z.string().min(1),
  country: z.string().length(2),
  currency: z.string().length(3),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  gross: z.number().positive(),
  vat: z.number().nullable(),
  net: z.number().nullable(),
  payment_method: z.enum(['card', 'cash', 'bank', 'unknown']),
  category: z.enum(['travel', 'meals', 'supplies', 'software', 'fuel', 'other']),
  line_items: z.array(z.object({ label: z.string(), amount: z.number() })).nullable(),
  confidence: z.number().min(0).max(1),
})

const raw = JSON.parse($json.claude_output)
const parsed = Receipt.safeParse(raw)

return [{
  json: parsed.success
    ? { ...parsed.data, status: parsed.data.confidence >= 0.7 ? 'ok' : 'needs_review' }
    : { status: 'needs_review', error: parsed.error.message, raw },
}]
Tech stack

Tools, picked deliberately.

n8nTelegram Bot APIClaude 3.5 Sonnet (vision)Supabase PostgresSupabase StorageZodTypeScript
Run it yourself

From clone to working.

01

Create the Telegram bot

Talk to @BotFather, get a token, set the bot to private. Save the token in n8n credentials.

02

Spin up Supabase

Run the included migration: a receipts table and a storage bucket with a five-day signed URL policy.

03

Import the n8n workflow

Import receipts.json into your n8n. Plug in Telegram, Anthropic and Supabase credentials.

04

Activate the workflow

Switch it to active. Send a photo to the bot. The first parse takes about three seconds; subsequent ones are faster because of caching on the prompt.

05

Set up the export

A Supabase scheduled function runs at 23:00 on the last day of the month, exporting a clean CSV to a separate bucket and emailing the accountant.

Read the source.

The repository ships with a working example, env file template and a short README. Star it if it helps.