Suzanne Sallaj

Case study · Agent behavior design

Curb

By Suzanne Sallaj

· 19 min read · Curb series, part 1 of 5

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.

10.4Mreal LA citations behind the rules engine
87%of resolved blocks are swept on a different day on each side
iOSa real app I'm building, not a mockup
The problem

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.

My role

Behavior design, the specs, the data engine, the Agentic Walkers, and the iOS build, working with Claude Code.

What works

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.

What the data changed

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.

Live project

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

What Curb looks like on the phone

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.

0:00
A park at N Kenmore Ave on a Wednesday when street sweeping is active. When parked, a notification occurs (restricted during screen recordings) of the parking restriction. Opening for more details shares a view of time, location, rule, and confidence.
Lock screen notification reading: move car before 8am tomorrow, street sweeping, confidence level high.
Confident: a deadline you can act on.
Lock screen notification with a low confidence label and an invitation to share feedback.
Unsure: hedged, and it asks for help.
Curb detail screen showing the parked spot on a map, the fine, the location, the rule, and the confidence level.
Opening the app provides a detailed view of time car was parked, where, which rules apply, and the confidence level.

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

What makes Curb an agent, not an app

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.

On the left, one goal: protect me from getting a ticket, set once and never touched again, with a list of things the user is explicitly not signing up for. On the right, nine judgments the agent takes on instead, grouped into delegated cleanly, delegated with a scaffold, and kept human.
Mapping the user's goals is almost a wasted exercise here, and that turned out to be the useful finding. They want one thing, they hand it over once, and they never revisit it. So the delegation work sits entirely on the other side, where a single standing instruction has to become every judgment it implies.

Reasoning 01

Confidence is shown by what the agent does, not by a number

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

Curbnow
Move car before 8am tomorrow
Street sweeping: no parking 8-10am Wednesdays
Confidence level: High
$65 fine

Medium confidence

Curbnow
Move car
Street sweeping: no parking 9-11am Tuesdays
Confidence level: Medium
$65 fine

Low confidence

Curbnow
Move car
Street sweeping: no parking 9-11am Tuesdays
Confidence level: Low
$65 fine
Share feedback to contribute
A chart of the agent's behavior. The horizontal axis is confidence level, the vertical axis is consequence severity from ticket to tow. A stay-silent band on the left, then three notify bands at below 40 percent, 40 to 70 percent, and 70 percent and up confidence. The boundaries slope leftward as severity rises, so the agent speaks at lower confidence when the stakes are higher.
Same action, three different ways of saying it. The interesting part is that the boundaries lean, so the bar to stay quiet rises as the consequence gets worse and the same confidence that earns silence on a two hour limit gets you a warning on a tow-away zone. The cutoffs are the ones the app ships with, and they stay provisional until they can be fitted against the real accuracy of each neighborhood.

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 full four-tier model, and what v1 actually ships

The spec defines four behaviors, each traced to the principle doing the work:

ConfidenceBehaviorWhy
HighNotify, stated flatlyBe decisive on facts
MediumNotify, hedgedBe honest about uncertainty
Low, restriction plausibleAskDefer on genuine judgment calls
Confident it's clearStay silentProtect 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.

Try it: same spot, different certainty
Curbnow

Reasoning 02

Deciding when the agent stays quiet

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.

What has to be true before it speaks

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 chain of five checks that must all hold before the agent speaks: the plug, the fix, the curb, the rule, the clock. Each has its own defined outcome when it fails.
The five checks, and what happens when each one fails. Four of those silences are earned, in the sense that the agent knows exactly why it is quiet. The rule check is the one that is not: on the lock screen, no data and you are clear produce the same nothing. Only the app's home screen says No Data.

What it does when a check fails

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.

Four ways the app fails: it speaks when a drive ends somewhere it could not see, withholds when the position is uncertain, decides when rules stack, and stays silent when no rule exists.
The four failure behaviors. The interesting one is the second: it knows the position it was handed is not trustworthy, so it holds the park as uncertain and refuses the curb rather than guessing. The last row is the one everything else avoids.

Reasoning 03

How the park signal is detected

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.

Ending the belief when the car moves again

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.

Most of the hard problems here were not about what the agent should say. They were about whether it had any business speaking at all.
How the park is found and what ends the belief: plug in and drive, unplug, check whether the car is still moving; the unplug spot or a five-minute rest becomes the park. The belief ends when the car moves again and is kept one step behind for Not my car. A killed app restores it on relaunch. Nothing expires on a timer.
Finding the park, then ending the belief. At the unplug the app checks whether the car is still moving. At rest, the unplug spot is the park. Still at driving speed, and the fallback follows the motion until the car has rested five minutes, which becomes the park. After that the belief ends only when the car moves again, seen live or proven at the next wake, and the retired park is kept one step behind for "Not my car." Nothing decays on a timer.

Reasoning 04

What the agent decides, and what it hands back

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.

What sits in each quadrant
Low judgmentHigh 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

The week real data changed the design

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.

Diagram of one block of Vermont Avenue. The odd-numbered curb drew 251 Friday sweeping citations, the even-numbered curb drew 192 Wednesday citations, and both are geocoded to the identical point on the street centerline.
One block of Vermont Ave. Two curbs, two different sweeping days, and a single shared coordinate. The city geocodes every citation to the middle of the street, so in the data the two sides are the same place.

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.

Working out which side of the street the car is on

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.

This is the part I'm proudest of, and it isn't the assumption. It's the correction.
Why I'm suspicious of constants now

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

The behavioral spec caught a bug in my own build

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

It is a real app, not a mockup

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

What Curb taught me about designing agents

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 specs behind it

The design work lives in a set of specs written alongside the build. Excerpts shown on request.

Behavioral specification

The agent's governing principles, each with an authority level and a compliant/violation pair.

4,200 words

Trust architecture diagnosis

Which layer of trust actually fails for this product, and why the other two are minor.

1,700 words

Delegation map

Every task the agent does, sorted by frequency and judgment, with the scaffold each one needs.

1,600 words

Architecture diagnostics

Tools, memory, composition and oversight, and the design consequence of each choice.

2,600 words

Audit interface spec

What the agent did while you were not watching, and how to review a judgment rather than rubber-stamp it.

2,400 words

Provenance record

Where every claim comes from, how it was verified, and what was refused along the way.

1,200 words

Decision ledger

Twenty-four field decisions, each with its evidence, its cost, and what would prove it wrong.

5,900 words

The Curb series

  1. 1Curb You are here19 min
  2. 2Designing how the agent reasons about data10 min
  3. 3The Agentic Walkers46 min
  4. 4AI in the Wild: the day the agents went rogue for the mission13 min
  5. 5Where the build of Curb is, and what I learned5 min

Share this post

Written by Suzanne Sallaj

Product designer working on agentic AI, six years across healthcare, fintech and consumer.