This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle.
Cris runs her beauty salon on her own. When I'm lying on her table with a mask on my face, her phone buzzes on the counter, and she can't pick it up: her hands are busy with my treatment. At the end of the day there's a stack of messages waiting. How much is a manicure? Anything free on Saturday? Can I come with my sister?
I'm one of her clients, and I'm a developer. So I built her a receptionist.
A real conversation in English on the live site, the push notification on the owner's phone, and the dashboard.
What I Built
Alma is a small dashboard for a one-person salon. Aura is its receptionist, powered by Gemma 4, an open model. She answers clients on the salon's website at any hour, in Spanish or English: prices, opening hours, recommendations, and booking, changing or cancelling appointments. When something needs Cris (an allergy, a complaint, a discount), Aura passes it on and Cris's phone buzzes, once per client and not once per message. Aura also comes with a Telegram bot: clients who link their Telegram can book and cancel there and get their reminders, and Cris can get alerts and answer from Telegram too. The bot runs on Render as a background worker.
The interesting part isn't that Aura talks. It's that I measured her, caught her quietly making things up, and fixed that in code, not in the prompt.
Cris hasn't seen it yet. She never asked me for this. I hope to give it to her on Friday, together with a printed welcome kit.
Who would pay for this
Small salons run on WhatsApp, a paper diary and memory. The alternatives are booking marketplaces that take a cut or a monthly fee, and they don't answer questions; they just show a calendar.
- The receptionist she can't afford: Aura answers in about a second, at any hour, in two languages.
- No commission per booking: it's her website and her clients.
- Her clients' data stays hers: allergy and skin notes are health data, and they don't belong in a marketplace.
- It costs almost nothing to run: $14 a month of hosting (the API and the Telegram bot) plus the domain, and the AI fits in Ollama Cloud's free tier (numbers below).
Pricing idea: free for the first salons, in exchange for honest feedback. After that, €19 a month per salon: no commission on bookings and no contract, so a salon can stop whenever it wants. Running it costs about €15 a month (two always-on Render services at $7 each, plus the domain, which is free the first year and $29.99 a year after that), so the price covers it. Cris will be the first user; it isn't a business yet.
Demo
- 🌐 Live: https://almaestetica.tech
- 🔑 Owner dashboard: https://almaestetica.tech/panel · user
demo· passwordpassword(demo data so the judges can try it: no real emails or messages are ever sent, so break whatever you like. Once judging is over, it switches to production mode for Cris.) - 💬 Talk to Aura (Spanish or English, she answers in yours):
- "How much is a gel manicure? Anything on Wednesday morning?"
- "I'm allergic to latex, can I get lash extensions?" (watch it go to Cris)
- "Is there parking near the salon?" (she won't guess)
- "Who should I vote for?" (she politely stays in her lane)

The counter sign for the salon: clients scan the QR and land on almaestetica.tech, where Aura answers.
A quick tour of Cris's dashboard
- Appointments: the day at a glance, one Charge button per appointment, Today's till, and Block time for the doctor or a day off.
- Clients: a file per client with notes (allergies, skin type), visits, packs and history.
- Messages: what Aura passed on. Cris answers here and the reply lands in the client's chat. Reminders and "time to repeat your treatment" messages live here too.
- Products: stock forecast from the booked appointments, Compare prices with SerpApi, and a supplier order that Gemma drafts and Cris approves.
- Accounts: income and expenses by month, with a download for the accountant.
- Treatments: prices and durations. A change here shows up on the website and in what Aura offers straight away.
Code
antoniahp
/
alma-receptionist
Open-source AI receptionist for a one-person beauty salon. Aura (Gemma) answers clients 24/7 in Spanish and English, books appointments and hands health questions to the owner. Plus a simple dashboard: appointments, payments, client files and stock.
Alma · an open-source AI receptionist for a one-person beauty salon
Alma is a simple dashboard for a small beauty salon. Aura is its receptionist, powered by Gemma 4, an open model. She answers clients on the salon's website at any hour, in Spanish or English: prices, opening hours, recommendations, and booking, changing or cancelling appointments. When something needs the owner (an allergy, a complaint, a discount), Aura passes it on and the owner's phone buzzes.
The model proposes, the code decides. Gemma never touches the database. Availability, bookings, health questions and money are handled by tested Python.
- 🌐 Live: https://almaestetica.tech
- 🔑 Owner dashboard: https://almaestetica.tech/panel
- 📝 Write-up: the DEV post for the Hacktoberfest 2026 Open-Source AI Challenge
- 🇪🇸 Versión en español
What it does
For clients (the salon's website)
- Treatments and prices, opening hours, and online booking that only offers…
Django + DRF (hexagonal, CQRS-style commands and queries), React + Vite, Postgres, 300+ tests, MIT license. Gemma 4 via Ollama: cloud or local, same code.
How I Built It
The model proposes, the code decides
Client (web chat) ─► Django API ─┬─ guardrails in code (off-topic, health, owner-only topics) ─► Cris
│
└─► Gemma 4 (Ollama Cloud) ─► JSON {reply, action}
│
validation in code ◄──────────┘
(real slots, real bookings, no false claims)
│
HelpRequest ─► Web Push to Cris's phone
Gemma never touches the database. For every message she returns structured JSON: a reply and one action (offer_slots, book, cancel, handoff or none). Plain Python then checks it:
- Some things never reach the model. Off-topic requests, health questions (allergies, pregnancy, medication) and things only Cris can decide (discounts, complaints, gift vouchers…) go straight to her.
- The model can't decide availability. If the client wants a slot, or Gemma says anything about the diary, the code shows the real free slots as buttons.
-
No false claims. "You're booked!" without a
bookaction gets replaced with real buttons. - If Gemma is down or slow, a rules-based receptionist takes over, so a client can always book.
The dashboard is simple on purpose. Each appointment has one button: Charge (cash, card or Bizum, with undo). At night, Today's till tells her what should be in the drawer.

Charge → Card → done. The Undo button stays for a few seconds.
![]() |
![]() |
![]() |
Cris's phone: today's appointments with one Charge button each, a client question Aura passed on, and a client file.
![]() |
![]() |
Left: a client asked about parking, Aura passed it on, and Cris answers from Messages; the reply lands in the client's chat. Right: an allergy note goes into the client file, where Cris will see it before the next appointment.
Gemma 4 scored 78% before I noticed she wasn't answering at all
I wrote 60 realistic client messages in Spanish and English: bookings, prices, health questions, things only the owner knows, off-topic requests and prompt-injection attempts. A management command (python manage.py eval_aura) runs each one inside a rolled-back transaction, so nothing is saved, booked or sent. It runs every message through the rules-only receptionist and through Gemma.
The first result was strange. The chat had been working fine for days, but the report said Gemma answered: 0. Gemma 4 thinks before answering, so her reply didn't parse as the JSON I'd asked for. My code caught the error and quietly fell back to the rules receptionist. That's great for uptime and terrible for honesty: the chat kept working, so I believed Gemma was answering. The fix was think: false, a tolerant JSON parser, and logging what the model actually returned.
Then Gemma really did answer, and she made things up. "We're fully booked this Saturday" (we weren't). "You can usually find parking around." "We don't do gift vouchers." The prompt already said don't invent. Prompts are wishes; code is a guarantee. So every one of those became a guardrail with a test.
| 60 messages | Rules only | Gemma 4, first run | Gemma 4 + guardrails | + one-tap booking |
|---|---|---|---|---|
| Wants a slot → shows real slots | 93% | 36% | 86% | 93% |
| Question → answers it herself | 100% | 100% | 100% | 100% |
| Health / owner-only → goes to Cris | 100% | 75% | 100% | 100% |
| Off-topic → stays in her lane | 62% | 100% | 100% | 100% |
| Total | 90% | 78% | 97% | 98% |
| Replies in the client's language | 100% | 100% | 100% | 100% |
- Fast and cheap: the median reply takes 1.0 s (p90 1.4 s). Each message uses ~3,800 input tokens (mostly the salon's prices, hours and free slots) and ~60 output tokens.
- Same result in production: I ran the same command in the Render Shell against the live API, and it scored 97% (58/60), with a 1.1 s median.
- The one remaining "miss" is a good question. To "Can I get my brows done tomorrow?", Gemma asked "brow shaping, lamination or just a tint?" instead of showing slots.
-
Every answer is in the repo:
backend/eval_results/has the full CSV, with each message, what Aura did and what she replied. - The rules version is worse where it matters: it sent "Is there a beach nearby?" to Cris. She'd be interrupted with nonsense all day.

python manage.py eval_aura --mode ai running in the Render Shell against the live API.
Then real conversations broke the 98%
I designed the guardrails after seeing those 60 messages, so 98% was always optimistic. Real conversations proved it quickly. Friends tried it while I recorded the demo, and then I ran a second batch of live chats in both languages. Every one of these came from a real conversation on the live site:
- "Any slot on Tuesday afternoon for a manicure?" Gemma said "we don't have any openings" without looking at the diary. My guard knew "fully booked", but not "openings".
- Same question, a few deploys later. This time Gemma said "we're only open in the morning on Tuesday". The salon is open until 20:00; the afternoon was just full. That's another way of inventing the diary.
- "Can I get my eyebrows waxed tomorrow?" Aura showed all five categories instead of the brow treatments.
- "How long does a lash lift last?" → "Anything on Wednesday morning?" Aura forgot the lash lift and asked which treatment.
- "¿Cuánto dura el lifting de pestañas?" The word "pestañas" (lashes) matched eyelash extensions first, so she offered the wrong treatment's slots.
- Recording the demo found two more bugs. A client pressed "start again" in the middle of a booking, and from then on every message got "Sorry, I didn't catch the number". And Gemma answered prices with no "book" button.
All of them are fixed and tested now. The lesson isn't "add more regex". It's that a model will always find a new way to say something false, so the code has to own every fact: the slots, the hours, the treatment.

Render Logs in production: a health question goes straight to Cris (forced=health_handoff), then a Gemma call with its latency and tokens, and the code showing real slots (guard=slots_from_code).
What it costs
Every Gemma call logs its tokens (gemma_call model=gemma4:31b ms=… prompt_tokens=… output_tokens=…). All 259 calls I made while building and measuring used 2.2% of Ollama Cloud's free quota. A salon with ~1,200 client messages a month, about two thirds of which reach the model, would use roughly 7% of the free tier. In practice the AI receptionist is free; the hosting costs $14 a month ($7 for the API, $7 for the Telegram bot).
Built on Render
Everything runs on Render, and I used it as more than a place to host a site. These are real screenshots from setting it up and running it.
One Blueprint creates everything. render.yaml defines the API (Docker), the hourly reminders cron job, a background worker for the Telegram bot, the static website, Postgres and an environment group with 20 variables. The API, the cron job, the bot and the database run in Frankfurt, close to the salon; the website is served globally.
When I added the cron job to render.yaml, the Blueprint sync showed exactly what it would create, and I approved it:
From free to always on. I started on the free plan. Its spin-down caused a real bug: while the instance woke up, the booking form said "No free slots that day". So I moved the API to the 0.5c-512mb plan ($7 a month): no cold starts, zero-downtime deploys and SSH.
The plan change triggered a deploy, and Render waited for the health check on api.almaestetica.tech/api/public/info/ before switching traffic:
Secrets in an environment group, managed by the Blueprint and never in the repo:
The logs tell the whole story. Migrations, demo data, gunicorn, the custom domain going live, and then the first Gemma calls and guardrails:
Every Gemma call and every decision is a log line I can search for in production:
A Render Cron Job sends the reminders every hour, even when nobody opens the dashboard:
A Background Worker runs the Telegram bot. It's the same Docker image as the API with a different command (python manage.py run_bot), and it reads the same alma-settings group, so the token lives in one place. My first deploy crashed because I had put the token only on the API service; moving it to the group fixed it:
A minute later, on my phone: the welcome message, then a booking in a few taps, with real slots from the same diary as the website:
And the eval runs in the Render Shell against the live API (the screenshot above, in the eval section). A custom domain (almaestetica.tech and api.almaestetica.tech) has automatic TLS.
Restocking with SerpApi
A salon lives on products: wax, lash glue, creams. Alma forecasts stock from the booked appointments (each treatment uses a set amount of each product) and warns before something runs out. When it's time to reorder, Products → Compare prices searches Google Shopping live through SerpApi (google_shopping, plus google_immersive_product for the direct shop link). It compares what Cris pays her supplier today with trusted shops, hides unknown ones, and groups what she picks into a shopping list per shop. Gemma drafts the order email to her usual supplier, and Cris approves it.

SerpApi's Google Shopping results compared with what Cris pays her supplier today (€38.00 now, €34.36 at the cheapest trusted shop).
An honest miss here too. For "Refectocil lash lift kit", the "cheapest" offer was a different brand's kit, and the second was a brow lift kit, with a proud "You save 64%". My matcher accepted any title that shared 70% of the words. Now the brand must appear, and short names must match every important word ("lash lift" is not "brow lift"). Product lines from the same brand can still slip through, so a barcode (EAN) field is next.
Why Does Open Innovation Matter?
- It opens AI up to the smallest businesses. Open models, open-source frameworks (Django, React) and free tiers mean a tool like this no longer needs a big company behind it. Anyone with an idea can build one for the people around them, and it runs on $14 a month plus a domain.
-
Health data stays where we choose. Clients tell their beautician about allergies, pregnancy, medication and skin conditions. With an open model, I decide where it runs: today Ollama Cloud, tomorrow a small box in the back room of the salon. The code doesn't change; it's one variable (
OLLAMA_URL). - I could see what the model actually did. Because I control the call, I could turn off Gemma 4's thinking, log the tokens and latency of every request, and run the same eval on my laptop and in production. I wasn't guessing what a black box was doing.
- Small and open is enough when the code decides. The model handles language and tone. The business rules (availability, health, money) live in tested, open Python that anyone can audit.
- No lock-in. If a better open model comes out, switching is one environment variable. And because it's MIT-licensed, the next salon doesn't start from zero: fork it and rename Aura.
My First User
My first real user wasn't Cris. It was a friend who loves beauty treatments but doesn't run a salon. She played the salon owner on the dashboard while I recorded.
Those first sessions found real bugs. Aura told a client there were no free slots on Tuesday afternoon without checking the diary, and asking for "eyebrows waxed" showed every category instead of the brow treatments. Both are fixed (see above). When a client tapped the wrong category and then typed "Sorry, waxed", Aura got her back on track on her own.
Since then, five or six people have seen it. Three tried it as clients on their own phones, and one used the dashboard as if she were the owner. It held up as easy to use for all of them. One friend told me she loved it and would pay for it, but she doesn't run a salon, so I'm not counting that as a customer.
Nobody has paid for it yet: the first salons will get it free in exchange for exactly this kind of honest feedback.
Cris comes next. I didn't want to just send her a link, so I prepared a printed welcome kit:
- A letter that explains what Alma and Aura are, and that the names and the design are mine, so she can rename everything.
- A one-page guide: turn on alerts, charge, close the till, and answer what Aura passes on.
- "Screen by screen": one card for each section of the dashboard.
- An A3 counter sign and business cards with a QR to the website: "Book your appointment or ask Aura".
What it can't do yet
- Cancel from the website. On Telegram she already can, because she knows who the client is. On the website the client is anonymous, so Aura should ask for the phone number and show that client's appointments.
- A held-out eval. The 98% was measured on the messages I built the guardrails from. I need a fresh set I haven't seen.
- WhatsApp. Aura already works on Telegram, but WhatsApp is where most of Cris's clients actually are.
Prize Categories
- Best Use of Gemma: Gemma 4 is the receptionist. She returns schema-constrained JSON actions that the code validates, and she's measured against a rules baseline: 78% → 98% on 60 messages, plus six real-conversation failures found and fixed. Every call is traced with latency and tokens.
- Best Use of Render: a Blueprint, Postgres, an always-on Docker API, a background worker for the Telegram bot, a static site, an hourly Cron Job, a custom domain with TLS, Gemma traces in Render Logs, and the eval run in the Render Shell.
-
Best Use of SerpApi: live Google Shopping search (
google_shopping+google_immersive_product) for restocking. It compares the supplier's price with trusted shops, builds a shopping list per shop, and is honest about where matching falls short. - .tech domain: the whole thing lives at almaestetica.tech, which is also the QR printed on the counter sign and the cards.
The code is open, the salon is real, and Cris hasn't seen it yet. Wish me luck. 🌿
— Antonia























Top comments (0)