How it was built

Agents that call tools, and code that keeps them honest.

FuelWell helps an adult child cook familiar Chinese, Korean and Vietnamese meals for a parent with kidney disease. Behind two small features sit two Claude agents with real tool loops, and the whole product was built by an orchestrated team of Claude Code subagents.

A student project. All data is sample data, and nothing here is medical advice.

2 tool-calling agents Code-enforced red flags Rules fallback, never a blank error Custom Claude Code skill

01 / Runtime AI agents

Two agents, one hardened pattern

Each feature is a Supabase Edge Function that runs a Claude tool-use loop. The model plans and writes; code looks things up, does the maths, enforces safety and validates the final JSON. If anything fails, the same endpoint answers from a deterministic rules engine.

Recipe adapter tool loop The dashboard sends a dish to the recipe-adapter Edge Function, which checks the JWT, validates input and counts the daily quota. Claude Sonnet 5.5 calls lookup_nutrients and compute_totals, which run as code, then submits. Code validators accept the result or send it back once for repair. Validated JSON returns to the UI. If the key is missing, the API errors, or output stays invalid, a rules engine answers instead. Caregiver Meals tab Edge Function verify JWT (signed in) validate input (≤ 600 chars) daily quota, atomic SQL timeouts + capped tokens Claude Sonnet 5.5 plans swaps, writes copy ≤ 8 turns · 45 s budget Tools (plain code) lookup_nutrients compute_totals 88 foods, USDA FDC levels from thresholds Code validators submit_adaptation every food in the table totals recomputed by code sodium may not rise JSON → UI source: 'ai' Rules fallback: dish library + swap map no key, error, timeout or invalid → source: 'rules' tool_use tool_result invalid: is_error, one repair
Agent 1, recipe-adapter. Input: a family dish such as "my mom's doenjang jjigae". Output: a kidney-friendlier version with per-ingredient K/P/Na, before and after totals, low/mid/high levels, swaps and tips.
Caregiver question triage tool loop A caregiver question goes to the inquiry-triage Edge Function with the same auth, validation and quota checks. Claude calls check_red_flags, search_care_faq and get_open_slots, then submits a triage. Code validates the category, the slots and the wording, and code re-runs the red-flag rules on the raw message and overrides urgency if any match. If the model path fails, keyword rules produce the triage. Caregiver Care team tab Edge Function verify JWT (signed in) validate input (≤ 600 chars) daily quota, atomic SQL timeouts + capped tokens Claude Sonnet 5.5 classifies, drafts reply ≤ 6 turns · 40 s budget Tools (plain code) check_red_flags search_care_faq get_open_slots regex rules · BM25 · calendar Code validators submit_triage slots must be offered no dose instructions red flags override urgency JSON → UI + step log Rules fallback: keywords + FAQ + slots red-flag check still runs → source: 'rules' tool_use tool_result invalid: is_error, one repair
Agent 2, inquiry-triage. Input: a caregiver's message. Output: category, urgency, red flag and escalation, a draft reply for staff to review, up to three bookable dietitian slots, and a step-by-step log the UI shows as "How the agent handled this".

Red flags live in code

Chest pain, trouble breathing, confusion, no urine, severe swelling, muscle weakness, irregular heartbeat and fainting are regex rules. The model can call them, but the function also runs them on the raw message and forces urgency: 'urgent', red_flag: true and a call-911-or-care-team message, whatever the model said. The rules are deliberately over-inclusive.

The model never does maths

Language models are unreliable at arithmetic, and a wrong sodium total is worse than none. Claude chooses ingredients and grams; compute_totals multiplies USDA values and code assigns low/mid/high from documented per-meal thresholds. Final totals are always recomputed from the submitted ingredients.

Never a blank error

No API key, an API error, a timeout, a refusal or output that fails validation twice all lead to the same place: a deterministic rules answer with source: 'rules'. Quota storage being down also routes to rules rather than to the paid model.

Same shape as leasing-inquiry triage and tour booking

Swap "caregiver" for "prospective tenant" and the triage agent is a leasing assistant: classify the inquiry (pricing, availability, maintenance, emergency), retrieve the property FAQ, enforce hard rules in code (a gas leak or flooding escalates no matter what the model thinks), offer real tour slots from a calendar tool, and hand staff a validated draft plus an audit trail of tool calls. The pieces that make it production-safe carry over: auth at the edge, per-user quotas, input limits, strict tool schemas, validated structured output, and a fallback path.

Hardening checklist

  • verify_jwt on, plus a server-side user lookup that rejects the anon key and anonymous users.
  • Input: JSON only, 8 KB body cap, 600-character text cap, enum checks, control characters stripped.
  • Quota: ai_usage table with RLS and no client policies, written by an atomic INSERT … ON CONFLICT DO UPDATE … WHERE calls < limit. 40 calls a day per user, 300 for the shared demo account.
  • Model calls: capped max_tokens, per-request timeout from a wall-clock budget, one SDK retry, turn cap, server-side refusal fallback.
  • Strict tool schemas (strict: true), then semantic validation in code, with one repair round.
  • User text is wrapped as data, and the prompts tell the model to ignore instructions inside it.

Where the knowledge comes from

  • 88 ingredients common in Chinese, Korean and Vietnamese home cooking, in mg per 100 g from USDA FoodData Central (SR Legacy / FNDDS), with FDC IDs. Approximations are marked as such.
  • Per-meal thresholds (K < 400 / ≤ 700, P < 200 / ≤ 300, Na < 400 / ≤ 700 mg) derived from NKF and KDOQI 2020 daily ranges and labelled as demo heuristics.
  • 15 plain-language FAQ entries on diet, scheduling, insurance, labs and the caregiver role.
  • One shared file, renal-data.mjs, feeds both the Edge Function and the developer skill, so they cannot drift.

02 / Built with Claude Code

An orchestrator and six parallel subagents

One orchestrating Claude Code session planned the work, wrote the shared contracts, and fanned out research and build tasks to subagents that ran in parallel. A human owner made the product calls and reviewed the result.

  1. Research: three subagents in parallel

    Competitor audit

    Visual and UX audit of DaVita, Fresenius, Nourish, Fay, Season, Foodsmart, Kidney Ally and NKF.

    People and access

    Accessibility for older readers, the adult-child caregiver role, and auth constraints.

    Codebase and fit

    Audit of the existing app, plus what the work should demonstrate for an AI-agent engineering role.

  2. Contracts before code

    The orchestrator wrote two contracts that every builder had to honour: the design-system tokens and components in styles.css, and a typed AI client (ai-client.js) whose JSDoc return shapes are exactly what the Edge Functions return and the dashboard renders. That let the UI and the agents be built at the same time without waiting on each other.

  3. Build: three subagents with strict file ownership

    Sign-in and public site

    Landing pages, sign-in, and a one-click demo login.

    Owns public pages and auth UI.

    Tabbed dashboard

    Overview, meals and care-team tabs built on the AI client contract.

    Owns dashboard pages, scripts and sample data.

    AI functions and docs

    Both Edge Functions, the quota migration, the custom skill, this page and the README.

    Owns supabase/, the skill and docs.

    Each agent had an explicit allow-list of files, could not commit or deploy, and had to pass a build check. Nobody edited anyone else's files, so there were no merge conflicts to untangle.

  4. Integration and review

    The orchestrator merged the work, ran the build, reviewed the diffs against the contracts, applied the database migration and deployed the functions, then checked the live demo end to end.

MCP tool integrations

  • Supabase MCP: inspect the project and tables, apply migrations, deploy Edge Functions, read logs and security advisors.
  • GitHub: repository, branches and pull requests.

Skills used

  • claude-api: current model IDs, the tool-use loop, strict tools and refusal fallbacks.
  • supabase: Edge Function and RLS conventions and the security checklist.
  • artifact-design: page design guidance.

Custom skill: renal-recipe-check

Triggers whenever a developer adds or edits meal data. It runs a Node script against the same nutrient table and thresholds the agent uses, flags unrealistic values, badge mismatches, unknown ingredients and missing disclaimers, and suggests culturally faithful swaps.

$ node .claude/skills/renal-recipe-check/scripts/check-meals.mjs --dish "kimchi jjigae"

== dish library: kimchi jjigae
  - Kimchi jjigae with rice (kimchi jjigae) K 965 · P 368 · Na 1780 mg (computed) [K high / P high / Na high]
      swap  kimchi -> kimchi, rinsed: A quick rinse and a slightly smaller amount keep the
            sour kimchi taste with less salt.
      swap  anchovy stock -> homemade low-sodium stock: Same long-simmered base, made without
            added salt.
      swap  gochujang -> 50% of the amount: Half the paste with a pinch of gochugaru keeps
            colour and heat with less sodium.

Summary: 1 meals, 0 errors, 0 warnings. Table: 88 ingredients (USDA FDC).
The skill's script, run on one of the dish-library entries the rules fallback also uses.

03 / Design system

Emerald, rendered live

Everything below is drawn from the real styles.css on this page, not screenshots. Pages build on the same layers, the pattern of a reusable dashboard template.

  • Tokens
  • Base
  • Components
  • Page styles

Color tokens

Primary
--primary
Primary strong
--primary-strong
Primary soft
--primary-soft
Accent (main buttons)
--accent
Accent soft
--accent-soft
Ground
--ground
Ink
--ink
Ink 3 (min text)
--ink-3
Low
--ok
Mid
--mid
High
--high
Focus ring
--focus

Type scale

--text-3xlFamiliar food
--text-2xlThoughtful care
--text-xlDoenjang jjigae
--text-base18px body in Plus Jakarta Sans
--text-sm16px floor: nothing smaller

Components

ChineseKoreanVietnamese
Potassium low Phosphorus mid Sodium high

Sample data. Nutrient status always pairs color with a text label.

04 / Research → decisions

What we saw, and what we changed

Findings from the research subagents' audit of kidney-nutrition and dietitian-booking products, and the product decision each one drove.

Finding

Most kidney-diet tools speak to the patient. In immigrant families, the adult child often researches, translates and cooks.

Decision

The adult child is the primary user. Copy, the triage agent's replies and the recipe tips are written to the caregiver about their parent.

Finding

Large provider sites (DaVita, Fresenius) carry deep recipe and education libraries in long, scrolling pages that are hard to scan.

Decision

Tabs instead of long scroll: the dashboard and this page use one accessible tablist with focused panels.

Finding

NKF's "instead of this, try this" swap lists are the most usable advice format we saw: concrete and easy to act on.

Decision

NKF-style swap tables. The recipe agent returns from → to → why rows and keeps the dish recognisable.

Finding

Kidney Ally shows potassium, phosphorus and sodium as simple bars, which reads faster than tables of milligrams.

Decision

Kidney Ally-style nutrient bars with low/mid/high text labels, never color alone, set by code-owned thresholds.

Finding

Dietitian services (Nourish, Fay, Season, Foodsmart) lead with booking and insurance, but say little about family food traditions.

Decision

Booking lives inside the question flow: the triage agent offers real slots when they help, and cultural dishes are first-class.

Finding

Supabase's default email service only delivers to project team members, so outside reviewers could never confirm an account.

Decision

A pre-confirmed shared demo login with sample data only, and its own larger AI quota so reviewers do not hit limits.

Finding

Many readers are older or reading in a second language; small grey text and color-only status fail them.

Decision

18px body text with a 16px floor, AA contrast on every text pair, and 48px touch targets. These rules carried over when the October redesign moved from Atkinson Hyperlegible and Clay & Teal to Plus Jakarta Sans and Emerald.