Inspiration

Small teams lose a surprising amount of time on purchasing. Someone finds an item, someone else approves it, someone pays. An AI agent could do most of that, but an agent that holds a payment method and has no hard limits is a real risk. I wanted to build the opposite: an AI buyer that does the legwork, while the rules and the final decision stay firmly in human hands.

What it does

Steward lets a team describe what they need in plain language, for example "a waterproof hiking jacket under $100".

  1. A Gemini agent searches real products through Channel3 and picks the best-value, in-stock option.
  2. Every candidate is checked against a spending policy: a per-purchase cap, a monthly budget, and blocked categories.
  3. The agent proposes one purchase as a slip showing the item, the price, its reasoning and the checks the policy verified.
  4. Nothing is charged until a person clicks Approve. Then the server creates a PayPal order, the buyer approves it in PayPal, and the server verifies and captures the payment.
  5. A history page shows the monthly budget, completed purchases and an audit trail of every step the agent took.

If a request breaks the rules, such as something over the cap or a gift card, the agent can't buy it. The panel explains why nothing was bought.

How I built it

  • Agent: Gemini function calling (@google/genai) with three tools: search_products, check_policy and propose_purchase. The model decides what to search for and which option is best.
  • Guardrails in code, not prompts: the policy is a plain function in lib/policy.ts. It runs again inside propose_purchase, again when the checkout is created, and once more right before PayPal captures the payment, so the model cannot talk its way past it. Prices always come from my own search results, never from what the model says.
  • Products: Channel3 supplies live product data (titles, prices, retailers, images). A small fallback catalog keeps the demo working if the API is unreachable.
  • Payments: PayPal Orders API v2 (create, approve, capture) on the sandbox. Before capturing, the server fetches the order from PayPal and checks that it belongs to the recorded proposal and that the amount matches exactly. Idempotency keys stop a page refresh from creating or capturing twice.
  • Stack: Next.js, TypeScript, deployed on Render. It starts from the PayPal x AG Grid starter project, and the checkout, agent, policy, ledger and interface are my own work.

Challenges

  • Free-tier limits: Gemini's free quota allows only a handful of requests per minute, and the model is sometimes busy. I added retry handling and cut the number of model calls per run.
  • A changed SDK: the Channel3 SDK's response shape differed from its documentation example, so I inspected the raw response and wrote a small adapter that turns it into a simple product format.
  • Sandbox permissions: PayPal refused the invoice endpoint for my sandbox app, which broke the starter dashboard, so I made it tolerate endpoints that fail instead of crashing.
  • Trusting only the server: it took deliberate design so the browser never supplies a price or a decision that matters.

What I learned

The AI is best used for the judgment calls (what to search, which option is best value, how to explain it), and plain code is best for the rules about money. Making the policy a function that can't be bypassed made the agent both safer and easier to demo.

What's next

PayPal webhooks for payment confirmation, a database in place of the local ledger file, per-team and per-category budgets, and an approval flow for a manager rather than the person asking.

Honest limits

This is a prototype on the PayPal sandbox, so no real money moves. Product links point to the retailers' own sites, and PayPal can't pay those retailers directly, so in this prototype the payment goes to the merchant account as a purchase order the app records, with the retailer link kept as the fulfilment reference. On the free host the ledger resets when the service restarts.

Built With

Share this project:

Updates

Submission history