Expense Tracker
Agent-ready recipe for creating an expense tracker in andibase
This recipe tells an agent exactly how to build an expense tracker in andibase.
Goal
Create three things in one workspace:
- A
budgetdata definition. - An
expensedata definition. - One agent that helps users manage budgets and log expenses.
- One mini app that shows budget progress and recent expenses.
Expected outcome
After following this recipe, the workspace should have:
- a
budgetdefinition for monthly or custom budgets - an
expensedefinition for individual spending entries - an
expense-tracker-agentagent - an
expense-trackermini app
Use the API documented in:
- Data Model
- Apps
- Get started (for AI Agents)
- Data definitions API
- Data API
- Agents API
- Apps API reference
Agent instructions
When an agent executes this recipe, it should follow these rules:
- Create the
budgetdefinition first. - Create the
expensedefinition second. - After the
budgetdefinition exists, wire theexpense.budgetIdrelationship to the created budget definition id. - Create the expense-tracker agent with instructions focused on budget management and expense logging.
- Create the mini app last.
- Do not invent custom fields outside this recipe unless the user explicitly asks for them.
- Use sensible defaults and ask the user only when a missing choice changes money behavior, such as currency or budget period.
Step 1: Create the data model
Create two data definitions: budget and expense.
1. Budget definition
Use handle budget.
Recommended fields:
{
"name": "Budget",
"description": "Tracks a planned spending period and its limits.",
"fields": {
"periodName": {
"name": "Period name",
"type": "text"
},
"periodType": {
"name": "Period type",
"type": "select",
"options": [
{ "value": "weekly", "label": "Weekly", "color": "sky" },
{ "value": "monthly", "label": "Monthly", "color": "blue" },
{ "value": "quarterly", "label": "Quarterly", "color": "violet" },
{ "value": "yearly", "label": "Yearly", "color": "emerald" },
{ "value": "custom", "label": "Custom", "color": "zinc" }
]
},
"startDate": {
"name": "Start date",
"type": "date"
},
"endDate": {
"name": "End date",
"type": "date"
},
"currency": {
"name": "Currency",
"type": "select",
"options": [
{ "value": "CLP", "label": "CLP", "color": "red" },
{ "value": "USD", "label": "USD", "color": "green" },
{ "value": "EUR", "label": "EUR", "color": "blue" },
{ "value": "GBP", "label": "GBP", "color": "violet" }
]
},
"totalBudget": {
"name": "Total budget",
"type": "number"
},
"spentAmount": {
"name": "Spent amount",
"type": "number"
},
"remainingAmount": {
"name": "Remaining amount",
"type": "number"
},
"status": {
"name": "Status",
"type": "select",
"options": [
{ "value": "draft", "label": "Draft", "color": "zinc" },
{ "value": "active", "label": "Active", "color": "green" },
{ "value": "paused", "label": "Paused", "color": "amber" },
{ "value": "closed", "label": "Closed", "color": "blue" },
{ "value": "archived", "label": "Archived", "color": "stone" }
]
},
"categoryBudgets": {
"name": "Category budgets",
"description": "Optional JSON map of category to planned amount.",
"type": "json"
},
"notes": {
"name": "Notes",
"type": "text",
"variant": "long-text"
}
}
}Recommended defaults for agent-created budget rows:
periodType:monthlycurrency: use the workspace default or ask oncespentAmount:0remainingAmount: same value astotalBudgetstatus:active
2. Expense definition
Use handle expense.
Recommended fields:
{
"name": "Expense",
"description": "Tracks one spending event or charge.",
"fields": {
"title": {
"name": "Title",
"type": "text"
},
"description": {
"name": "Description",
"type": "text",
"variant": "long-text"
},
"amount": {
"name": "Amount",
"type": "number"
},
"currency": {
"name": "Currency",
"type": "select",
"options": [
{ "value": "CLP", "label": "CLP", "color": "red" },
{ "value": "USD", "label": "USD", "color": "green" },
{ "value": "EUR", "label": "EUR", "color": "blue" },
{ "value": "GBP", "label": "GBP", "color": "violet" }
]
},
"expenseDate": {
"name": "Expense date",
"type": "date"
},
"category": {
"name": "Category",
"type": "select",
"options": [
{ "value": "housing", "label": "Housing", "color": "stone" },
{ "value": "groceries", "label": "Groceries", "color": "green" },
{ "value": "transport", "label": "Transport", "color": "sky" },
{ "value": "utilities", "label": "Utilities", "color": "yellow" },
{ "value": "health", "label": "Health", "color": "red" },
{ "value": "education", "label": "Education", "color": "blue" },
{ "value": "shopping", "label": "Shopping", "color": "pink" },
{ "value": "dining", "label": "Dining", "color": "orange" },
{ "value": "entertainment", "label": "Entertainment", "color": "violet" },
{ "value": "travel", "label": "Travel", "color": "cyan" },
{ "value": "subscriptions", "label": "Subscriptions", "color": "indigo" },
{ "value": "debt", "label": "Debt", "color": "rose" },
{ "value": "taxes", "label": "Taxes", "color": "amber" },
{ "value": "gifts", "label": "Gifts", "color": "fuchsia" },
{ "value": "other", "label": "Other", "color": "zinc" }
]
},
"paymentMethod": {
"name": "Payment method",
"type": "select",
"options": [
{ "value": "cash", "label": "Cash", "color": "zinc" },
{ "value": "debit-card", "label": "Debit card", "color": "blue" },
{ "value": "credit-card", "label": "Credit card", "color": "violet" },
{ "value": "bank-transfer", "label": "Bank transfer", "color": "emerald" },
{ "value": "digital-wallet", "label": "Digital wallet", "color": "cyan" },
{ "value": "other", "label": "Other", "color": "stone" }
]
},
"status": {
"name": "Status",
"type": "select",
"options": [
{ "value": "pending", "label": "Pending", "color": "amber" },
{ "value": "cleared", "label": "Cleared", "color": "green" },
{ "value": "reimbursable", "label": "Reimbursable", "color": "blue" },
{ "value": "reimbursed", "label": "Reimbursed", "color": "cyan" },
{ "value": "cancelled", "label": "Cancelled", "color": "stone" }
]
},
"merchant": {
"name": "Merchant",
"type": "text"
},
"isRecurring": {
"name": "Is recurring",
"type": "boolean"
},
"recurringCadence": {
"name": "Recurring cadence",
"type": "select",
"options": [
{ "value": "one-time", "label": "One time", "color": "zinc" },
{ "value": "weekly", "label": "Weekly", "color": "sky" },
{ "value": "monthly", "label": "Monthly", "color": "blue" },
{ "value": "yearly", "label": "Yearly", "color": "emerald" }
]
},
"receipt": {
"name": "Receipt",
"type": "files"
},
"tags": {
"name": "Tags",
"type": "multi-select",
"options": [
{ "value": "personal", "label": "Personal", "color": "blue" },
{ "value": "business", "label": "Business", "color": "violet" },
{ "value": "family", "label": "Family", "color": "green" },
{ "value": "urgent", "label": "Urgent", "color": "red" }
]
},
"budgetId": {
"name": "Budget",
"type": "relationship",
"dataDefinitionId": "<budget-definition-id>"
}
}
}3. Example create requests
Create the definitions with POST /api/v1/experimental/data-definitions.
Example for budget:
curl -X POST "https://andiapi.com/api/v1/experimental/data-definitions" \
-H "Authorization: Bearer $ANDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Budget",
"description": "Tracks a planned spending period and its limits.",
"fields": {
"periodName": { "name": "Period name", "type": "text" },
"periodType": {
"name": "Period type",
"type": "select",
"options": [
{ "value": "monthly", "label": "Monthly", "color": "blue" },
{ "value": "custom", "label": "Custom", "color": "zinc" }
]
},
"startDate": { "name": "Start date", "type": "date" },
"endDate": { "name": "End date", "type": "date" },
"currency": {
"name": "Currency",
"type": "select",
"options": [
{ "value": "CLP", "label": "CLP", "color": "red" },
{ "value": "USD", "label": "USD", "color": "green" }
]
},
"totalBudget": { "name": "Total budget", "type": "number" },
"spentAmount": { "name": "Spent amount", "type": "number" },
"remainingAmount": { "name": "Remaining amount", "type": "number" },
"status": {
"name": "Status",
"type": "select",
"options": [
{ "value": "active", "label": "Active", "color": "green" },
{ "value": "closed", "label": "Closed", "color": "blue" }
]
}
}
}'Create sample expense rows with POST /api/v1/experimental/data-definitions/expense/data/upsert-many.
curl -X POST "https://andiapi.com/api/v1/experimental/data-definitions/expense/data/upsert-many" \
-H "Authorization: Bearer $ANDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"data": {
"title": "Supermarket",
"amount": 42500,
"currency": "CLP",
"expenseDate": "2026-03-12",
"category": "groceries",
"paymentMethod": "debit-card",
"status": "cleared",
"merchant": "Jumbo",
"isRecurring": false,
"recurringCadence": "one-time"
}
}
]
}'Step 2: Create the budget assistant agent
Create one workspace agent with handle expense-tracker-agent.
Recommended behavior:
- help users create a budget for a period
- update an existing budget when the user changes limits or dates
- add expenses one by one from natural language
- suggest the closest matching category when the user does not provide one
- warn when an expense would push the active budget over its limit
- keep
spentAmountandremainingAmounton the linked budget in sync after each expense change
Recommended agent payload:
{
"name": "Expense Tracker Agent",
"handle": "expense-tracker-agent",
"description": "Creates budgets, logs expenses, and keeps budget totals updated.",
"model": "gpt-5",
"capabilities": {
"webAccess": false,
"browserAccess": false,
"objectsAccess": true
},
"instructions": "You manage a workspace expense tracker. Use the budget and expense data definitions as the source of truth. When the user asks to create a budget, create one budget row with a clear periodName, startDate, endDate, currency, totalBudget, spentAmount, remainingAmount, and status. When the user adds an expense, create an expense row, attach it to the correct active budget when possible, then update the linked budget spentAmount and remainingAmount. If category is missing, infer the best category from the title or merchant and say which category you used. If currency is missing, use the active budget currency. Never create duplicate budgets for the same period unless the user explicitly asks for a separate budget. If multiple active budgets could match, ask one short clarifying question. If no budget exists, offer to create one first."
}Create the agent with POST /api/v1/experimental/agents.
Step 3: Create the mini app
Create one app with handle expense-tracker.
Mini app requirements:
- read budgets and expenses using
window.andibase.fetch - show the active budget summary at the top
- show total budget, spent amount, remaining amount, and percent used
- show recent expenses in a simple table
- group expenses by category and show totals
- keep styling simple and flat
Safer default access:
memberRole:adminpublicRole:nullpublicPermissions:null
If the user explicitly wants a public dashboard, use:
{
"publicPermissions": {
"data": ["read"]
}
}Example app code:
import { useEffect, useMemo, useState } from "react";
const currencyFormatters = {
CLP: new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP", maximumFractionDigits: 0 }),
USD: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }),
EUR: new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }),
GBP: new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" }),
};
function formatMoney(value, currency) {
const formatter = currencyFormatters[currency] ?? currencyFormatters.USD;
return formatter.format(Number(value || 0));
}
export default function App() {
const [budgets, setBudgets] = useState([]);
const [expenses, setExpenses] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
const [budgetResponse, expenseResponse] = await Promise.all([
window.andibase.fetch("/data-definitions/budget/query?page=1&pageSize=20"),
window.andibase.fetch("/data-definitions/expense/query?page=1&pageSize=100"),
]);
const [budgetPayload, expensePayload] = await Promise.all([
budgetResponse.json(),
expenseResponse.json(),
]);
if (cancelled) return;
setBudgets(budgetPayload.items ?? []);
setExpenses(expensePayload.items ?? []);
setLoading(false);
}
load().catch(() => setLoading(false));
return () => {
cancelled = true;
};
}, []);
const activeBudget = useMemo(
() => budgets.find((item) => item.data?.status === "active") ?? budgets[0] ?? null,
[budgets],
);
const currency = activeBudget?.data?.currency ?? "USD";
const totalBudget = Number(activeBudget?.data?.totalBudget ?? 0);
const spentAmount = Number(activeBudget?.data?.spentAmount ?? 0);
const remainingAmount = Number(activeBudget?.data?.remainingAmount ?? totalBudget - spentAmount);
const percentUsed = totalBudget > 0 ? Math.min(100, (spentAmount / totalBudget) * 100) : 0;
const categoryTotals = useMemo(() => {
const totals = new Map();
for (const item of expenses) {
const category = item.data?.category ?? "other";
const amount = Number(item.data?.amount ?? 0);
totals.set(category, (totals.get(category) ?? 0) + amount);
}
return Array.from(totals.entries()).sort((a, b) => b[1] - a[1]);
}, [expenses]);
if (loading) {
return <main style={{ minHeight: "100vh", padding: 24, fontFamily: "system-ui" }}>Loading...</main>;
}
return (
<main style={{ minHeight: "100vh", padding: 24, fontFamily: "system-ui" }}>
<h1>Expense tracker</h1>
<section>
<h2>{activeBudget?.data?.periodName ?? "Current budget"}</h2>
<p>Total: {formatMoney(totalBudget, currency)}</p>
<p>Spent: {formatMoney(spentAmount, currency)}</p>
<p>Remaining: {formatMoney(remainingAmount, currency)}</p>
<p>Used: {percentUsed.toFixed(1)}%</p>
</section>
<section>
<h2>By category</h2>
<ul>
{categoryTotals.map(([category, amount]) => (
<li key={category}>
{category}: {formatMoney(amount, currency)}
</li>
))}
</ul>
</section>
<section>
<h2>Recent expenses</h2>
<table>
<thead>
<tr>
<th align="left">Date</th>
<th align="left">Title</th>
<th align="left">Category</th>
<th align="right">Amount</th>
</tr>
</thead>
<tbody>
{expenses.map((item) => (
<tr key={item.id}>
<td>{item.data?.expenseDate ?? "-"}</td>
<td>{item.data?.title ?? "-"}</td>
<td>{item.data?.category ?? "-"}</td>
<td align="right">{formatMoney(item.data?.amount, item.data?.currency ?? currency)}</td>
</tr>
))}
</tbody>
</table>
</section>
</main>
);
}Create the app with POST /api/v1/experimental/apps.
Recommended acceptance checks
An agent should consider the recipe complete only if all of the following are true:
budgetexists and includes the required fields.expenseexists and includes category options plus thebudgetIdrelationship.expense-tracker-agentexists.expense-trackerexists.- At least one sample budget row can be created successfully.
- At least one sample expense row can be created successfully.
- The app loads and displays budget and expense data without runtime auth errors.
Minimal delivery summary
When the agent finishes, it should report:
- the created data definition handles
- the created agent handle
- the created app handle
- whether the app is member-only or public
- any assumptions made for default currency or budget period