Inspiration
Gold prices move every minute, so a noon quote is rarely a three o'clock price. A local customer in a Thai gold shop pays cash or PromptPay at the counter and the problem never arises. A tourist, expat or online buyer cannot: they want a price in English, and they want the shop to hold the piece until they arrive. Shops answer that today with Line messages and screenshots, and a lot of "sorry, it sold". I already had a working gold POS, so the question was how to let a remote buyer commit to a piece with a real deposit without adding a second system for the staff.
What it does
AGIS is a point-of-sale for Thai gold shops with an in-chat PayPal reservation flow for buyers who are not in the shop.
- A customer opens
/reserveand chats in English with a tool-calling AI agent. It searches the live catalog, suggests up to three in-stock pieces, and loate for 15 minutes: weight × purity × liverate + labour fee. - Once the customer gives a name and a phone the piece and creates a PayPal order for a10% deposit. Paying it holds the piece and the quoted price for 48 hours. The customer gets a short reference (R-XXXXXX), a PDF deposit receipt, and can pthe chat later to check status and balance.
- Staff see the paid booking at
/reservations, and at collection the existing Cash / PromptPay checkout rings up the balance. Stock and held counts move atomical normal sales report.
How we built it
React 19 + Vite on the front, Express 5 + Preployed to Render. The agent is a small toolloop over a Messages-style API with four tools: search the catalog, quote a price, create a reservation, and look up a reservation. The tools never accept amounts, price; every number in the chat comes froma tool result.
PayPal is Orders v2 called directly with fetch. Money is parsed as integer minor units, and capture results are classified as completed, refused, or unknownate machine: PENDING → CAPTURING → PAID →COLLECTED, with EXPIRED, CANCELLED and REVIEW as exits. Capture is single-claimant inside a transaction so two tabs pressing the button cannot double-capture, adempotency key so a double tap at the counter cannot ring up two sales. Uniqueness is enforced by the database, not by lookups in the app.
The whole thing is covered by 81 server tests on node:test, each file on its own fresh migrated SQLite, including the PayPal client, the state machine, expiry, peath.
Challenges we ran into
- PayPal sandbox in Thailand: a Thai sandbox_VIOLATION at approval for every buyer andcurrency. A US sandbox merchant settles THB captures fine, so the demo runs on that; a real shop would use its own business account.
- Keeping the agent honest. Early versions quoted from memory or repeated the quote on every turn. The fix was a strict chat contract in the system prompt plver-computed prices.
- Concurrency on SQLite: two capture requests, a capture racing an expiry sweep, a stale CAPTURING row when a PayPal order lapsed. Each one became a test before
- Mixed Thai and Latin text in the PDF receipts needed a small run-placing helper so both scripts sit on the baseline.
Accomplishments that we're proud of
- The full judge path works on the live site: chat → quote → PayPal sandbox deposit → staff Collect, with no manual steps in between.
- A late whole-repo audit removed four dependencies and about 25,000 lines of vendored tooling and dead code, replacing bcrypt and JWT with node:crypto an for database unique constraints. Every teststill passed.
- Staff never learned a new screen for paymeash / PromptPay dialog they already use, with the deposit already subtracted.
What we learned
- Spec the state machine before writing the agent. Four spec revisions on paper were cheaper than one refactor in code.
- An agent is only as trustworthy as its tools. Narrow, server-owned tools made the prompt short and the behaviour predictable.
- Sandbox constraints are product constraints. Currency and merchant-country rules shaped the demo as much as any UI decision.
What's next for AGIS (Advanced Gold and Inventory System)
- Refund the deposit from the staff screen whe forfeit.
- Thai-language chat alongside English.
- Photos of each piece in the chat, taken frready exist.
- Multi-item reservations and a per-shop deposit percentage.
- Move from SQLite to Postgres for multi-bra
Built With
- better-sqlite3
- express.js
- minimax
- node.js
- paypal
- paypal-orders-api
- pdfkit
- prisma
- react
- render
- sqlite
- tailwindcss
- typescript
- vite
- zod