Featured
MyAssistant - household operations platform
The app my household runs on, and the .NET 10 system I built solo to power it.
The problem
A shared household runs on a hundred small obligations. Whose turn is it to take out the bins. Did anyone pay the internet bill. What's left in the grocery budget this month. Why is there a $40 charge on the bank statement nobody recognizes.
Normally this stuff is scattered across group chats, a fridge whiteboard, two or three half-used apps, and whoever happens to remember. Splitting a bill turns into mental arithmetic. "Whose turn" turns into a quiet argument. I wanted one shared place that holds all of it, where the money math and the rotation logic are handled for you instead of relitigated every week.
Show details ▾Hide details ▴
What it does
It puts a household's day-to-day in one shared place. People in the same household get recurring tasks with a rotation that tracks whose turn is next, bills with line items and percentage splits that settle themselves, budgets funded from a personal wallet through a running ledger, and shopping lists with an in-store mode that groups items by aisle for the actual trip.
The feature people react to most: you upload a bank statement PDF and the app reads it. It pulls out the account, the statement period, every transaction, and a category for each one, then checks its own arithmetic before saving anything. Receipts work the same way from a photo.
Everything shared updates live across devices. There's a notification feed, an activity timeline, system-wide search, five switchable dashboards, an admin console, and a public demo anyone can open without signing up.
Under the hood
I built it as a .NET 10 Clean Architecture solution where dependencies only ever point inward. The Domain layer has no idea EF Core or ASP.NET exist, and that's enforced by the project references, not by good intentions. Application sits on top with CQRS handlers, Infrastructure implements the interfaces, and the API stays thin.
A few decisions hold the whole thing together:
- Expected failures return a Result<T> instead of throwing. A missing bill comes back as NotFound, a broken business rule as Invalid, and a controller extension maps each state to the right HTTP status. Exceptions are reserved for the genuinely unexpected.
- Only aggregate roots get repositories, and the rule is a generic type constraint, so asking for a repository over a child entity won't compile.
- Query logic lives in 173 named specification classes, never in the repositories or the handlers. BillByIdSpec knows how to load a bill with its lines and splits; the repository just runs it.
- Domain events are recorded inside entities as they change, but they fire only after the database commit succeeds, dispatched by an EF SaveChanges interceptor. A failed save fires nothing.
- Cross-cutting concerns are wired exactly once. Audit logging, the soft-delete query filter, exception capture, and validation all live in shared infrastructure, so every new feature inherits them for free.
Realtime is convention-driven. A startup scan registers every aggregate root, and the SignalR hub authorizes a subscription by calling the same service method the REST endpoint uses, so the two can't drift apart. Plain CRUD broadcasts a change event automatically from that same post-commit interceptor, and a push failure can never roll back a write.
Budgets don't store a balance anywhere. Every figure is folded on demand from immutable ledger entries, so the history is the source of truth and a correction is just another entry. The statement importer validates before it persists: it hashes the file, checks two unique indexes to reject duplicate uploads, and only then spends money on the LLM call.
The AI layer
This is the part I'd point an AI engineer at. Five features are backed by OpenAI: bank-statement extraction, receipt parsing from a photo, free-text recipe parsing, grouping a shopping list by store aisle, and an admin tool that explains production exceptions in plain language. The plumbing around them matters more than the prompts.
- Structured output is guaranteed, not hoped for. A small generator reflects over a C# record and emits a strict JSON Schema, which goes to OpenAI in strict mode, so the bank-statement response deserializes into a typed object every time.
- Every call goes through one gateway that records the model, token counts, estimated dollar cost, latency, status, and a preview of the prompt and response. Admins can audit exactly what the app asked and what it cost.
- The demo account can't drain my OpenAI budget. The gateway counts a demo user's calls since the last reset and, once they hit the cap, throws before the HTTP call ever fires; a MediatR pipeline behavior turns that into a clean 400 instead of a 500.
- The receipt reader checks itself. It asks the model for line items, verifies they sum to the printed total, and when they don't, it sends the discrepancy back and asks again, up to three attempts.
Built solo, by the numbers
I designed and wrote every layer myself, browser through database:
- 7-project Clean Architecture solution, roughly 118,000 lines of C# (about 68,000 once you exclude generated migrations)
- 30 REST controllers exposing 208 endpoints
- 203 CQRS handlers guarded by 94 validators
- 173 query specifications, 36 domain entities, 58 domain events
- 13 background services handling funding, reminders, alerts, and file cleanup
- 284 Blazor WebAssembly components and pages
- 1,154 unit tests
- 5 OAuth providers and 5 AI-powered features
A representative problem
One bug is worth telling because it shows the kind of thing that eats real days. Completing a shared task started returning 500, but only sometimes, and only when a second person completed it. The completion itself had already saved. The cause sat three layers away: a child entity assigns its own Guid in its constructor, EF Core read that non-empty Guid as proof the row already existed, and when a notification appended a new actor it issued an UPDATE against a row that was never there. That threw inside a domain-event handler, poisoned the request's shared database context, and surfaced as a 500 on a completely unrelated endpoint. The fix is one line per child configuration. Finding it meant tracing a failure backward through the event dispatcher, the change tracker, and the request scope, then writing it up as a standing rule so the next entity can't repeat it.
Tech stack
- Backend: .NET 10, ASP.NET Core, EF Core 10 on SQL Server, MediatR, FluentValidation, Ardalis.Result and Ardalis.Specification, Mapperly for compile-time mapping, Scrutor for assembly scanning.
- Realtime and auth: SignalR, ASP.NET Identity with JWT, and OAuth through Google, Microsoft, Apple, Facebook, and GitHub.
- Frontend: Blazor WebAssembly with MudBlazor.
- AI and documents: OpenAI over a resilient HttpClient, PdfPig for statement text, Magick.NET for images.
- Other: RestSharp for Mailgun email and currency rates, WebPush for browser notifications, Swagger, and xUnit with Moq for tests.
Results
It's live, and anyone can use it. You can register an account, or open the demo with one click and walk through tasks, bills, budgets, shopping, and a sample bank import without signing up. The demo resets itself, so explore freely. My own household uses it as our daily driver for chores, shared bills, and the grocery run, which is the test I care about most: software my family relies on every day rather than a screenshot in a repo.
The button below opens the landing page, where "Try the demo" gets you straight in.