# I Sent an AI Agent to Wait in Line for Me

> Pronto’s best House Help slots appeared while I was away, so I asked an AI agent to keep looking and come back when it found one I could actually use.

Sid Jain · Published 2026-09-03

Canonical post: https://f0rr0.dev/blog/no-slots-so-i-built-tranquilo

---

Pronto's House Help appointments had become a queue I could only join by repeatedly opening the app. For several evenings I checked after work, found nothing useful, closed it, and tried again later. Every so often a decent slot would appear and disappear before I could act. The supply existed; my timing was rubbish.

Checking every few minutes would probably have worked. I didn't want to donate my evenings to a refresh button, so I built [Tranquilo](https://github.com/f0rr0/tranquilo) and gave an agent a fairly ordinary instruction:

> Find an hour tomorrow after six. If there is nothing, keep looking.

“The agent waited in line” is a convenient description, but it collapses three different bits of software. The model translated my sentence into structured intent. Ordinary TypeScript ranked and revalidated slots. An operating-system scheduler did the waiting. Keeping those jobs separate turned out to matter more than making the agent sound clever.

## First, make “after six” boring

An availability response is an observation, not a reservation. By the time I read it, the remote state may already have changed. Tranquilo therefore normalises the response at the edge and carries one small slot model through the rest of the program:

```ts
type SlotObservation = {
  startTime: string;
  durationMinutes: number;
  listingId: string;
  listingItemId: string;
  slotsLeft?: number;
  isFull: boolean;
  observedAt: Temporal.Instant;
};
```

That also avoids hard-coding “60 minutes” to one product identifier. The listing depends on the selected address and live catalogue, so search resolves the service first and carries the identifiers forward.

My preferences weren't interchangeable. After work mattered more than whether the appointment began twenty minutes earlier; tomorrow mattered more than a slightly better duration three days later. The interactive `find` command uses separate score bands to preserve that order:

```ts
const rank =
  matchedWindow.rank * 1_000_000 +
  datePenalty * 10_000 +
  durationRank * 100 +
  hour * 2 +
  minute / 30;
```

It is a lexicographic sort wearing a numerical moustache. A lower-priority preference can't accumulate enough points to defeat a higher-priority one.

The watch path is deliberately simpler: it checks the concrete date, duration and window saved in the watch and stops at the first acceptable live result. It does **not** currently run every candidate through the full weighted ranker. That distinction had disappeared in my first account of the project, and the code is more interesting when I don't make it tidier than it is.

Appointment times are local civil times; scheduled checks happen at instants. Tranquilo uses the [`Temporal`](https://tc39.es/proposal-temporal/docs/) model to keep those concepts apart and rejects past or out-of-horizon results. The [MCP](https://modelcontextprotocol.io/) tool can turn “tomorrow after six” into validated dates, a time window and duration preferences. It then persists a watch rather than leaving the conversation open.

## A scheduler, not an immortal loop

My first version wanted to do this:

```ts
while (true) {
  await checkForSlots();
  await sleep(60_000);
}
```

That works until the laptop sleeps, the network changes, the process dies, or I forget why a random Bun process has been alive for four days.

The repository implements all three desktop scheduler adapters:

| Platform | What Tranquilo installs            |
| -------- | ---------------------------------- |
| macOS    | a per-user `launchd` agent         |
| Linux    | a `systemd` user service and timer |
| Windows  | a Task Scheduler task              |

Each trigger launches a short-lived check which loads due watches, queries availability, records the result and exits. On macOS, `StartInterval` doesn't magically poll while a laptop is asleep. The useful property is that the saved watch is still there when the machine is available again; this is durable intent, not an always-on service.

The state progression is small enough to read without a diagram:

```text
enabled --no match--> enabled (next run)
enabled --error-----> enabled (delayed retry)
enabled --slot------> found
enabled --date past-> expired
paused  --resume----> enabled
```

There are two reliability details worth stating precisely. The current lock is an exclusive `wx` sentinel file. It prevents a scheduled run and a manual run from overlapping normally, but it isn't a lease and it has no stale-lock recovery after an abnormal exit. State is also written directly as JSON rather than through a temporary file plus `fsync` and atomic rename. So a reboot doesn't require a daemon restart, but I won't claim the watch store is crash-proof.

Notification delivery has a similar edge. Today the watcher notifies before it saves the final `found` state. If the notification succeeds and the process dies before the write, a later run can notify twice. If notification fails and the error is swallowed, the watch can still become `found` and stop checking. This is best-effort delivery. A production-grade version needs a small outbox, an atomic state transition and retryable notification records. “Exactly once” would be a very ambitious phrase for a desktop notification anyway.

Those aren't theoretical distributed-systems flourishes. They're the bits I would harden before telling someone else to depend on a week-long watch.

## Finding a slot still isn't booking it

No local lock can reserve an appointment in a remote service. When I select a result, the booking path searches again for the exact timestamp and duration. If the slot has gone, it stops before checkout. If it remains, Tranquilo:

1. resolves the saved address and current House Help listing;
2. sets exactly one resolved item and the selected slot in the cart;
3. fetches the cart again;
4. verifies both listing identifiers against the choice; and
5. creates checkout using the amount and cart version from that fresh read.

Another customer can still take the appointment between the final check and checkout. Tranquilo treats `SLOT_OVERBOOKED` as an expected result and returns to fresh availability. The revalidation narrows the race; it doesn't turn Pronto into my database transaction.

Preparing a booking is itself a remote mutation: it changes the cart and can create the Juspay order before I pay. That is why it is a separate, explicit operation rather than something a notify-only watch may call.

## The agent-facing boundary is narrow, not magic

The MCP catalogue gives search, watching, booking preparation and payment handoff different capabilities:

| Tool                        | Behaviour | Boundary                         |
| --------------------------- | --------- | -------------------------------- |
| `househelp_find_slots`      | read-only | never touches cart or checkout   |
| `househelp_watch_create`    | mutating  | persists a notify-only watch     |
| `househelp_prepare_booking` | mutating  | needs an exact duration and slot |
| `househelp_payment_handoff` | read-only | returns a local payment command  |

The watch can't auto-book, and search can't quietly mutate the cart. Long-lived authentication tokens and the payment URI are kept out of the public agent-facing response; payment approval stays in my terminal. That is narrower than saying “provider credentials never enter model context”. A phone number, OTP, address label or order metadata can still be exposed if someone places it in a prompt or chooses a hosted model. The boundary depends on the client and model provider as well as Tranquilo's response schema.

I also don't regard an undocumented personal-service API as an invitation to hammer it. The current watcher uses a fixed cadence and a coarse delay after errors. Before broader use, I would add jitter, explicit `Retry-After` handling, a longer adaptive backoff and a conservative ceiling on requests. The tool should remain a quiet substitute for my own refreshes, not become a small denial-of-service project because I wanted a clean floor.

## What the repository proves

The [public source](https://github.com/f0rr0/tranquilo) contains the TypeScript CLI, MCP server, scheduler adapters, booking revalidation and local payment handoff. The [documentation](https://tranquilo-ai.vercel.app/docs) describes the commands and installation. It demonstrates the mechanism; it isn't a controlled experiment in slot-acquisition speed, and I don't have a neat graph of polls-to-booking to pretend otherwise.

What changed for me was much smaller. I stopped keeping an empty booking screen in my working memory. The model parsed the request, the scheduler checked when my laptop was awake, and a notification returned the choice to me.

I had sent the machine to spend time, not authority. Then I got on with my evening.
