andibase

Expense Tracker

Agent-ready recipe for creating an expense tracker in andibase

Open Markdown

This recipe tells an agent exactly how to build an expense tracker in andibase.

Goal

Create three things in one workspace:

  1. A budget data definition.
  2. An expense data definition.
  3. One agent that helps users manage budgets and log expenses.
  4. One mini app that shows budget progress and recent expenses.

Expected outcome

After following this recipe, the workspace should have:

  • a budget definition for monthly or custom budgets
  • an expense definition for individual spending entries
  • an expense-tracker-agent agent
  • an expense-tracker mini app

Use the API documented in:

Agent instructions

When an agent executes this recipe, it should follow these rules:

  1. Create the budget definition first.
  2. Create the expense definition second.
  3. After the budget definition exists, wire the expense.budgetId relationship to the created budget definition id.
  4. Create the expense-tracker agent with instructions focused on budget management and expense logging.
  5. Create the mini app last.
  6. Do not invent custom fields outside this recipe unless the user explicitly asks for them.
  7. 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: monthly
  • currency: use the workspace default or ask once
  • spentAmount: 0
  • remainingAmount: same value as totalBudget
  • status: 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 spentAmount and remainingAmount on 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: admin
  • publicRole: null
  • publicPermissions: 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.

An agent should consider the recipe complete only if all of the following are true:

  1. budget exists and includes the required fields.
  2. expense exists and includes category options plus the budgetId relationship.
  3. expense-tracker-agent exists.
  4. expense-tracker exists.
  5. At least one sample budget row can be created successfully.
  6. At least one sample expense row can be created successfully.
  7. 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

On this page