Inspiration

We were in the parking garage when I understood.

Above us were the two floors I had already decided we were going to live in. Then someone opened the door to the garage. Water. Not a puddle, water across the whole floor, and a corner of ceiling on its way down, and the smell that goes with both.

We drove home and I did the arithmetic I should have done six months earlier. Every apartment we had been shown was still on the market. In Paris, a good apartment that is still on the market after two weeks is still on the market for a reason. We weren't being offered the market. We were being offered what was left of it, because we always arrived third.

Some background. I was born in 2010, my sister in 2014, and the bedroom that was mine became the bedroom that is ours. I'm sixteen and I still share it with her. We have been about to move since 2024.

It was never the money. It was that nobody in my house had a free hour at 9:14 on a Tuesday morning, which is when the good ones go up. Last year my parents finally committed and went at it properly. Six months. Somewhere around fifteen replies. A handful of viewings, all of them leftovers.

And a sentence, over and over, in a phone voice that has said it a thousand times: quelqu'un a proposé plus. Someone offered more. You know there's a decent chance it isn't true. There is no version of you that can prove it. The only defence against that sentence is to have arrived before the person who supposedly said it.

There was one my family really wanted. Gone in the weeks the bank needed to produce its paperwork.

Then: The property is already let. Five words that end a thing you had spent three days imagining.

I was standing there with my hands in my pockets. Sixteen. I can't sign a lease, I can't call a bank, I can't take an afternoon off. The one thing I could do was build the part that was killing us, which was never judgement. It was latency, repetition, and being awake at the right minute.

The queue isn't sorted by who deserves the flat. It's sorted by who answered first.

In French playgrounds, when you want to call something before anyone else, you shout prems (First!).

Why me

I should explain why a sixteen-year-old is the one writing this, because the answer is the same answer as why the product looks the way it does.

It starts on a motorway. I was small enough to be in a car seat, in the back on the left, wearing a blue jacket I loved. Road signs kept going past, covered in writing I couldn't read, and it drove me slightly mad. So I asked what each one said, over and over, the whole trip, and someone in the front would read it out and I would understand it for about four seconds until the next one arrived. The strange part is what that memory looks like now. When I replay it, I still see the sign, and in the memory the French on it looks like Chinese. I can see the letters and I cannot read them, even though today I obviously can.

I did not want to wait for school. Two years before I was supposed to start reading, I learned, syllable by syllable, with the Boscher method, at home. By the time the class started teaching it I already knew, and I was bored, so I looked at what the older classes were doing and started teaching myself the numbers instead. That has been the pattern ever since: boredom is not a state I sit in, it is a signal that there is something adjacent I haven't opened yet.

In CM2 I got my first phone. I did not install a single game. I installed Duolingo and I started learning English on the day I got it. I lost the streak plenty of times, holidays mostly, and I'm on 294 days today. Two years in, somewhere in 5ème, I clicked on a YouTube video that happened to be in English, and I understood all of it. That was genuinely shocking. Nobody had told me the day would come. It just arrived because I had shown up for two years.

The same stubbornness worked on something less pleasant. I used to be very overweight. I decided, one day, to stop being, and I did it entirely by reading everything I could find about food and then eating well for a year without ever going to the gym. Fifteen kilos. Not talent, not a program, just deciding and then not stopping.

At ten, during the first lockdown, I built a website. WordPress, on a computer at home, with no idea what I was doing. Kids at school called me l'intello, the nerd, as a joke, so I decided to own it and put it in the name. It went round the class, a few strangers online found it and followed, and then I typed my own name into Google and my own photograph came back. I was ten years old and I had a search result.

A little after that, some friends and I turned it into a small newspaper, Les Rumeurs de la Peur, which I wrote and published myself. It was the first time I made something out of nothing and put it in front of people who hadn't asked for it.

Then came the one that actually taught me something. I lost a fountain pen I loved, and I decided nobody in my class should lose anything again, so with four friends I started Discretos, September 2020 to June 2021. We were the class detective agency. We ran investigations, found lost objects, worked out who had taken what, and laid out the facts. I ran it, and I had a team of four. I learned Figma so I could design the tickets, and there were tiers: gold tickets, joke tickets, ones that stacked up every time somebody called us in, unlocking the next level. The whole class kept their tickets and tried to climb.

Nobody tells you this when you are learning to write code: a product without customers is not a product. My actual job at Discretos was not solving cases. It was finding the people who needed us and making the class want to come back. That is where I learned that the social half matters as much as the technical half, and I have never been able to unlearn it.

Then I made a decision I'm still not sure about. I had learned HTML and CSS. GPT-3 came out, and it couldn't really code, and it couldn't really do anything except talk — and I looked at it and concluded it was over, and I stopped learning to program. Everyone says you must learn to code. I decided I was going to lose that race and went and learned marketing, persuasion and storytelling instead. I read Cialdini. I studied how things are sold and how stories are built. I do not know yet whether that was clever or lazy. What I know is that this project was written by someone who spent those years thinking about who the thing is for.

And I have failed at this before. My last project was Casanova, an agent that would run your dating profiles for you. I put up a site and a waiting list. A stranger found it and took it apart in the comments: illegal, he said, and no woman is going to trust an AI to pick her partner. My first honest reaction was delight, because a person I had never met had read every word of my site closely enough to get properly angry about it. Then I did the research he had pushed me into, and I found the real problem, which was not his argument. Dating platforms throw random liveness checks at you — film yourself touching your nose, right now. An agent that hits one has to phone a stranger and ask them to open an unfamiliar browser session on the account it could get banned. Trust dies at that exact moment. I shut it down. It cost me nothing except the belief that a good product finds its own customers.

Last thing, and then I'll get to the build. Earlier this year I wanted an internship at Google. I walked to their Paris office and started talking to people going in and out, which made them uncomfortable and made them leave quickly. Security came over and explained, kindly, that this was not how it worked, and that if I wanted to apply the letter had to be in before six. It was five. I got on a train, and I wrote the whole cover letter on my phone with the cold going down my throat, and I made the images with about twenty minutes left. Then I ran from the station hard enough to crush the paper in my hand. I arrived at 18:20, twenty minutes late, and the guard let me in anyway, and flattened my crumpled CV out with his palm before he filed it.

Two days. No answer. So I went back. This time somebody walked out, saw me talking to the guard, came over to see what was going on, listened to the whole thing, and took me inside. First visitor badge of my life, with my photo on it. I got the tour. The internship turned out to be open only to employees' family members, so I never had a chance, and I came out of it with a badge, a day inside Google, and a fact I now trust: the second visit is a different visit.

(I'll also admit I have been annoyed at Google since Bard launched, because I signed up from France and never got access.)

I built this project the same way I got through that door. Nothing worked the first time. Nothing.

What it does

Prems is an agent that applies to rental listings for you, answers the agency, and books the viewing, from your own mailbox, while you're asleep.

It checks the listing source once a minute. A flat goes up, we see it inside a minute, and it gets scored against every active search. The thing that dominates that score is how old the listing is:

$$f(t) = \left(\frac{1}{2}\right)^{t/90}, \qquad t \text{ in minutes since publication}$$

A listing is worth half as much every ninety minutes, and this freshness number carries 27% of the total score — more than the budget fit, more than the surface, more than the number of rooms. That single weight is the whole thesis of the product. Put a flat that fits slightly better but went up four hours ago next to one posted a minute ago, and the fresh one is the only one you can still get. A perfect match nobody can reach is worth nothing.

Then the agent writes the application, and it leaves from your own Gmail with your name at the bottom. Here is a real one, translated into English:

Hello,

I am contacting you regarding your listing for the 56 sq m, 2-bedroom (4-room) apartment in Paris 2nd arrondissement, offered at €2,043 per month.

I am self-employed with a monthly income of €7,000, which is well over three times the rent.

I would like to arrange a viewing with you. I am available both on weekdays and weekends.

Best regards, Oren Song

That email is real. So was the accident that sent it, which I'll get to.

The agency's reply lands in your inbox, not ours. A classifier reads it and decides what kind of message it is. If it's an offer of a slot, the negotiator opens your actual Google Calendar, checks the days the agency proposed against what is already in your week, and answers with one you can genuinely keep. The agency writes "Tuesday at 2:00 PM or Thursday at 10:00 AM?"; the agent finds Tuesday is taken and replies "Tuesday does not work for me, Thursday at 10:00 AM works well." Nobody woke you up, and it never proposed a slot it hadn't checked. When the viewing is confirmed, it writes the appointment back into your calendar.

There's a tab in the app that shows what it looked at, what it concluded, and which tools it called to conclude it: "Read your Google Calendar, re-read your file, drafted the reply." That is the difference between a decision you can check and a decision you have to believe.

And there is a hard stop. The agent never signs anything. It goes as far as booking the viewing and no further. Nobody should be able to commit a person to hundreds of thousands of euros by writing a good prompt.

How we built it

The whole system is six small programs that wake up on a timer, do exactly one job, and go back to sleep.

They are all the same container image, deployed six times as Cloud Run Jobs in europe-west9. A single environment variable tells each copy which of the six it is supposed to be, which means one build and one deploy command covers all of them. Here is what each one does:

  • prems-scrape reads the listing site every minute, looking for anything new.
  • prems-enrich runs every five minutes and turns each new listing into a vector, so we can compare listings by meaning and not only by numbers.
  • prems-match runs every minute and scores new listings against every active client search.
  • prems-agencies runs every fifteen minutes and tries to find the e-mail address of an agency that only published a phone number.
  • prems-apply runs every two minutes, writes the application, and puts it in the outbox.
  • prems-inbox reads the replies that have come back and decides what to do about them.

Cloud Scheduler holds those six timers. Everything above happens with nobody watching, and none of it happens inside the website. You could delete the front end tonight and the product would carry on applying to flats, because the website is a way of looking at the system rather than a part of it.

Three of those steps need judgement, so three of them call a model. All three agents are built with the Agent Development Kit and run on gemini-3.7-flash through Vertex AI. The model and its authentication are written down in exactly one file in the entire repository, which is what makes changing model a one-line edit instead of an afternoon of hunting.

Two of the three agents have no tools at all, and that is a decision rather than something we forgot to do. A tool, here, means a function the model is allowed to call when it wants information it doesn't have. The agent that writes an application doesn't need one, because every fact it is allowed to use — the rent, the surface, the client's income, their name — was already collected before the agent was asked to write anything. The agent that reads an agency's reply and sorts it into one of four categories doesn't need one either. Reading a message is not an errand. Both of them answer against a response schema, which means the shape of their answer is enforced by the API itself. "Answer in strict JSON" is an instruction a model is free to ignore. A schema is a wall.

The third agent, the negotiator, has six tools, and adding them is the change that made the product actually work.

Before it had tools, we simply pasted the client's availability into its instructions and hoped it would read them. That failed in the worst way something can fail. A model that skimmed those lines would produce a completely reasonable-sounding sentence proposing a Tuesday afternoon on which nobody was free, and nothing further down the pipeline could tell that sentence apart from a real proposal. It looked correct. It was fiction. And it would have gone out under a client's own name.

So we took the facts out of the prompt and put them behind functions the agent has to go and call:

  • get_client_availability returns the slots the client actually saved.
  • check_calendar_conflicts reads the client's real Google Calendar and says which of the agency's proposed times are free and which clash, and with what. If the calendar cannot be read, it says so rather than reporting an empty diary — an agent told "nothing is booked" by a failed lookup will happily accept a viewing the client cannot attend.
  • get_client_facts returns what we know about the candidate. A missing field means information we do not have, and the agent is not allowed to fill it in.
  • request_document records something the agency has asked for that the client's file doesn't contain, so it appears in the app with an upload button next to it. Before this existed, the agent would write "I'll send that over today" and nobody was ever told.
  • queue_reply puts the message in the outbox. It is idempotent: a second call is refused, not applied.
  • stand_down sends nothing, and states why.

Every tool the agent calls is written into the event log next to the message it produced. So when a client asks "why did it propose Thursday?", there is a record to point at instead of a theory about what the model was thinking.

The rules that really matter live in code, not in the prompt, because a rule a model can talk itself out of isn't a rule. There are five of them:

  1. If the agency has refused, the thread is closed before the agent is even constructed. It never gets the chance to argue, because arguing with an agency that has said no is how a client's own e-mail address ends up marked as spam.
  2. The limit on replies per thread and the operator kill switch are stored in the database and checked before the agent runs, not asked of it politely.
  3. If the client had availability saved and the agent never called the tool that returns it, whatever it wrote is thrown away and replaced by a plain message. Whatever it said about dates, it did not read them here.
  4. A calendar date invented for a client who saved no availability is refused outright.
  5. An empty answer is not treated as a decision to stay silent. Only stand_down counts as choosing silence.

All five are covered by tests that replay a script of tool calls against the real tool implementations with no model involved at all, so the guarantees hold whatever the model does on a given day.

The rest of the stack is ordinary and deliberately so: Document AI reads payslips and ID documents, Secret Manager holds every credential so that no key file ever ships inside a container, Artifact Registry and Cloud Build handle the images (one YAML file is the entire deployment), Supabase Postgres is the single source of truth for everything, and Composio brokers each client's own Gmail and Calendar access, one connection per person.

Two honest notes about how this actually got made.

I built the landing page before I built the scraper. That looks backwards and it isn't. I had already learned, expensively, that a product without customers is not a product, so I wanted to be able to say the thing out loud and watch somebody's face before I wrote a single line of pipeline.

And I wrote all of it with an AI, one to two hours a day, mostly over the holidays, mostly at night, sometimes until two in the morning. My family went to the pool and I stayed at the desk, quietly annoyed every time we had to go out. What I contributed was the precision of what I asked for and the shape of the system: where data goes, what runs on a timer, what is allowed to fail and what is not. My debugging method was crude and beat everything clever I tried: open a completely new conversation, hand it the code cold, and ask it to find every bug. There were always more. Every single time.

Challenges we ran into

The night it sent four real emails. Thursday, around eleven at night. I pushed to Cloud Run to see what would happen, and what happened was that within a minute the agent found four listings matching demo criteria I'd typed at random, wrote four applications, and sent them. To four real agencies. Signed with my name.

I killed it. Then the replies started (translated into English).

Hello, from what date are you looking to move in?

I had to write back and tell a working estate agent that it was a mistake and I wasn't looking anymore. That is where the kill switch came from, and the reply cap, and the whole idea that the constraints belong in code rather than in a paragraph asking the model nicely.

The agent that argued against me. I'd skipped the guarantor field during onboarding because I was in a hurry. So the agent, being honest and having no notion of what honesty costs, told the agency every weak point in my file. Unprompted. Accurate. Useless.

A model that wasn't where I asked for it. Things failed for hours and I couldn't see why. It turned out gemini-3.7-flash is served from the global endpoint and 404s in every European region I tried, and that eu-aiplatform.googleapis.com doesn't exist at all — it answers 400, Invalid hostname. The European multi-region is a Document AI thing. I had a documented fallback that would have died on its first call. Now a script calls every endpoint and prints the matrix, and the model, its region and the residency consequence are logged on every single run.

Green logs that proved nothing. The scraper reported 1,440 runs in 24 hours and zero failures, and I was proud of that for about a day before realising it says the scraper ran. It says nothing about whether it found everything. A scraper missing half the market produces identical green logs. So we went and measured against the source: query the site directly, filter to the zones we crawl, diff the identifiers against the database. 41 out of 41 over 51 hours. When I understood how little the first number meant, I smiled, mostly at myself.

A client competing against himself. 86 matches rejected and shown to the client as losses, when the "someone else" who'd won them was the same client's other saved search. That one went cold down my back.

Money. €40 of credits in a week, on a project run by someone with no income, genuinely afraid of waking up to an empty balance.

And the one that actually hurts. Before this, I built for another hackathon. I finished it. I submitted it at 23:01. One minute after the deadline. I went to bed and didn't want to speak to anyone.

I have now lost twice for being late. Once for my family, once for myself. That is a strange thing to have in common with your own product.

Accomplishments that we're proud of

The first time the negotiator handled an agency on its own, I didn't say anything. I sat there with my mouth open and something rising in my chest that I didn't let out.

That is not normal for me. In maths class, when a proof lands, I shout. Out loud. "C'est du génie !" My teacher finds it very funny and I have never been able to stop. So the fact that I went completely silent watching a machine write a message I hadn't written, in my name, to a person who would answer it — that silence is the thing I'm proudest of. I went and found my family thirty seconds later, talking much too fast.

The rest, measured rather than claimed: 815 listings, 737 matches after de-duplication, zero in the dead-letter queue, ~390 ms to score a listing, 59 tests, CI green. Five guardrails pinned by replaying a script of tool calls with no model involved. A region matrix established by calling live endpoints instead of reading documentation.

And the honest one. Outside my own household, nobody has used this yet. I connected my own Gmail, I watched it work end to end, and I loved it. That's one mailbox, and I'd rather say so than let the number be inferred.

I'm sixteen. I'll say that once and then let the code speak.

What we learned

Take the facts out of the prompt. This is the single most useful thing I now know about building agents. Anything important that you paste into an instruction is something the model is free to skim, and when it skims it does not fail loudly — it produces a confident sentence built on a fact it never actually read. Put that fact behind a function it has to call, record that it called it, and the difference between a slot the agent read and a slot the agent invented becomes something you can check instead of something you have to trust.

A rule you can talk a model out of isn't a rule. It's a suggestion with good branding. If the consequence of breaking it is paid by your user rather than by you, it belongs in code, upstream of the model, where no amount of clever wording reaches it.

An agent needs an off switch that lives outside the agent. I learned this from four e-mails I did not mean to send. The kill switch is a row in a database that the workers read before they do anything, so stopping the system does not require a deploy, a code change, or me being awake.

Green is not proof. A process reporting success is telling you about itself. Measure the output against the world instead: I only found out our coverage was real by querying the listing site directly and diffing the identifiers against our own database. Every dashboard I had was green either way.

What's next for Prems

My parents are next. Not as a metaphor. They are going to run their own search on this, and we are going to try to move house with it, and I will find out in the least comfortable way possible whether the thing I built is any good.

Then, in order: swap raw payslips for a DossierFacile link, which is the French state's own document service, so the sensitive papers stay where the state put them and I only ever handle a URL. Move the model to europe-west3 before the first paying customer, because the prompts carry a named person's income, their diary and their private correspondence, and that decision belongs before the money, not after it. Add sources beyond the first one, which is an adapter and a database row, never a deployment.

And the business, since I'd rather be judged on the real plan than a modest one. The subscription is $30 a week and that is the door, not the room. Moving looks like a one-off transaction, and it isn't: it's the single moment a household buys home insurance, an electricity contract, movers, furniture, storage. I want a commission at signature and a margin on the rest of the move. The tools I've found abroad send applications, or they send alerts, and they charge €9.99 a month for it. They're building a small business next to a very large moment.

We are still four people in a flat with one bedroom too few, and I still share a room with my sister.

I'd like the first apartment this thing wins to be ours.

Built With

Share this project:

Updates

Submission history