- A Google account (the free one).
- An iPhone (no iPhone 17 Pro Max 2 TB required).
- Some money to be spent (spending it wisely, thank me later).
- Levels 1 - 3 are follow-along instructions while level 4-5 only cover the key highlights.
- Levels 1 - 3 are designed to you hands-on and comfortable playing around with iOS Shortcuts.
- Setup a Google Form, create a Gemini API Key, download the ready-to-use shortcut and here we go โฝ๏ธ!
This stage covers initial setup and foundational knowledge you need to get started ๐.
- Go to https://docs.google.com/forms/u/0/.
- Choose Blank Form and rename to
Daily Spending Tracker Indo. - Leave Form description blank.
- Rename Untitled Question to
Kategoriand change to Short answer. - Click Copy icon to Duplicate Question and rename to
Deskripsi. - Click Copy icon to Duplicate Question and rename to
Nominal. Click Triple dots icon and choose Response validation. Choose Number and Is number. - Move to Responses Tab and click Link to Sheets. Stay on Create a new spreadsheet option and rename to
Daily Spending Tracker Indo (Responses). Click Create. - Click Publish button. Click Publish button again.
- Click Triple dots icon on the right side of Published button and choose Pre-fill form. Fill the form with the random values and click the Get link button.
- Click the COPY LINK button on the bottom pop-up later on. Paste it somewhere:
For example, here is the clipboard (your Form ID maybe different):
https://docs.google.com/forms/d/e/1FAIpQLSfgd-AefHbwUojiaFUEnrnMSWaeXqMxoumpn40CALxNksfC5g/viewform?usp=pp_url&entry.108078765=food&entry.799982393=satay&entry.778257956=25000so the Form URL will look like this:
https://docs.google.com/forms/d/e/1FAIpQLSfgd-AefHbwUojiaFUEnrnMSWaeXqMxoumpn40CALxNksfC5g/formResponsealso jotted down the entries reflected to our questions (Your Entry ID maybe different):
| Question | Type | Response validation | Entry ID |
|---|---|---|---|
Kategori |
Short answer | entry.108078765 |
|
Deskripsi |
Short answer | entry.799982393 |
|
Nominal |
Short answer | Number; Is number; Custom error text | entry.778257956 |
- Open Shortcuts app in iPhone. Tap + to create a new shortcut. Rename New Shortcut to
Daily Spending Tracker Indo. - In Search Actions bar, find and choose Ask for Input. Fill Prompt with
Kategori?. Tap Chevron icon and untick Allow Multiple Line option. - Tap Ask for Input icon action and choose Duplicate. Rename it into
Deskripsi?. - Duplicate once more. Tap Text and choose Number. Rename it into
Nominal?. - Tap Chevron icon and untick Allow Decimal Numbers and Allow Negative Numbers options.
- In Search Actions bar, find and choose Get contents of URL. Fill URL with the
Form URLthen tap Chevron icon. - Tap GET and change the Method into
POST. Tap JSON and change the Request Body intoForm. - Tap Add new field and choose Text. Fill Key with
Entry ID. Repeat for allEntry ID. - Tap Text on each field and choose Select Variable. Swipe up and select Ask for Input, matching it accordingly.
- Tap Add new field and choose Text. Fill Key with
submitand Text withSubmit.
For the first time, pop-up will be shown to ask permission for accessing the form URL. Simply choose Always Allow.
This stage adds context to your data. Now we don't just track what you spent, but also where you spent it ๐๏ธ.
- Go to https://docs.google.com/forms/u/0/.
- Open
Daily Spending Tracker Indoform and click Amount question. Click Copy icon twice to Duplicate Question two times. - Rename the duplicated questions to each
LongitudeandLatituderespectively. - Click Triple dots icon on the right side of Published button and choose Pre-fill form. Fill the form with the random values and click the Get link button.
- Click the COPY LINK button on the bottom pop-up later on. Paste it somewhere:
Take a look only for the new Entry ID:
https://docs.google.com/forms/d/e/1FAIpQLSfgd-AefHbwUojiaFUEnrnMSWaeXqMxoumpn40CALxNksfC5g/viewform?usp=pp_url&entry.108078765=food&entry.799982393=satay&entry.778257956=25000&entry.214181402=-1.5&entry.714053152=1.5| Question | Type | Response validation | Entry ID |
|---|---|---|---|
Latitude |
Short answer | Number; Is number; Custom error text | entry.214181402 |
Longitude |
Short answer | Number; Is number; Custom error text | entry.714053152 |
- In Search Actions bar, find and choose Get Current Location.
- In Search Actions bar, find and choose Get Details of Locations. Tap Detail and choose
Latitude. - Tap Get Details of Locations icon and choose Duplicate. Tap Latitude and change it to
Longitude. - Tap and hold Get contents of URL action then drag it to the very bottom. Next, Tap Chevron icon.
- Tap Add new field and choose Text. Fill Key with the new
Entry ID. Repeat for all newEntry ID. - Tap Text on each field and choose Select Variable. Swipe up and choose
LongitudeandLatituderespectively. - Tap and hold Hamburger icon on
submitfield then drag it to the very bottom.
This stage makes your data cleaner and your shortcut smarter. No more random in Kategori and let's start make things tidy and programmatic ๐ฏ.
- In Search Actions bar, find and choose List.
- Change One to
๐ฑ๐ Makan/Minumand change Two to๐ฅค๐ฉ Jajan. - Tap Add New Item and fill Text to
๐๐ Belanja. Continue for the rest or feel free to adjust as needed. - Tap and hold List action then drag it to the very top.
- In Search Actions bar, find and choose Choose from List. Tap and hold then drag it to down below the List action.
- Tap Chevron icon and fill Prompt with
Kategori?and remove Ask for Input action for theKategori?we've created previously. - In Search Actions bar, find and choose Set Variable. Tap and hold then drag it to down below the Choose from List action. Fill
Variable Namewithcategory. - In Search Actions bar, find and choose Set Variable. Tap and hold then drag it to down below the Ask for Input action for
Deskripsi?. FillVariable Namewithdescription. - In Search Actions bar, find and choose Set Variable. Tap and hold then drag it to down below the Ask for Input action for
Nominal?. FillVariable Namewithamount. - Go to the Get contents of URL action and tap Chevron icon. You'll notice the
Kategori?value field within Request Body turn into red. - Tap on it and click Clear variable. Tap Text and click Select Variable. Swipe up and choose
categoryvariable. - Do the same for the two next fields for clear the previous variables and choose our newly created variables,
descriptionandamount.
For the first time, pop-up will be shown to ask permission for accessing our current loction. Simply choose Always Allow.
| No. | Category | Examples |
|---|---|---|
| 1. | ๐ฑ๐ Makan/Minum | warung, resto, food delivery, nasi padang |
| 2. | ๐ฅค๐ฉ Jajan | boba, donut, snack, cafe ringan, bakery |
| 3. | ๐๐ Belanja | raw groceries: sayur, daging, bumbu, beras |
| 4. | ๐งด๐ชฅ Rumah Tangga | sabun, odol, deterjen, tissue, perlengkapan rumah |
| 5. | ๐ถ๐ Tagihan | PLN, PDAM, Indihome, pulsa, BPJS |
| 6. | ๐โฝ Transport | bensin, ojol, parking, tol, tiket |
| 7. | ๐ฌ๐ฎ Hiburan | Spotify, Netflix, Vidio, bioskop, game |
| 8. | ๐๐ Fashion | baju, sepatu, salon, skincare premium, parfum |
| 9. | ๐๐ช๐ป Kesehatan | apotek, dokter, vitamin, gym, klinik |
| 10. | ๐ง ๐ Level Up | course, buku, sertifikasi, ChatGPT, Udemy |
| 11. | ๐๐ Sosial | hadiah, kondangan, sumbangan, kasih ortu |
| 12. | ๐ฐ๐ Finansial | cicilan, asuransi premi, investasi |
This stage is where AI takes over! Just upload a receipt and let the magic happen ๐ง .
- Go to https://aistudio.google.com/api-keys.
- At the right top, click Create API Key button.
- Rename Gemini API Key to something more descriptive.
- Assign it to a specific project to stay organized.
- You can also choose + Create Project directly if you prefer.
The API Key will be used later on for the headers value of x-goog-api-key key.
%%{init: {'flowchart': {'curve': 'basis'}}}%%
flowchart TD
Start([Start])
Menu{Choose
from Menu}
Start --> Menu
Menu --> | Manual Input | ChooseList
Menu --> | Scan Receipt | SelectPhoto
ChooseList[/Choose
from List/]
AskDesc[/Ask for Input/]
AskAmt[/Ask for Input/]
SetCat1["Set Variable
(category)"]
SetDesc1["Set Variable
(description)"]
SetAmt1["Set Variable
(amount)"]
ChooseList -.-> List --> SetCat1 --> AskDesc --> SetDesc1 --> AskAmt --> SetAmt1
SelectPhoto[/Select
Photo/]
subgraph AIModel["Send to Gemini AI Model"]
Encode[Encode
Base64]
Prompt[/"Text
(AI Prompt)"/]
API[/"Text
(API Key)"/]
AIPost["Get Contents
of URL (POST)"]
end
Parse[[Parsing Response]]
SetCat2["Set Variable
(category)"]
SetDesc2["Set Variable
(description)"]
SetAmt2["Set Variable
(amount)"]
API -.-> | Headers | AIPost
SelectPhoto --> Encode -.-> | Request Body | AIPost
Prompt -.-> | Request Body | AIPost
AIPost --> | Response Body | Parse --> SetCat2 --> SetDesc2 --> SetAmt2
subgraph GForm["Send to Google Form"]
Variables["
category, description, amount, latitude, longitude"]
FinalPost["Get Contents
of URL (POST)"]
end
Location[Get Current
Location]
LocationDtl[Get Details of
Locations]
SetAmt1 --> Variables
Location --> LocationDtl --> Variables
SetAmt2 --> Variables
Variables --> | Request Body | FinalPost
FinalPost --> End([End])
Headers
โโโ x-goog-api-key: "AIzaSy...your_api_key"
โโโ Content-Type: "application/json"
Request Body: JSON
โโโ contents (Array, 1 item)
โ โโโ [Item 1: Dictionary]
โ โโโ parts (Array, 2 items)
โ โโโ [Item 1: Dictionary - IMAGE]
โ โ โโโ inline_data (Dictionary)
โ โ โโโ mime_type: "image/jpeg"
โ โ โโโ data: [Base64 Encoded] โ variable
โ โโโ [Item 2: Dictionary - TEXT]
โ โโโ text: `"You are a receipt parser..."`
โโโ generationConfig (Dictionary)
โโโ responseMimeType: "application/json"
Here's the prompt or feel free to adjust as needed.
You are a receipt parser for Indonesian receipts. Look at the receipt image and return ONLY a valid JSON object.
OUTPUT FORMAT:
{
"ai_description": "string in Proper Case",
"ai_amount": number,
"ai_suggestedCategory": "string"
}
RULES:
1. AMOUNT: Indonesian format, dots/commas are THOUSAND separators (Rp16.000 = 16000). Pick FINAL TOTAL paid (look for "Grand Total" or "Total"). Ignore DPP, PPN, Harga Jual, Subtotal, "Number of Items".
2. DESCRIPTION (MUST be in Proper Case โ first letter of each significant word capitalized):
- Find MERCHANT NAME. Usually at top header, but for small businesses may appear at FOOTER (after "Terima Kasih").
- IGNORE address/location-only lines (e.g., "Batukajang", "Jakarta Selatan", "Jl. Sudirman").
- Strip legal prefixes: PT, CV, UD.
- Format: "Merchant: Item1, Item2 (xN), Item3"
- For QTY > 1: append "(xN)" after item name.
- For QTY = 1: write item name ONLY. DO NOT add "(x1)".
- EXCEPT For FUEL, write item name and its numbers of Liter, add "L (without prefix space)"
- Convert ALL CAPS items to Proper Case (e.g., "ROTI TAWAR GANDUM" โ "Roti Tawar Gandum").
- Max 90 chars total.
- If no items, use merchant name only.
Examples:
- "Julie The Half Plate: Roti Tawar Gandum, Abon Roll (x2), Roti Keju, Snack Bolen Pisang, Roti Pisang (x2), Box Medium"
- "Indomaret: Aqua (x3), Sabun, Tissue (x2)"
- "Solusi Mart QRIS BNI"
3. SUGGESTED_CATEGORY must be EXACTLY one of (case-sensitive):
Makan/Minum (warung, resto, food delivery, nasi padang)
Jajan (boba, donut, snack, cafe ringan, bakery)
Belanja (raw groceries: sayur, daging, bumbu, beras)
Rumah Tangga (sabun, odol, deterjen, tissue, perlengkapan rumah)
Tagihan (PLN, PDAM, Indihome, pulsa, BPJS)
Transport (bensin, ojol, parking, tol, tiket)
Hiburan (Spotify, Netflix, Vidio, bioskop, game, konser)
Fashion (baju, sepatu, salon, skincare premium, parfum)
Kesehatan (apotek, dokter, vitamin, gym, klinik)
Level Up (course, buku, sertifikasi, ChatGPT, Udemy)
Sosial (hadiah, kondangan, sumbangan, kasih ortu)
Finansial (cicilan, asuransi premi, investasi)
4. PRIORITY RULES (if ambiguous):
- Premium personal care (>Rp100K, aesthetic-driven) โ Fashion
- Basic hygiene (<Rp100K, daily use) โ Rumah Tangga
- Streaming subscription โ Hiburan (not Tagihan)
- AI/productivity tools (ChatGPT, Claude) โ Level Up
- One-time course/exam โ Level Up
- BPJS / mandatory insurance โ Tagihan
- Optional insurance/investment โ Finansial
- Bensin + minor item (e.g., aqua) โ Transport (dominant cost rule)
- Convenience store mostly food โ Belanja
- Convenience store mostly toiletries โ Rumah Tangga
- Bakery/cake shop โ Jajan
5. Use null if cannot extract.
This stage unlocks direct camera scanning and resilient error handling. Congratulations, King ๐.
%%{init: {'flowchart': {'curve': 'basis'}}}%%
flowchart TD
Start([Start])
Menu{Choose
from Menu}
Menu2{Choose
from Menu}
Start --> Menu
Menu --> | Manual Input | ChooseList
Menu --> | Scan Receipt | Menu2
ChooseList[/Choose
from List/]
AskDesc[/Ask for Input/]
AskAmt[/Ask for Input/]
SetCat1["Set Variable
(category)"]
SetDesc1["Set Variable
(description)"]
SetAmt1["Set Variable
(amount)"]
ChooseList -.-> List --> SetCat1 --> AskDesc --> SetDesc1 --> AskAmt --> SetAmt1
SelectPhoto[/Select
Photo/]
subgraph AIModel["Send to Gemini AI Model"]
Encode[Encode
Base64]
Prompt[/"Text
(AI Prompt)"/]
API[/"Text
(API Key)"/]
AIPost["Get Contents
of URL (POST)"]
end
TakePhoto[/Take
Photo/]
subgraph Result[" "]
direction LR
error[/Error/]
candidates[/Candidates/]
error -.- candidates
end
subgraph Result2[" "]
direction LR
error2[[Error Handling]]
candidates2[[Parsing Response]]
error2 -.- candidates2
end
SetAmt2["Set Variable
(category, description, amount)"]
API -.-> | Headers | AIPost
Menu2 --> | Open Camera | TakePhoto --> Encode -.-> | Request Body | AIPost
Menu2 --> | Open Galery | SelectPhoto --> Encode
Prompt -.-> | Request Body | AIPost
AIPost --> | Response Body | Result --> Result2 --> SetAmt2
subgraph GForm["Send to Google Form"]
Variables["
category, description, amount, latitude, longitude"]
FinalPost["Get Contents
of URL (POST)"]
end
Location[Get Current
Location]
LocationDtl[Get Details of
Locations]
SetAmt1 --> Variables
Location --> LocationDtl --> Variables
SetAmt2 --> Variables
Variables --> | Request Body | FinalPost
FinalPost --> End([End])