Coffee Compass — Vision & Build PRD
A build-it-yourself product brief for an AI-assisted Fellow Aiden brew-profile companion.
Scope. This document is a build specification, not a supported software distribution. It provides the product model, requirements, sequencing, and implementation lessons needed to create your own version. Your resulting application, technology choices, security posture, deployment, and maintenance remain yours.
Version: 1.1 — complete. Covers the everyday tool, two-way device sync, profile materialization, self-grounding on your own profiles, and the Brew Log. Built and running. Audience: a Fellow Aiden owner who wants to build their own version, with their own AI coding agent, on whatever stack they prefer.
How to use this document
This is a product requirements + vision brief, not a codebase. Hand it to a capable AI coding agent (or read it and build by hand). It describes what the tool does and how the pieces fit, plus the hard-won facts about the Fellow Aiden integration that will save you days. It deliberately avoids prescribing a language, framework, database, or model: those are yours to choose. Where a decision matters, the brief explains the reasoning so you can adapt it to your needs rather than copy ours.
There is no repository and no shared code by design. The goal is for you to build something that fits your taste, your coffees, and your setup, grounded in your data, rather than inherit assumptions baked into someone else's build.
Why a PRD instead of a repository
This is a deliberate choice, not laziness about packaging.
Software is trending toward disposable and single-use. A great deal of what gets published as a repo is something almost no one else will actually run, because running someone else's build means trusting it. And trust is the real obstacle here: this tool holds your account credentials, talks to a reverse-engineered API, and can create and delete things on a physical device. Reasonable people are skeptical of letting an unfamiliar codebase touch any of that. No one wants a stranger's project taking down their home server, burning their API access, or quietly opening a backdoor.
A PRD sidesteps all of it. You hand this brief to an agent you already trust, in an environment you control, and it builds something you can read and own end to end. You adapt the data model to your coffees, swap the model for whatever you run, and decide your own safety posture. You get the design and the hard-won integration knowledge without importing anyone's dependencies, assumptions, or risk. The thing you end up with is yours, not a fork of ours.
If you build something good from this, that is the entire point. There is nothing to star and no one keeping score.
The problem
The Fellow Aiden brews wonderfully once a profile is dialed in, but dialing in is a craft, and the results are fragile:
- Profiles live on the device and the device holds only 14. Your dialed-in recipes get evicted to make room. There is no durable, personal home for them.
- Tuning is trial and error. People ask an AI for settings, type them in by hand, brew, adjust, repeat, with nothing remembering why a change was made or what your palate actually prefers.
- Knowledge is scattered. The coffee's origin and roaster notes, the brewing principles that should inform temperature and pulses, and your own taste history rarely sit in one place where they can inform the next cup.
The vision
A personal companion that:
- Keeps a library of the coffees you brew, with the details that matter for brewing.
- Generates and refines Aiden brew profiles with an AI model, grounded in real brewing principles and in a model of your taste that learns from your feedback.
- Acts as the durable home for your profiles, beyond the device's 14-slot limit, and reconciles cleanly with the machine in both directions.
- Stays personal and private: your data, your palate, your device. No accounts to create for anyone else, no telemetry, no publishing.
It is explicitly a single-user, personal tool. That keeps the design honest and small.
Core capabilities
Each capability below is described as an outcome, not an implementation.
1. Coffee library
Track the coffees you brew: name, roaster, origin, process, variety, roast level, flavor notes, a rating, and free notes. Support adding a coffee by pasting a product URL or a block of text and letting the AI extract the structured fields (never fabricating; leaving blanks when a fact is not present). Warn when a coffee name already exists.
2. AI profile generation, grounded
Generate a complete Aiden brew profile for a coffee. Two inputs make the output good rather than generic:
- A brewing-principles reference folded into the model's context: vetted, source-tagged guidance on how bloom, temperature, pulse count, and grind interact with roast level and origin. This keeps the model reasoning from real craft, not vibes.
- A taste model: a small set of preference dimensions (sweetness, body, acidity, brightness, fruit, chocolate, nutty, floral, fermentation, plus bitterness and astringency tolerances) on a 0–10 scale, supplied to the model so the profile leans toward what you like.
The model returns a structured profile (the exact field set is in "The Aiden profile" below) plus a short rationale. Validate every value against the device's allowed ranges before trusting it.
3. The iteration loop
Refining matters more than first-shot generation. Support:
- Current-profile-aware refinement: when a profile already exists, the model adjusts it rather than starting over.
- A "blind take": generate a profile without showing the model your existing recipe, so it reasons independently. This is a good way to check whether the AI is genuinely reasoning or merely echoing your seed data. (We found ours was echoing until we built this.)
- Inline editing of any field, with a "final" lock that marks a profile as settled.
- Conversational feedback that nudges the current profile ("a touch sweeter", "less astringent").
4. Brew logging and taste learning
After brewing, log the result with structured flags (too thin, too bitter, too astringent, too bold, perfect, etc.) and an optional rating. Each logged brew nudges the taste model by a small, bounded amount and records the evidence (the "why") so the learning is transparent and correctable. Keep this visible on a "taste model" page where the user can see every dimension, the evidence behind it, and edit any value directly. Small bounded nudges (for example ±0.25 on the 0–10 scale, clamped) beat large swings; the point is gentle drift toward truth, auditable at every step.
5. Push a profile to the Aiden + share
Send a generated profile to the device so it appears in the official app, ready to brew. Optionally generate a shareable link and QR code for it. (See "Fellow integration reality".)
6. Two-way sync and a durable archive
Pull every profile down from the account into a durable archive, and manage which of your custom profiles are on the device. This is where the 14-slot problem gets solved and where your library becomes the source of truth. The full model is in "Lifecycle and sync".
The Aiden profile (data contract)
A profile the device accepts has exactly these fields. Temperatures are in Celsius. This is the single most important and least obvious fact: the official app shows Fahrenheit, but the API speaks Celsius, on a half-degree grid. If you send Fahrenheit values they will be rejected (they fall outside the allowed range).
| Field | Type | Allowed values |
|---|---|---|
profileType |
int | 0 for a standard custom profile |
title |
string | up to 50 characters |
ratio |
number | 14 to 20, step 0.5 (coffee-to-water; advisory for some users) |
bloomEnabled |
bool | |
bloomRatio |
number | one of {1, 1.5, 2, 2.5, 3} |
bloomDuration |
int | 1 to 120 (seconds) |
bloomTemperature |
number | 50 to 99 °C, step 0.5 |
ssPulsesEnabled |
bool | single-serve |
ssPulsesNumber |
int | 1 to 10 |
ssPulsesInterval |
int | 5 to 60 (seconds) |
ssPulseTemperatures |
number[] | each 50–99 °C step 0.5; array length equals ssPulsesNumber |
batchPulsesEnabled |
bool | |
batchPulsesNumber |
int | 1 to 10 |
batchPulsesInterval |
int | 5 to 60 (seconds) |
batchPulseTemperatures |
number[] | each 50–99 °C step 0.5; array length equals batchPulsesNumber |
Conversion: C = (F − 32) × 5/9, then snap to the nearest 0.5 and clamp to [50, 99]. Example:
195 °F becomes 90.5 °C; 198 °F becomes 92.0 °C.
A note on the coffee-to-water ratio: keep it in the contract because most people want it, but be aware some users (including ours) ignore it and dose grounds by hand, because the machine will not let them dose as much coffee as they want. Treat it as advisory, not a bug.
Fellow integration reality (the part worth reading twice)
There is no official public API. There is a cloud API the mobile app uses, and the community has reverse-engineered it. You can talk to it directly or use a community client library. The behavior below is what we learned the hard way; expect it to drift as Fellow updates things.
Authentication. Log in with the account email and password and you get a bearer token (plus a refresh token). Treat these as secrets: keep credentials in environment configuration, never in code, and do not log the auth response — some community clients print it at debug level, tokens and all. Silence that.
Endpoints (shape, not a contract). Roughly: a login endpoint; a "devices" endpoint that returns your brewers; per-device "profiles" endpoints to list, create, and delete; and a per-profile "share" endpoint that returns a shareable link. The base is an AWS API Gateway host that community clients already know; rather than hardcode a URL that may rotate, discover it from a current community client.
Two traps that will break a naive client (these cost us real time):
- The device object no longer embeds the profile list. Older clients assume the device
record contains
profilesandschedulesarrays and crash on construction when those keys are absent. Fetch the profile list from the dedicated profiles endpoint instead, and read only the brewer id from the device record. - Delete validates against that same list. A delete-by-id call checks the id against the client's internal profile list before issuing the delete. If you left that list empty (because of trap 1), every delete fails with "profile does not exist." Populate the profile list from the endpoint and deletes work.
Sharing is server-side, and the QR is just the link. When you request a share for a
profile, the cloud returns a single thing: a brew.link URL (we confirmed the response payload
is literally { "link": "https://brew.link/p/…" }, nothing more). There is no separate QR image
from the API. The QR code you see is simply that URL encoded as a square: the brew.link page
and the Fellow app's share screen render one, and you can equally render your own from the link
to show in your tool or print on a coffee bag. Scanning it opens the profile in the Brew with
Aiden app. You create the profile on the account first, then request its link; you cannot
construct a brew.link URL offline from settings.
The device holds 14 profiles; the cloud holds more. This asymmetry is the whole reason the archive and the managed-sync model exist.
Profiles get a server-assigned id when created (and a new id if you delete and recreate). Do not treat that id as stable identity (see "Lifecycle and sync").
Conceptual data model
Described as entities and the fields that carry meaning. Use whatever storage you like.
- Coffee: identity + metadata (name, roaster, origin, process, variety, roast, flavor notes, rating, favorite/buy-again/subscribe flags, free notes).
- Brew Log: one chronological timeline per coffee, two entry types — a note (a tasting impression you write) and a dial-in event (auto-recorded when conversation feedback produces a new profile: "you said X → profile vN"). Recent entries are fed to the model as grounding, so your impressions and dial-in history carry into the next regen. This is the passive memory; the conversation is the live steering.
- Heuristics: per-user trigger → adjustment rules (e.g. "too thin → raise Pulse 2 by 1°F; then tighten spacing"), fed into every generation so feedback nudges reflect the user's own proven moves, not generic advice. Plus structural tendencies mined from the user's real profiles (their typical bloom temps by roast, bloom-ratio and pulse-count habits) stated as explicit guidance — so the model generalizes to how the user actually brews instead of a generic middle.
- Profile (generated): an immutable, versioned set of brew settings attached to a coffee, with the AI's rationale and a confidence band, and a "final" flag.
- Taste model: one per user; the preference dimensions and tolerances on 0–10.
- Taste evidence: append-only rows explaining each nudge (which brew, which flag, which direction), so the model is auditable.
- Brew session + feedback: what you brewed and how it turned out (flags + rating + notes).
- Archive snapshot: append-only, insert-on-change snapshots of every profile on the account (your customs, the vendor's built-ins, any seasonal "drops"), each with full settings and a content hash. This is the memorialization layer; it never deletes. Latest state for a profile is its most recent snapshot.
- Managed custom: the working record for each of your custom profiles, with the lifecycle fields below.
Lifecycle and sync (the heart of the design)
This is the most reusable idea in the whole project, and it is fully stack-agnostic. Treat the relationship between your library and the device as desired state plus an explicit apply, the same shape as infrastructure tooling's "plan / apply".
The coffee is the unit; everything is one set in three states. Resist the temptation to model "your coffees" and "the device profiles" as two separate worlds — users don't think that way. A coffee is either On device, in the Queue (staged to move), or in the Archive (off device, kept). "Not on the device" simply is "archived." The device view should show your whole coffee set, bucketed by state — not a separate list of device-only profiles. Link each coffee to its device profile with a stable id (not name-matching — coffees collide on origin words like "Peru"); a coffee with no device profile yet is just an archived coffee you haven't dialed for.
Two independent axes drive the buckets:
- Desired (you choose):
activemeans "I want this on the Aiden". Because the device holds 14, active is hard-capped at 14. The opposite isarchived: kept locally, not on the device. "Memorialized" and "deleted from the device" are the same end state, and nothing is ever lost locally. - Observed (sync sees):
on_device, true or false, learned by reading the device.
These are different things and should not be collapsed into one checkbox. One is a wish, the other is a fact.
The queue is the difference between them, derived live (never stored):
| on device | not on device | |
|---|---|---|
| active | On device — in sync | Queue: ADD — push on next sync |
| archived | Queue: REMOVE — delete on next sync | Archive — off device, kept |
Elevating a coffee from Archive makes it active → it shows as a queued ADD. Archiving an
on-device coffee makes it archived → a queued REMOVE; after apply it drops into the Archive.
A coffee with no device profile at all is in the Archive too — to elevate it you dial a profile
first (there's nothing to load otherwise).
Apply is manual and ordered. When the user syncs, process REMOVES before ADDS. Even though the desired set is capped at 14, the device may currently hold 14; removing first frees a slot so an add never transiently pushes the device to 15 and gets rejected. So the real-world swap is explicit: archive one (queues a remove), sync, the remove runs first, then the add.
Two stable links keep identity straight. (1) Each device profile points at its coffee by a stable id, so the device view can bucket the one coffee set — never match coffee↔profile by name (origin words like "Peru" collide and mis-link). (2) The server-assigned profile id is just a pointer to whatever is currently on the device: set it on push, cleared on remove. Because delete-then-recreate yields a new server id, keying identity on the server id would spawn duplicates; keying on your local ids (and relinking when a recreated profile reappears) does not. Expect a few device profiles that aren't in your coffee history yet — give them coffee cards so every profile has a home.
Safety boundaries:
- Apply mutates the device only on an explicit manual action. Confirm before deletes.
- A background poller, if you add one, observes only. It can notice that you deleted a profile directly on the machine, or that a new one appeared, and notify you. It never pushes or deletes on its own.
- The archive never deletes. A conflict (the device changed under you) can never cost data, because the archive already captured the device's truth. Surface conflicts for manual resolution rather than overwriting.
Suggested build order
We built it in this order and it worked well, because each phase ships value and de-risks the next:
- Library + AI generation + iteration loop + brew logging + taste learning. The everyday tool, before any device integration.
- Push a profile to the device + share link/QR. First contact with the Fellow API. Expect the Celsius conversion and the two client traps here.
- Pull + archive (memorialization). Read-only with respect to the device. This alone solves the 14-slot fear: your library outlives any eviction.
- Managed customs: active/archive, the derived queue, manual apply (with the remove-before- add ordering and the 14-cap guard). The two-way sync.
- A "generate → queue" bridge, so a freshly generated profile flows into the queue rather than being pushed immediately. One path, not two.
- (Planned) Grounding the AI in your real, dialed-in profiles, so refinement reasons from what is actually on your machine, not from seed approximations. The "refinement tour" section will document this once complete.
Roles you will need to choose
To keep this stack-agnostic, here are the roles, not our picks:
- A web UI (any framework) for the library, profile cards, taste model, and the device page.
- A datastore for the entities above (any relational store is plenty).
- An LLM for extraction, generation, and refinement. A capable local or hosted model both work; ground it well and validate its output against the data contract.
- A Fellow cloud client (community library or your own thin HTTP client) that handles the auth, the profile endpoints, and the two traps above.
- A QR generator if you want printable share codes.
Principles that kept the build honest
- Personal, private, no publishing. Your data and palate stay yours.
- The machine is never mutated silently. Manual apply, confirmed destructive actions.
- Memorialize, never lose. The archive is append-only.
- Make the AI's reasoning auditable. Show the evidence behind taste nudges; offer a blind take to expose echoing.
- Validate everything against the device contract before sending it.
- Ground the AI in real principles and real data, not vibes.
Prior art and resources
You do not have to reverse-engineer the Fellow cloud API from scratch. The community has already done much of it:
- A reverse-engineered Fellow Aiden client. A well-known community Python library handles the authentication, the device and profile endpoints, and the share link (search "fellow aiden" on PyPI or GitHub; the popular one also ships a small "brew studio" with AI-generated recipes). It is unofficial and community-maintained, so it can lag behind Fellow's changes: the two traps in "Fellow integration reality" above are exactly that kind of lag. Even if you write your own client, read this one as a reference for the API shape.
- An MCP server for the Aiden (
github.com/ravenintheforrest/aiden-mcp), plus agent-skill wrappers of the community client, for driving the brewer by natural language from an AI assistant. If natural-language device control is your goal, start from one of these rather than the raw API.
On referencing these at all: they are public community work, not our build. Pointing you to them is the opposite of exposing a private codebase; it is the fastest way to stand on what already exists and then go your own way. If you would rather not depend on a community client, the API shapes and the gotchas documented above are enough to write a thin client of your own.
The refinement tour (calibrating the AI to your palate)
Covers the calibration method, the baseline, the validated before/after of grounding, and a stricter "names withheld" integrity check.
The roaster's published profile is generic by necessity. The whole value of a personal tool is dialing in to your taste, and if you are an established brewer you already hold the answer key: your own dialed-in profiles. This section is a repeatable way to teach the model your own approach and verify it, useful even if you do not do this kind of work for a living.
Two stages, on purpose:
- Known / named calibration (names can bleed in). Generate a profile while giving the model the coffee's name and attributes. Be aware that the name, and any reference profiles you feed the model, can bleed into its reasoning: it may lean on what it already "knows" about a well-known coffee rather than reason purely from character. That is fine for a first pass; it tends to produce a sensible, close, but generic result. You compare that result against your real device settings to see the gap.
- Blind refinement (force independent reasoning). Withhold the dialed-in recipe (and, for a stricter test, the name too), leaving only character: origin, process, roast, tasting notes, plus your taste model. This removes the bleed and tests whether the model has actually learned your taste rather than recognized a coffee. The gap closing here is the real proof.
Baseline finding (stage 1, recipe withheld, name given), against three dialed-in profiles:
- The profile structure was already correct: the model reliably produced an Aiden-shaped profile (a sensible ratio, a ~2.5 bloom, a descending multi-pulse temperature curve in the brewing range). On one coffee the batch pulse curve matched the user's exactly.
- Two systematic gaps emerged. Bloom temperature ran hot: the model defaulted to the middle of the range while the user blooms cooler, and cooler still on darker or natural coffees. And it missed per-coffee intensity: where the user had tuned one coffee to a hotter peak and an extra pulse, the model produced its generic curve instead.
- In short, the model produced a competent house profile, not yet the user's own style.
What grounding changed (validated). We then supplied the user's real dialed-in profiles as worked examples in the generation context, excluding the coffee being profiled so a blind take stays honest, and re-ran the same three coffees:
- The bloom-runs-hot bias corrected. Bloom temperatures dropped into the user's real range. One coffee's bloom went from 7°F too hot to within 4°F and its bloom time started matching exactly; another landed precisely on the user's value.
- Per-coffee intensity was captured. The coffee the user had tuned to a hotter, longer five-pulse batch (which the baseline rendered as a generic four-pulse) came back as a five-pulse batch. The model had no way to know that before; it learned it from the examples.
- A new nuance appeared, worth stating plainly. Grounding teaches the central tendency of how the user brews, so a coffee that bucks the trend (one the user actually blooms warmer than their average) got pulled slightly toward the mean and overcorrected cooler. The lesson: grounding produces a markedly better starting point, not a perfect clone. The iteration loop (nudge it back via feedback) closes the last gap on atypical coffees.
So the arc is: the profile structure was free; calibration against your own profiles fixes the systematic bias and captures your per-coffee patterns; and the feedback loop handles the outliers. That is the whole point of a personal tool, and it only works because your dialed-in profiles are the answer key.
Hard mode: names withheld (the integrity check). To confirm the model was reasoning from character and the user's own taste rather than recognizing a known coffee, we re-ran with the coffee name and roaster removed entirely (only origin, process, roast, and tasting notes given), the same coffee still excluded from the examples, and the seed library reference turned off so the only profile signal was the user's own dialed-in profiles. It held up: outputs still landed in the user's real neighborhood (bloom within a few degrees, correct structure). On the character-rich coffee it actually got closer with no name at all. Two honest caveats surfaced: where character was thin (a vague blend), the model leaned hardest on the user's central tendency and drifted from that coffee's atypical setting; and where the name had been carrying a real decision (one coffee's extra pulse), removing it lost that detail. The practical guidance: give the name in normal use — identity is legitimate signal — and let grounding plus character do the rest. The nameless run is a validation tool, not the recommended mode.
Second case study: low-feedback coffees vs the user's curated profiles. We later ran the same blind comparison on six coffees the user had only lightly refined (and for which the user supplied a curated "best-known" profile each, with a self-assigned confidence tag). The result sharpened the picture: on a conventional coffee the grounded model nailed the bloom and came within a degree or two on pulses; but on coffees where the user makes unusual choices it diverged predictably — it stayed near its ~188-190°F bloom comfort zone where the user blooms much cooler (176-184°F), it preferred four batch pulses where the user runs five, and it could not reproduce an idiosyncratic ascending pulse ramp the user had dialed by hand. The lesson: grounding reliably lands the well-trodden coffees, but a user's hard-won, atypical choices on lightly-explored coffees are theirs — the model gives a sensible generic, not a clone. This is exactly why the user's own stored profiles (and a confidence tag noting how much a profile was refined) matter: trust the user's value over the model's where the two disagree on a low-confidence, idiosyncratic coffee.
The takeaway for a builder: do not judge the AI only on first-shot generation. Build the calibration loop (generate, compare to your real settings, feed your reals back as examples, re-compare). The structure comes for free; your palate is what you are teaching.
Scaling the examples. Feed the model the dialed-in profiles most similar to the coffee being profiled first (rank by shared origin, process, roast, and tasting-note terms), and only truncate the list once the library is large. Two cautions learned the hard way: keep the cap generous, because a coffee with no close precedent still benefits from the full breadth of your experience, and do not over-read a single generation, because a model run at a low temperature still wobbles a few degrees run to run. You are aiming the starting point into the right neighborhood, not hitting an exact number on the first try.
Getting smarter over time (without fine-tuning)
The goal is to stop reaching for an outside model to get good profiles. The path is layered, and none of it is weight training:
- Grounding self-improves as the library grows (passive, biggest lever). Every coffee the user dials adds a real worked example; similarity-ranking surfaces the closest ones. More of the user's actual profiles = less drift to the generic middle.
- Encode measured tendencies as explicit guidance (active). Mine the user's real profiles for their structural habits (bloom temp by roast, bloom-ratio and pulse-count preferences) and state them as rules in the principles. This corrects systematic biases the examples alone don't (the model averaging to a median instead of, say, cooling the bloom for sweet-protection).
- The Brew Log feedback loop (compounding, per-coffee). Tasting notes and dial-in events accumulate and ground future regens; structured brew feedback nudges the taste model.
A measured caveat: idiosyncratic, hand-built structures (an unusual ascending pulse ramp) the model will not invent — those stay the user's stored values. Trust the user over the model where they disagree on a low-confidence, atypical coffee. Fine-tuning is not worth it at personal scale; in-context grounding plus encoded tendencies captures the value with none of the cost.
Working with your AI agent (starter prompts)
This brief is meant to be handed to an AI coding agent, not read like a manual. Below is a sequence of starter prompts that mirrors the suggested build order. Paste the whole brief into your agent first, then drive it phase by phase. Adapt the bracketed parts to your stack and your coffees. These are a starting point, not a script.
0. Orient the agent and choose your stack.
I'm giving you a product brief for a personal Fellow Aiden brew-profile tool. I want to build my own version for myself. My environment is [language / framework / database / model]. Read the whole brief, then propose an architecture and a phase-1 plan that follows the suggested build order. Flag anywhere my stack changes the brief's assumptions, and ask me anything you need before you write code.
1. The everyday tool first (library, generation, iteration, logging).
Build phase 1 from the brief, with no device integration yet: the coffee library and the conceptual data model, the add-a-coffee flow (paste a product URL or text and extract the structured fields, never fabricating, leaving blanks when a fact is absent), AI profile generation, the iteration loop, and brew logging. Generate the schema and the core screens.
2. Ground the model in real principles and a taste model.
Wire in two grounding inputs from the brief: a brewing-principles reference, and a taste model with the preference dimensions and tolerances on a 0–10 scale. Generate a complete profile plus a short rationale, and validate every value against the device data contract (Celsius, the half-degree grid, the allowed ranges) before trusting the output.
3. Teach it what I actually like, from my own dialed-in profiles.
Here are [N] of my dialed-in profiles [paste them]. Build my taste model from them and mine my structural tendencies — typical bloom temperature by roast level, bloom-ratio and pulse-count habits — and state those as explicit guidance in the generation context. When you generate for a coffee, feed the most similar of my profiles first (rank by shared origin, process, roast, and tasting-note terms).
4. Prove it learned, the blind way.
Add a "blind take": generate a profile without showing the model my existing recipe, then show me the gap against my real settings. Then run a leave-one-out across all my profiles — hide each coffee's real recipe, predict it from character plus my taste model and my other profiles, and report how close it lands. I want to see whether it's actually reasoning from my taste and my real profiles, or just echoing my seed data.
5. Connect to the Aiden (mind the traps).
Implement the Fellow cloud client per the "Fellow integration reality" section. Watch the two traps: fetch the profile list from the dedicated profiles endpoint (the device object no longer embeds it), and populate the client's internal profile list before any delete or deletes fail. Keep credentials in environment config, never in code, and do not log the auth response. Push a profile to the device and request its share link / QR.
6. Solve the 14-slot problem, then sync both ways.
Implement pull + an append-only archive first (read-only with respect to the device — this alone outlives any eviction). Then build the managed sync from the brief: desired state (active vs archived, capped at 14) versus observed state (on-device or not), the queue derived live as the difference, and a manual, confirmed apply that processes removes before adds.
A few cautions worth pasting when relevant:
Don't over-read a single generation — even a low-temperature run wobbles a few degrees run to run; you're aiming the starting point into the right neighborhood, not hitting an exact number. And where the model and one of my hard-won, atypical profiles disagree on a low-confidence coffee, trust my stored value over the model's.
This brief documents the project through two-way device sync, the calibration method, grounding, the Brew Log, and how the model improves over time.
