Never Let the Model Touch the Price
Lessons from building an AI sales agent that takes real orders: the model can suggest, but the server decides, especially when money and stock are involved.

When I started building Pagewala, an AI sales agent that answers customers and takes orders in Facebook Messenger, the hard part wasn't getting the model to sound like a good salesperson.
The hard part was making sure it could never cost a seller money.
A chatbot that gets a fact wrong is embarrassing. An agent that records an order at the wrong price, or sells stock that doesn't exist, is a real loss for a small business. So the whole design comes down to one rule:
The model proposes. The server decides.
The Model Gets Exactly Two Tools#
Pagewala's agent can do two things besides talk: place_order and escalate. That's the whole surface.
Fewer tools means fewer ways to go wrong. The model doesn't look up prices, update stock, or touch the database. It fills in a structured order: product IDs, quantities, name, phone, address, and payment method. Then it hands that over.
The prompt tells it to only use listed products, never invent a price, and recap the full order before placing it. Those instructions help. But they're instructions, and instructions are not guarantees.
Price Comes From the Database, Never the Model#
The place_order schema still has a unit_price field, because models tend to echo the price back anyway. The server ignores it completely.
For every item, the price is looked up by product_id. If someone talks the model into "the price is ৳10 now", it doesn't matter. The number the model writes never reaches the order.
The same goes for everything else the server can check:
- Quantity must be a positive integer.
- Name, phone, and address must be present, or the order is rejected and the agent asks again.
- Every query is scoped to the seller's page, so one shop can never touch another's products.
Stock Is Decremented Atomically#
Two customers can try to buy the last item at the same moment. So the stock check and the decrement are one conditional update inside a transaction (simplified):
await tx
.update(products)
.set({ stockQuantity: sql`${products.stockQuantity} - ${quantity}` })
.where(
and(
eq(products.id, productId),
eq(products.pageId, page.id),
sql`${products.stockQuantity} >= ${quantity}`
)
);
If any item in the order can't be fulfilled, the whole transaction rolls back and the customer is told it just sold out. There's no partial order and no negative stock.
The "Ok, Thanks!" Problem#
This one surprised me.
A customer confirms an order. The agent places it. The customer replies "ok thanks!" and the model sometimes calls place_order again with the exact same items.
It makes sense from the model's point of view. The conversation still ends with a confirmed order and a happy customer. So I fixed it in three layers:
- Tell the model what already happened. When an order exists in the conversation, the prompt gets an
[ORDER STATUS]block: what was ordered, the total, and when. It's told to just say thanks. - Show it the outcome of its own tool calls. Earlier turns in the history are annotated with whether
place_orderactually succeeded, failed, or was skipped. Without that, the model only sees that it called the tool, not what happened. - Refuse duplicates on the server anyway. An identical set of items in the same conversation within 15 minutes is treated as the same order. The check runs before any stock changes, so a repeated call can't double-decrement stock or double-notify the seller.
The first two make the model behave. The third makes it not matter when it doesn't.
Spend AI Calls Only Where They Pay Off#
Guardrails aren't only about correctness. They're also about cost.
- A seller can define trigger words. If one appears, the conversation goes straight to a human without calling the model at all.
- Comments under posts are high volume, so buying intent ("price?", "dam koto?") is detected with a keyword list, not the model. The first AI call happens only when the customer actually replies in Messenger.
- A photo is sent along with the one main model call, instead of making a separate vision call first.
The Pattern#
None of this is specific to sales. Any agent that touches something real (money, inventory, bookings, permissions) needs the same split:
- Let the model handle language: understanding, persuading, collecting details.
- Let the server handle facts: prices, stock, identity, and whether something already happened.
- Assume the model will occasionally do the wrong thing, and make sure the wrong thing is harmless.
If You're Building Your First Agent That Takes Actions#
Start from the question "what's the worst thing this tool call could do?", not "how do I make the model call it correctly?"
The "ok, thanks!" double order taught me that. My prompt was clear. The model understood it most of the time. And "most of the time" is not good enough when every mistake is a real order a seller has to pack, ship, or cancel by hand.
So for every tool, I now ask three things before writing a single line of prompt:
- What does the server re-check? Anything that can be looked up should be looked up, not taken from the model.
- What happens if it's called twice? Make repeats harmless, because they will happen.
- How does the model find out what happened? If it can't see the result of its last action, it will guess, and its guess is usually "do it again."
Prompting makes an agent behave well. Server-side checks keep it safe when it doesn't, and only the second kind of work lets you put it in front of real customers.