AINode.jsAutomation
Receipt Scanner & Expense Tracker
TT
TopicTrick TeamReceipt Scanner & Expense Tracker
Manually entering receipts into expense software is one of the most tedious business tasks. This tool photographs or uploads a receipt, extracts all the data with GPT-4o Vision, and logs the expense automatically — with merchant, date, total, category, and line items.
This is Tool 19 of the Build 50 AI Automation Tools course.
What You'll Build
POST /scan— upload a receipt image, extract data, save to databaseGET /expenses— query expenses by date range, category, or amountGET /expenses/export— export as CSV for accounting software
Setup
bash
mkdir receipt-scanner && cd receipt-scanner
npm init -y
npm install express multer openai better-sqlite3 dotenvbash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000Database Setup
js
// src/db.js
import Database from 'better-sqlite3';
const db = new Database('expenses.db');
db.exec(`
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
merchant TEXT NOT NULL,
date TEXT,
total REAL NOT NULL,
currency TEXT DEFAULT 'USD',
tax REAL,
tip REAL,
category TEXT,
paymentMethod TEXT,
notes TEXT,
lineItems TEXT, -- JSON string
rawData TEXT, -- Full AI response JSON
imageHash TEXT,
scannedAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
export const queries = {
insertExpense: db.prepare(`
INSERT INTO expenses (merchant, date, total, currency, tax, tip, category, paymentMethod, notes, lineItems, rawData)
VALUES (@merchant, @date, @total, @currency, @tax, @tip, @category, @paymentMethod, @notes, @lineItems, @rawData)
`),
getExpenses: db.prepare('SELECT * FROM expenses WHERE scannedAt BETWEEN ? AND ? ORDER BY date DESC'),
getByCategory: db.prepare('SELECT * FROM expenses WHERE category = ? ORDER BY date DESC'),
getTotal: db.prepare('SELECT SUM(total) as total, COUNT(*) as count FROM expenses WHERE scannedAt BETWEEN ? AND ?'),
};
export default db;Receipt Scanning Service
js
// src/services/receiptService.js
import OpenAI from 'openai';
import { queries } from '../db.js';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const CATEGORIES = [
'meals_entertainment', 'travel', 'accommodation', 'software_subscriptions',
'office_supplies', 'transportation', 'utilities', 'marketing', 'healthcare',
'education', 'equipment', 'fuel', 'miscellaneous'
];
export async function scanReceipt(buffer, mimetype) {
const dataUrl = `data:${mimetype};base64,${buffer.toString('base64')}`;
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are an expert expense management system. Extract all data from this receipt image.
Available categories: ${CATEGORIES.join(', ')}
Return ONLY a JSON object — no markdown:
{
"merchant": "business name",
"address": "string or null",
"date": "YYYY-MM-DD format or null",
"time": "HH:MM format or null",
"lineItems": [
{ "description": "string", "quantity": number or null, "unitPrice": number or null, "total": number }
],
"subtotal": number or null,
"tax": number or null,
"taxRate": "string or null (e.g. '8.5%')",
"tip": number or null,
"discount": number or null,
"total": number,
"currency": "3-letter code (e.g. USD, EUR, GBP)",
"paymentMethod": "cash | credit_card | debit_card | digital_wallet | unknown",
"receiptNumber": "string or null",
"category": "one of the available categories",
"notes": "any other notable information (e.g. 'Business lunch with client')",
"confidence": 0.0-1.0
}
If the image is not a receipt, set merchant to null and confidence to 0.`,
},
{
role: 'user',
content: [
{ type: 'text', text: 'Extract all expense data from this receipt:' },
{ type: 'image_url', image_url: { url: dataUrl, detail: 'high' } },
],
},
],
temperature: 0,
response_format: { type: 'json_object' },
});
const extracted = JSON.parse(response.choices[0].message.content);
if (!extracted.merchant || extracted.confidence < 0.3) {
throw new Error('Could not extract receipt data — image may not be a receipt or quality is too low');
}
// Save to database
const expenseId = queries.insertExpense.run({
merchant: extracted.merchant,
date: extracted.date,
total: extracted.total,
currency: extracted.currency || 'USD',
tax: extracted.tax,
tip: extracted.tip,
category: extracted.category,
paymentMethod: extracted.paymentMethod,
notes: extracted.notes,
lineItems: JSON.stringify(extracted.lineItems || []),
rawData: JSON.stringify(extracted),
}).lastInsertRowid;
return { id: expenseId, ...extracted };
}
export function getExpenses(startDate, endDate, category = null) {
if (category) return queries.getByCategory.all(category);
const start = startDate || '2000-01-01';
const end = endDate || '2099-12-31';
return queries.getExpenses.all(start, end);
}Server
js
// src/server.js
import 'dotenv/config';
import express from 'express';
import multer from 'multer';
import { scanReceipt, getExpenses } from './services/receiptService.js';
import { queries } from './db.js';
const app = express();
app.use(express.json());
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (_req, file, cb) =>
['image/jpeg', 'image/png', 'image/webp', 'image/heic'].includes(file.mimetype)
? cb(null, true) : cb(new Error('Image files only')),
});
app.post('/scan', upload.single('receipt'), async (req, res, next) => {
try {
if (!req.file) return res.status(400).json({ error: 'No receipt image uploaded' });
const result = await scanReceipt(req.file.buffer, req.file.mimetype);
res.json({ success: true, expense: result });
} catch (err) { next(err); }
});
app.get('/expenses', (req, res) => {
const { startDate, endDate, category } = req.query;
const expenses = getExpenses(startDate, endDate, category);
const total = queries.getTotal.get(startDate || '2000-01-01', endDate || '2099-12-31');
res.json({ success: true, count: expenses.length, totalSpend: total.total, expenses });
});
app.get('/expenses/export', (req, res) => {
const { startDate, endDate } = req.query;
const expenses = getExpenses(startDate, endDate);
const csv = ['Date,Merchant,Category,Total,Currency,Tax,Tip,Payment Method']
.concat(expenses.map(e =>
`"${e.date}","${e.merchant}","${e.category}","${e.total}","${e.currency}","${e.tax || ''}","${e.tip || ''}","${e.paymentMethod}"`
)).join('\n');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="expenses-${startDate || 'all'}.csv"`);
res.send(csv);
});
app.use((err, _req, res, _next) => res.status(500).json({ error: err.message }));
app.listen(process.env.PORT ?? 3000, () => console.log('Receipt Scanner running'));Testing
bash
# Scan a receipt
curl -X POST http://localhost:3000/scan \
-F "receipt=@restaurant-receipt.jpg"
# Get all expenses this month
curl "http://localhost:3000/expenses?startDate=2025-11-01&endDate=2025-11-30"
# Export as CSV
curl "http://localhost:3000/expenses/export?startDate=2025-11-01" \
-o november-expenses.csvBuild 50 AI Automation Tools — Tool 19 of 50
Receipt scanning is live. Continue to Tool 20 to build a screenshot-to-code converter with GPT-4o Vision.
Summary
- GPT-4o Vision handles skewed, low-quality, and printed receipt photos where traditional OCR fails
- SQLite with better-sqlite3 provides zero-setup persistent storage — no database server required
- Confidence threshold prevents bad extractions from polluting the expense database
- The CSV export maps directly to accounting software import formats
- Extend by adding category budget limits and alerting when spending exceeds thresholds per category
Continue to Tool 20: Screenshot to Code Converter →
Post Navigation (Previous/Next)
