Case study · Agent behavior design
An AI agent that watches the curb so you don't get a parking ticket. I designed how it decides when to speak, when to stay quiet, and how to be honest about what it doesn't know. I'm also building it.
Los Angeles writes millions of parking tickets a year and has never published a complete map of its own curb rules. An agent that warns you has to know the rule, know which curb you're on, and know when it doesn't.
Behavior design, the specs, the data engine, the Agentic Walkers, and the iOS build, working with Claude Code.
A rules engine from 10.4M citations, park detection from the unplug moment, warnings on my own phone, and a group of agents checking signs on the street.
The detection approach, twice. The confidence model, from a number to a behavior. And the spec caught a real failure in my own build before a user ever could.
Curb runs on my phone and in the simulator, not on the App Store. The current status, what shipped and what comes next are kept on one page: Where the build of Curb is, and what I learned.
The product, first
The primary surface is a notification, because the agent speaks when you're not in the app. Opening Curb at all is the exception, not the flow.
The hard part
When the agent has no data about a spot, silence is dangerous. Silence reads as "you're safe."
Most parking apps answer "where can I park?" Curb answers a quieter question: should I say anything right now?
That turned out to be the whole design problem. An agent that pings you about every curb is noise, and you'll mute it. An agent that stays quiet when it actually knew something has failed you. And the case nobody designs for is the one in between, where the agent has nothing at all for this block.
If it stays quiet then, it's telling you the most confident thing it could possibly say, all clear, at the exact moment it knows the least. Getting that one behavior right, and the decision making that follows from it, is what this project attempts to solve in the new world of agentic AI.
What I was designing
Curb is a personal safety agent. It notices when you've parked, works out the rules for that exact stretch of curb, and tells you only if it's worth your attention. Most of the time it says nothing at all.
My job was the behavior, not just the screens. When does it interrupt. How does it show how sure it is. What does it do when it doesn't know. And how do you come to trust something that works while you're not looking.
The through line for all of it: an agent you delegate to is a relationship, and the whole relationship rides on how it handles uncertainty.
Reasoning 01
The agent is never certain. It works rules out of messy public data, and that data will never be complete. So the question was never how to hide the uncertainty. It was how to make it honest and readable.
I reframed the problem from competence, meaning is it right, to calibration, meaning can you tell how sure it is. An agent that's right 95% of the time but says everything in the same voice teaches you nothing about where the other 5% lives. You end up either over-trusting it or ignoring it, and both of those cost you a ticket eventually.
So confidence became something the agent does, not something it reports. Same restriction, three levels of certainty, three different ways of speaking.
High confidence
Medium confidence
Low confidence
Three things change with confidence. The label says it in a word, high, medium or low, sitting after the rule and the fine. A low-confidence rule does not reach the lock screen at all: it stays inside the app, so a notification never carries a claim the engine cannot back. And where the block's time limit is only a guess, the app asks instead of asserting, with its own estimate pre-highlighted. Check the sign, what is the posted limit? One tap starts the exact timer and settles a block no dataset covers.
A person reading these in the wild never thinks about calibration. They just notice that the app sounds different when it's guessing, and their trust starts tracking its actual reliability spot by spot.
The spec defines four behaviors, each traced to the principle doing the work:
| Confidence | Behavior | Why |
|---|---|---|
| High | Notify, stated flatly | Be decisive on facts |
| Medium | Notify, hedged | Be honest about uncertainty |
| Low, restriction plausible | Ask | Defer on genuine judgment calls |
| Confident it's clear | Stay silent | Protect attention, once earned |
The bar to reach silence is deliberately higher than the bar to speak. A false alarm costs a few seconds. A missed ticket costs real money. Those two are not equal, so doubt resolves toward speaking.
v1 ships a subset on purpose: the notify path with an explicit confidence label on every alert, and a No Data banner in the app with no push behind it. The ask tier is not built yet.
Reasoning 02
Silence is the state I spent the most time on, because it's the one that looks like success while it's failing. Nothing appears on screen. Nobody complains. You find out it was wrong when you come back to a ticket.
So "no data" had to be its own behavior rather than the absence of one. Not knowing, and knowing it's fine, are completely different situations, and they cannot feel identical from the outside.
Underneath all of it sits one rule that nothing can override. If the agent is confident about something severe, a tow-away zone, it warns you. No setting, no learned preference, no quiet mode gets past that. Everything else in the product is tunable. That isn't, and it can't be, because the moment a user can configure their way into silence on a tow zone, the product has traded away the thing it exists for.
Five checks, in order, and every one of them has to hold: you were plugged in, driving, and then unplugged; the location fix is good to 25 meters or better; the curb side is known, by heading or by a strong enough likelihood; a rule exists for that curb; and the rule bites within the next 48 hours. Only then does it send one notification, with its confidence stated. Each check also has a defined outcome when it fails, which is the part that took the work. Silence has to mean one thing, and the agent has to know which thing.
A failed check is not one behavior, it is four, and the agent picks by what it knows. When a drive ends somewhere it could not see, it speaks anyway: "Curb couldn't see where this drive finished. Check the signs where you parked." When the fix at the unplug is stale or shows the car still moving, it withholds rather than guesses: the park is held as uncertain, replaced by the first real fix, and the curb side is refused instead of being taken from a heading the car was holding blocks earlier. If the car keeps moving, the fallback follows it until it has rested five minutes. When several rules bite on the same curb at once, it decides, severity first and then confidence, and keeps the reasoning for why that one won. And when there is no rule for the block, it stays silent on the lock screen and says No Data in the app. The failure worth designing against is the one that does not announce itself, so each of these was given its own visible behavior before the happy path was built.
Reasoning 03
Everything above assumes the app knows you parked. For a while it did not, not reliably.
The first version worked that out from motion. A phone can tell driving from walking, so the moment you switch from one to the other is a park. It is the obvious approach, and it is what my own design docs called for.
Four field drives killed it. The motion detection was wrong in both directions, missing real parks and inventing ones that never happened, and when I traced the silent failures they all came back to the same place. The part meant to tell the app when to speak was the least reliable thing in the system.
So I stopped inferring it. What ships instead is a contract: plug your phone in when you drive, unplug it where you park.
I took the trade because of what it buys. The plug bounds the app's attention, so plugged and driving is the only state where Curb is watching at all. Never plugged is not a failure, it is the app being deliberately asleep.
Which turns the silence from ambiguous into defined, and that is the whole point. A guess about whether you parked produces a quiet that could mean anything. A gesture you made on purpose produces a quiet that means one thing.
V1 vs V2
v1 detects a park from the plug. Plug in when you drive, unplug where you park, and plugged and driving is the only state where Curb is watching. Behind that sits a fallback for the drive the app knows is live but that ends unplugged: it follows the motion until it rests, then records a provisional park. That fallback is the part being worked on now. Today it is one flat five-minute timer, which invents a park in gridlock and announces a real one after you have walked off. The design in progress keeps a ledger of recent stops, so the park is the last stop before you walked away, and adapts its patience to the geometry of the curb.
v2's mission is detection without a plugged drive. The first signal is already named in the project's documents: the car's Bluetooth as a second contract, and the reason it earns its place is not only to notice a drive but to falsify a park the app is still holding after the car has left.
Once the app believes your car is somewhere, that belief can outlive the truth. You drive without plugging in. Someone else moves the car. You forget. The app keeps the old spot and keeps warning you about it, confidently, about a curb your car left two days ago.
I found this in my own logs. A drive on a Saturday, a belief that stayed standing through Monday, and scheduled alerts still firing for a block the car was nowhere near.
So the belief ends the way it began, with motion. When the app sees the car driving again, the park is over: its alerts are cancelled and the belief is retired, kept one step behind in case that drive turns out to be somebody else's car, a rideshare or a friend's. Then the next alert carries a "Not my car" button, and one tap puts the previous park back with its warnings re-armed. Being killed does not touch the belief: on relaunch the app restores it from the ledger, re-arms the fence around the car, and re-derives the alerts if the rules changed underneath. And a drive the app saw start but never saw end, a plug-in that no park ever closed while the phone is unplugged when the app wakes, expires it outright: the car moved without the app seeing it, and the belief it left behind is false.
The rule underneath is the same asymmetry as everywhere else in this project. A warning cancelled by mistake costs you a ticket. A warning left standing when it has gone stale costs you trust. Those are not the same price, so the app leans toward keeping the warning and hedging it, and never toward quietly dropping it.
Reasoning 04
Not every judgment is the agent's to make. I mapped its tasks by how often they come up and how much judgment each one takes, which sorts them into work it can just do, work that needs a safety net, and work that belongs to a person.
Most products' hardest cell is the rare, high-judgment one, and the usual answer is to keep a human in the loop. Curb's hardest cell is the constant high-judgment one. The decision to speak or stay quiet happens every single time you park, with real money on it, and there is nobody available to ask. You're getting out of the car.
It couldn't be handed back, which is precisely why it needed a spec. The confidence tiers, the safety floor, the one-tap override: all of that is scaffolding for a single judgment the agent has no choice but to make alone.
| Low judgment | High judgment | |
|---|---|---|
| Often | Delegate cleanly: park detection, sign reading, citation inference | Delegate with scaffolding: confidence scoring, the interrupt decision |
| Rarely | Delegate quietly: routine data ingestion, contribution intake | Keep a person in it: conflicting sources, downgrading a real restriction, learning a preference |
The one I want to point at is downgrading. Deciding a spot Curb used to flag as risky is probably fine now is the highest-stakes call the background makes, because a wrong downgrade produces silence over a live restriction. The agent proposes it. The person can veto it. That asymmetry is deliberate.
Reasoning 05
I assumed a parking rule belongs to a block. It was such an obvious assumption that I never wrote it down as one, which is how the good mistakes usually work.
Then I ran the logic against the city's real citation records.
On that block, the odd-numbered side had 251 street-sweeping tickets on Fridays. The even side had 192, on Wednesdays. Same block, opposite curbs, completely different rules. And it wasn't a quirk of one street: across the segments where both curbs could be resolved, 87% carry a different sweeping rule on each side.
The part that actually hurt was that the data couldn't see the difference it was showing me. LA geocodes citations to the street centerline, so both curbs come back with an identical longitude. The only thing separating them was whether the house numbers were odd or even.
So a spot isn't a block. It's a block and a side. I rewrote the agent's core identity model, which meant side of street had to be part of the primary key from the beginning rather than something bolted on later.
Fine. Rules belong to a side. Which side are you on?
The obvious approach is position: look at where the phone is, relative to the middle of the street. It doesn't work, and more accurate GPS would not save it. The two curbs sit about 11 meters apart, and a phone's location fix is good to somewhere between 5 and 20 meters. You'd be measuring 11 meters with a 20 meter ruler. That stays marginal forever, no matter how much you polish it.
What worked was changing the question. Not where is the car, but which way was it going. In the US you park on the right of your direction of travel, and both of those numbers already exist: the phone knows the heading you were driving, and the map knows which way the street runs. Comparing two directions is sturdy in a way that measuring an 11 meter gap never will be.
It still can't always tell, and the design has to hold that. On a one-way street both curbs are legal, so direction settles nothing. If you parked facing against traffic, it gets the side exactly backwards. If the location fix is too rough, it can't resolve a side at all.
Which loops straight back to where this case study started. The answer to "which curb are you on" turned out to be the same answer as everything else here. Say what you know, say how well you know it, don't dress up a guess. When the app can't work out the side, it says so instead of picking one.
The first version of the side logic compared the car's offset from the centerline against the raw GPS error bar, asking whether the car could plausibly be past the middle of the street. Three field checks caught that it was the wrong question.
The right one is which of two curbs, a street's width apart, better explains the measurement. That turns it into a likelihood ratio rather than a threshold, and it degrades gracefully: a 3 meter fix can resolve a 1 meter offset, an 8 meter fix needs about 7, and a 40 meter fix resolves nothing at all and is honest about it.
There's a broader lesson in there that shaped how I write specs now. A fixed constant is a guess about every street at once. A wide boulevard and a narrow residential street are not the same problem, and a single number pretends they are. I learned that twice on the same street before it stuck.
Reasoning 06
Here's the test of whether any of this is more than a nice document.
One month into the build, a block near La Fayette Park started returning something incoherent: "white zone, any time" and "you are at a meter" at once, with a headline telling people not to park in what is a perfectly legal metered space. The proposed fix was to treat white zones and meters as mutually exclusive and let the meter win.
That fix would have been wrong, in a specific and familiar way. I went and read the sign. The curb is time-shared: white zone early morning and evening, metered in the middle of the day, open on Sundays. Both things were true at once. Deleting the white zone would have erased a real 6:30am parking ban.
Which is the exact failure my own spec names as the one thing the agent must never cause. Silence standing over a live restriction. It showed up in the wild, in my own pipeline, and what caught it was walking outside and looking at a pole.
The citation data agreed, once I knew what to look for. 383 white-zone tickets on that segment, heavy at 6, 7 and 8am, then zero at 9am, picking back up at 1pm and peaking around 5. The pattern had been sitting in the data the whole time. I'd thrown it away earlier by assuming the hours were uniform across the day.
This is why I write behavioral specs with named failure modes rather than principles. A principle is something you agree with. A named failure mode is something that catches you.
The build
Curb isn't a mockup. It's a real iOS app that knows you've parked because you unplugged your phone, resolves the rules for that curb from the city's own open data, and sends the notification locally, so nothing about where you park leaves your phone.
The reason that matters for a design role isn't the code. Every behavior above was designed first, written down as a spec with its failure modes named, and then tested against something real. Several of them changed because reality pushed back. A spec is a claim about how something should behave, and building it is how the claim gets checked, which is why the corrections in this case study matter as much as the plan.
Takeaways
Six principles, each tied to the moment on this page that taught it, are collected with the project status on Where it is, and what I learned, the last page of the Curb set.
Behind it
The design work lives in a set of specs written alongside the build. Excerpts shown on request.
The agent's governing principles, each with an authority level and a compliant/violation pair.
Which layer of trust actually fails for this product, and why the other two are minor.
Every task the agent does, sorted by frequency and judgment, with the scaffold each one needs.
Tools, memory, composition and oversight, and the design consequence of each choice.
What the agent did while you were not watching, and how to review a judgment rather than rubber-stamp it.
Where every claim comes from, how it was verified, and what was refused along the way.
Twenty-four field decisions, each with its evidence, its cost, and what would prove it wrong.
The Curb series