Inspiration
We're a small AI-native studio. Our organization had one shared workspace, but somehow our calendars in the same timezone never agreed, and a standing habit of letting good conversations die in the gap between "this was great, let's find time" and actually finding time.
The internal stuff was annoying. The external stuff was expensive. A buyer would reply asking for Tuesday or Wednesday, that mail would sit for two days behind whatever we were working on, and by the time one of us answered, the thread had gone cold. Nobody was dropping the ball out of carelessness. There just wasn't a person whose job it was to catch it.
We didn't want a calendar app. We have calendars. We wanted the part a good assistant actually does: read the inbox, know what's already on the books, know what we said last time, propose something real, and write the follow-up while the meeting is still fresh. Assist.me is our attempt to build that for a studio too small to hire one.
What it does
Assist.me works the shared inbox and the calendar together.
Someone emails asking to meet. A small model reads it and decides whether it's a
scheduling mail at all, and if so, which open request it belongs to. If it
matches, the agent picks up the existing thread instead of starting a new one.
It checks real availability through a rules engine that knows working hours,
buffers, protected blocks, and timezones, then proposes times that actually
exist. Every ask becomes a tracked request that moves through
proposing → awaiting_reply → confirmed → rescheduling, so nothing lives only
in someone's head.
The day before a meeting it drafts an agenda from the thread and our notes.
Afterwards it drafts the follow-up. Before a call with someone outside the
studio, the prep skill briefs us: what we already know from the request, the
thread and Drive, plus what's changed on their side, pulled from the web through
Tavily. That last part matters more than we expected. Knowing someone was
promoted last month changes how you walk into a room.
Two rules the agent cannot talk its way around. It never invents availability,
because availability isn't its to decide. And nothing leaves the studio without
a human pressing approve: send_email only ever queues an approval, and the
live send is a separate path a person triggers.
How we built it
TypeScript and Next.js on Vercel, Postgres for state, Google Calendar, Gmail and Drive for the real work, and NVIDIA Nemotron on Nebius Token Factory for the thinking.
The model layer is tiered on purpose, because the jobs are genuinely different sizes:
- Nemotron 3 Nano triages inbound mail. Bounded question, fixed JSON answer, runs on everything that hits the label.
- Nemotron 3 Super is the agent: a 12-step tool loop over calendar, Gmail, Drive, research and durable memory, plus agendas, follow-ups and nightly memory consolidation.
- Nemotron 3 Ultra is gated. It only wakes up when the rules engine finds no clean slot for two or more people, and even then it proposes human tradeoffs (drop an attendee, move a protected block, widen the window) rather than conjuring a time.
The design principle underneath all of it: the model does language and orchestration, never arithmetic we can compute ourselves. Conflicts come from a deterministic rules engine. The agent calls a tool and gets an answer; it is never asked to reason about whether 2pm is free.
Skills are YAML, not TypeScript. schedule, prep, followup, track_project
and stakeholder_update each live in config/skills/ as a file with trigger
phrases and instructions. Adding a skill is a file, not a deploy-shaped code
change, which has made us far more willing to try things.
Durable memory is Postgres tables for projects, people, deliverables and facts,
consolidated nightly, with a dump_memory tool that writes the whole thing to
Markdown files we own. If this project disappears tomorrow, our studio's memory
is still sitting in a folder.
There's a DEMO_MODE=1 that swaps Google, Postgres, Slack and auth for
in-memory doubles with a seeded fictional production, so the whole thing runs
with nothing but a Nebius key.
Challenges we ran into
The port looked broken when it wasn't. Our first day on Nemotron, every call
came back as an empty assistant turn and the tool loop just stopped. The models
return their answer in reasoning_content with content: null when thinking is
on, and standard OpenAI-compatible clients read content. Nothing was wrong
with the output; we simply weren't looking where it was. The fix is about sixty
lines in one file. Finding it cost most of a day.
Our agent booked meetings thirteen months in the past. This is the one that actually scared us. Ask it for "30 minutes next Tuesday afternoon" and it would confidently confirm a slot in August 2025, write the calendar event, and mark the request confirmed. The model wasn't malfunctioning. Nothing in our prompt ever told it what today was, so it resolved "next Tuesday" against its training cutoff, and our rules engine then dutifully validated the window it had invented because the engine checks working hours and conflicts, not whether a date is reachable.
We fixed it in two layers, and the second one is the lesson. The prompt now
carries the current UTC instant and each person's local wall clock. But a prompt
is a hint, so the tool boundary now enforces it: a search window that has
already elapsed is rejected, a partly-elapsed one is clamped forward to now so
"this afternoon" still works at 2pm, and create_event refuses a past start
outright and hands the real current time back so the model can correct itself.
Our own eval contradicted our architecture. We built a 30-email labeled set to prove Nano was the right call for inbox triage. It wasn't, quite. Nano came in 3.1× cheaper but also slower (6.3s vs 2.1s mean) and less accurate (83% vs 97%), because on a bounded classification task it spent more reasoning tokens than Super did. The honest saving turned out to be the deterministic heuristic that resolves obvious thread matches before any model call at all. We kept the tier, published the number that undercut us, and made the model swappable with one environment variable.
Adding web search opened a door we had to close. The moment prep could
pull from the open internet, our agent was reading attacker-controlled text
while holding tools that write calendar events and queue mail. So web results
are treated as data and never as instruction: snippets only and never whole
pages, turn-delimiter markup stripped, length capped, five results maximum, and
an explicit untrusted-text note on every result. There's a test asserting a
hostile snippet gets defanged while still being shown to us, because quietly
hiding it would be the worse failure.
Accomplishments that we're proud of
The agent cannot invent a free slot. That sounds small and it's the whole product. Everything downstream — proposing times to a buyer, holding a room, writing a confirmation — is only trustworthy because the availability under it is computed, not generated.
Nothing leaves the studio without one of us approving it. We built this to save time, not to put an unsupervised writer on our outbound mail.
We shipped the eval number that made our own design look worse, and said so in the README. It would have been easy to lead with "3.1× cheaper" and move on.
Three models doing three honestly different jobs, with the expensive one behind a real gate rather than used by default.
And it runs for anyone with one API key. DEMO_MODE=1, npm run dev, done — no
Postgres, no OAuth consent screen, no Slack app. 78 tests, green from a clean
clone.
What we learned
Give the model language, not arithmetic. Every bug that actually hurt came from a place where we'd let the model compute something a function could have computed. Dates were the obvious one. There will be others.
A prompt is a hint; the tool boundary is the contract. We told the model the current date and it still would have been free to ignore us. Validating at the tool is what made it true.
Small models need a thinking budget to be useful. We assumed the cheap tier
would be the fast tier. On a task with a three-word answer, Nano out-reasoned
Super and lost on both latency and accuracy. A respected reasoning_effort: low
would change that calculus completely, and it's our main ask of the platform.
Measure before you believe your own architecture. Thirty labeled emails and an afternoon told us more about our routing than weeks of intuition.
Untrusted input is a design constraint, not a feature flag. The second our agent could read the open web, it needed to be the kind of system that treats what it reads as evidence rather than orders.
What's next for Assist.me
Getting the background work onto Nebius Serverless Jobs in production. The inbox poll wants to run every five minutes and the memory consolidation nightly, and neither belongs on a web host's cron.
More skills, which is now genuinely cheap: a negotiate skill for multi-party
external scheduling, and a debrief skill that turns a recording into facts
worth remembering. Both are YAML files and a prompt.
Smarter conflict resolution. Ultra currently proposes tradeoffs when there's no clean slot; we want it weighing who actually needs to be in the room against what we know about the relationship.
And the honest one: we want to stop being the only two people using it. The rules engine, the approval gate and the phase system were all built so this could be handed to a studio that isn't us without anything embarrassing happening on day one. That's the real test.
Built With
- clerk
- docker
- drizzle-orm
- gmail-api
- google-calendar-api
- google-drive-api
- google-oauth
- javascript
- nebius-serverless-jobs
- nebius-token-factory-(openai-compatible-api)
- next.js-16-(app-router)
- node.js
- nvidia-nemotron-(super-/-nano-/-ultra)
- postgres.js
- postgresql
- react-19
- supabase
- tailwind-css-4
- tavily
- typescript
- vercel
- vercel-ai-sdk-(ai-/-toolloopagent)
- vercel-cron
- vitest

Log in or sign up for Devpost to join the conversation.