Process expense claims by matching YNAB transactions with uploaded receipts. Use when the user mentions claims, expenses, reimbursements, receipts, or YNAB TODOs.
Process expense claims by matching YNAB transactions with uploaded receipts.
You are helping the user process expense claims. Follow this workflow.
Parallelization Strategy: Use sub-agents (Task tool) throughout to maximize speed:
Use the Read tool to read .env in the project root. Extract these values:
YNAB_API_KEY - API key for YNABYNAB_BUDGET_ID - Budget ID to queryR2_WORKER_URL - URL of the receipt upload workerR2_PASSWORD - Password for receipt worker authIf .env is missing or incomplete, ask the user to set it up using .env.example as a template.
Important: When using these values in curl commands, substitute them directly into the command (don't rely on shell variable expansion from source .env as it doesn't handle comments well).
Use curl to fetch transactions marked with "TODO" in the memo:
curl -s -H "Authorization: Bearer <YNAB_API_KEY>" \
"https://api.ynab.com/v1/budgets/<YNAB_BUDGET_ID>/transactions" \
| jq '[.data.transactions[] | select(.memo) | select(.memo | ascii_downcase | contains("todo"))]'
Note: Filter for amount < 0 (outflows) to avoid duplicate transfer entries.
Parse the response to extract:
id - Transaction ID (for updating later)date - Transaction dateamount - Amount in milliunits (divide by 1000 for actual amount)payee_name - Merchant/payeememo - Contains "TODO: description"category_name - CategoryList receipts from R2:
curl -s -H "X-Auth-Token: <R2_PASSWORD>" "<R2_WORKER_URL>/list" | jq '.receipts'
Response includes link metadata (if user pre-linked via web UI):
{
"key": "2025-01-01_120000_abc12345_receipt.pdf",
"size": 12345,
"uploaded": "2025-01-01T12:00:00.000Z",
"originalName": "receipt.pdf",
"linkedClaimId": "ynab-transaction-id", // If pre-linked
"linkedClaimDescription": "ChatGPT" // Claim description
}
Pre-linked receipts: When linkedClaimId is present, auto-match this receipt to the corresponding YNAB TODO - skip manual matching for these.
Before matching, download and read ALL receipts to identify their contents. Don't rely solely on filenames - many receipts have generic names like "Receipt-1234.pdf" or "unnamed.png".
Use sub-agents for parallel processing: Spawn multiple Task tool agents (subagent_type="general-purpose") to download and identify receipts concurrently. Each agent handles one receipt:
Task 1: "Download receipt [key1] from R2, convert if HEIC, read and extract: merchant, date, amount, invoice#. Return structured summary."
Task 2: "Download receipt [key2] from R2, convert if HEIC, read and extract: merchant, date, amount, invoice#. Return structured summary."
...etc
Launch all agents in a single message (parallel tool calls) for maximum speed.
For each receipt, the agent should:
Download to /tmp/claims/:
mkdir -p /tmp/claims
curl -s -H "X-Auth-Token: <R2_PASSWORD>" "<R2_WORKER_URL>/receipt/[key]" -o /tmp/claims/[filename]
For HEIC/image files: Convert if needed:
sips -Z 1500 /tmp/claims/file.heic --out /tmp/claims/file.jpg
Read the receipt using the Read tool to extract:
Return structured data for the manifest.
Collect all agent results and build the receipt manifest for matching.
Compare TODOs against identified receipts and show a summary:
Matching priority:
linkedClaimId matches a TODO's transaction ID, use that receipt (highest priority)Present the overview:
=== CLAIMS OVERVIEW ===
🔗 PRE-LINKED (X items) - user already matched via web UI:
- [date] [description] $[amount] ← [receipt name]
...
✅ READY TO PROCESS (X items) - have matching receipts:
- [date] [description] $[amount]
...
❌ MISSING RECEIPTS (Y items) - need to find:
- 3x Cold Storage (~$40-60 each, Oct-Nov)
- 2x Grab rides (~$30-40, Nov)
- 1x GitHub ($133, Nov 4)
...
📎 UNMATCHED RECEIPTS (Z items) - uploaded but no matching TODO:
- [filename] [date]
...
Ask the user:
Sorting strategy (maintains claiming momentum by keeping similar items together):
Group by merchant first - All Cold Storage claims together, all Grab claims together, etc.
Within each merchant, sub-group by description similarity - Infer from the TODO description what type of expense it is:
Within sub-groups, sort by date - Chronological order within similar items
Example ordering:
Cold Storage (5 items):
- Groceries: Oct 1, Oct 8, Oct 15
- Household: Oct 5, Oct 12
Grab (3 items):
- Work commute: Oct 2, Oct 9
- Client meeting: Oct 7
GitHub (1 item):
- Subscription: Nov 4
Present this grouping to user and confirm the processing order before starting.
For each TODO transaction:
Show transaction details:
Find matching receipt(s):
linkedClaimId matching this transaction, use it automatically (skip manual matching)Download and open the receipt:
mkdir -p /tmp/claims
curl -s -H "X-Auth-Token: <R2_PASSWORD>" "<R2_WORKER_URL>/receipt/[key]" -o /tmp/claims/[filename]
For HEIC files: Convert to JPEG for easier viewing, then delete the HEIC:
sips -Z 1500 /tmp/claims/file.heic --out /tmp/claims/file.jpg
trash /tmp/claims/file.heic
Rename for clarity: Rename the local file to a descriptive format:
[claim#] - [merchant] [date] [amount].[ext]
Example: 1 - stratechery-dithering 25-oct 150.pdf
Then open the renamed file. Also use the Read tool to view and extract details.
Cleanup: After each claim, delete the processed local file immediately to keep /tmp/claims clean. Only the current claim's receipt should be in the folder.
Extract from receipt:
Present formatted claim summary:
=== CLAIM SUMMARY ===
Date: [date]
Merchant: [merchant]
Description: [description from memo]
Amount: S$[YNAB amount]
(or for foreign currency: US$[receipt amount] (S$[YNAB amount] at exchange rate of [rate]))
Tax: [tax amount if found, or "included" / "not shown"]
Receipt: file:///tmp/claims/[filename]
Folder: file:///tmp/claims/
Copy merchant to clipboard: Run echo -n "[merchant]" | pbcopy so user can paste it easily. Use the registered company name, not the trade name:
Currency discrepancies: If YNAB amount (SGD) differs from receipt amount, assume USD and calculate the exchange rate: YNAB_SGD / Receipt_USD. Display as: US$X (S$Y at exchange rate of Z)
Wait for user confirmation. When user says "done":
For speed: Show the next claim's details FIRST, then run cleanup via background sub-agent:
run_in_background: true) to handle cleanup for the completed claimBackground cleanup agent prompt:
"Complete claim cleanup for transaction [TRANSACTION_ID]:
1. Update YNAB memo from 'TODO: X' to 'CLAIMED: X' via PUT to transactions API
2. Delete receipt [key] from R2 via DELETE endpoint
3. Delete local file /tmp/claims/[filename] using trash command
Credentials: YNAB_API_KEY=[key], R2_WORKER_URL=[url], R2_PASSWORD=[pwd]"
This runs cleanup concurrently while user reviews the next claim. No need to wait for cleanup to complete before proceeding.
Cleanup tasks (for reference):
curl -s -X PUT -H "Authorization: Bearer <YNAB_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"transaction": {"memo": "CLAIMED: [description]"}}' \
"https://api.ynab.com/v1/budgets/<YNAB_BUDGET_ID>/transactions/<TRANSACTION_ID>"
curl -s -X DELETE -H "X-Auth-Token: <R2_PASSWORD>" "<R2_WORKER_URL>/receipt/[key]"
trash /tmp/claims/[filename]
Move to the next claim.
When all claims are processed:
Wait for background cleanup agents: Use TaskOutput to verify all background cleanup tasks completed successfully. Report any failures.
Show summary:
Use the Playwright script in scripts/volopay-submit.ts to automate Volopay claim submission.
cd scripts
npm run submit -- claim.json
Or pipe JSON directly:
echo '{"merchant":"...","amount":99.99,...}' | npm run submit
{
"merchant": "Lovable Labs Incorporated",
"amount": 33.39,
"date": "2025-12-20",
"volopayCategory": "Software",
"memo": "Lovable AI subscription",
"xeroCategory": "Computer Software (463)",
"xeroTaxCode": "OPINPUT:Out Of Scope Purchases",
"xeroBizUnit": "Classes",
"receiptPath": "/tmp/claims/receipt.pdf"
}
CRITICAL: Only use INPUTY24 if the receipt explicitly shows a GST line item with amount. Never assume GST.
| Condition | Tax Code |
|---|---|
| Receipt shows explicit GST amount (e.g., "GST 9%: $X.XX") | INPUTY24:Standard-Rated Purchases |
| No GST breakdown + Foreign currency (USD) | OPINPUT:Out Of Scope Purchases |
| No GST breakdown + SGD | NRINPUT:Purchases from Non-GST Registered Suppliers |
WARNING: "Inclusive of taxes" does NOT mean GST is shown. You must see an actual GST line item to use INPUTY24.
| Expense Type | Volopay Category |
|---|---|
| Software/SaaS | Software |
| Hardware/Equipment | Equipment & hardware |
| Food/Meals | Entertainment |
| Expense Type | Xero Category |
|---|---|
| Software/SaaS | Computer Software (463) |
| Software for class (IMDA VIBE, "for class") | Cost of Sales (320) |
| Hardware | Computer Hardware & Accessories (464) |
| Books | Books, Magazines, Journals (460) |
| Transport (local) | Local Public Transport (incl Taxi) (451) |
| Transport (overseas) | Overseas Transport (452) |
| Phone/Internet | Telephone & Internet (467) |
Note: Most software uses "Computer Software (463)". Only use "Cost of Sales (320)" when the YNAB memo explicitly mentions IMDA VIBE or "for class".
YNAB API: https://api.ynab.com/v1/ Transaction amounts: In milliunits (divide by 1000) Negative amounts: Outflows (expenses) Positive amounts: Inflows
Receipt filename format: YYYY-MM-DD_HHMMSS_originalname.ext
Volopay URL: ${VOLOPAY_URL}/my-volopay/reimbursement/claims?createReimbursement=true (configured in .env)