Inspiration
UA Market sells Ukrainian embroidered clothing and hand-made goods to buyers across Canada. Most orders are gifts for someone else: a blouse for a mother in Toronto, a shirt for a grandson's first Christmas. Buyers ask what a shopkeeper answers at the counter: which size, what it is made of, what fits in $150, does it ship to Alberta. The owner, meanwhile, keeps two ledgers in step by hand: the shop's orders and PayPal's payments, captures, tracking numbers and refunds.
We wanted an agent that can actually take a buyer to a paid order, and we wanted the money part to stay boring and correct.
What it does
- Concierge (buyer side). An AI agent in a drawer on every storefront page, in English and Ukrainian. It searches the real catalogue, reads product facts (maker, material, technique, care, sizes, stock), keeps to a spending limit the buyer sets, and prepares a checkout card with goods, shipping, the named tax for the province and the total. The buyer ticks the terms and presses the PayPal button in the card; PayPal asks for approval in its own window; the order is paid. A trace line under every answer lists the shop tools the agent called.
- PayPal checkout everywhere. The same PayPal JS SDK v6 button sits on the checkout page and on the order page, so an unpaid order can be paid later from its email link.
- After the payment. When the parcel ships, the carrier and tracking number are sent to PayPal automatically (seller protection needs them). A cancelled paid order can be refunded through PayPal from the admin.
- PayPal desk (owner side). An agent in
/adminthat reads the shop and PayPal side by side through the PayPal Agent Toolkit. "What needs my attention?" compares them: paid but not shipped, shipped without tracking in PayPal, cancelled but not refunded, open disputes. Every fix comes back as a card; money moves only when the owner presses Confirm.
How we built it
- PayPal Orders v2 over REST (FastAPI): the PayPal order is built from the shop's database only: items with SKUs, item_total, shipping, tax_total in CAD,
custom_id= shop order id,invoice_id= order number,shipping_preference: SET_PROVIDED_ADDRESS(tax was computed for that province). Capture on the server; the order turns paid only when the captured amount and currency equal the order total, otherwise a problem is opened for the owner.PENDINGwaits for the webhook. - Webhooks verified with PayPal's verify-webhook-signature API:
CHECKOUT.ORDER.APPROVEDcaptures an order whose buyer closed the page before the capture call,PAYMENT.CAPTURE.COMPLETEDpays a pending one,DENIED/REVERSEDraise a problem. Idempotent by event id. - Tracking and refunds: Orders v2
/trackwith PayPal carrier codes (Canada Post, Purolator, …,OTHER+ name), retried by the shop's five-minute timer if PayPal is down;/v2/payments/captures/{id}/refundfrom the admin. - PayPal JS SDK v6 web component (
paypal-button),createPayPalOneTimePaymentSession, started inside the click with a promise for the order id so the popup is never blocked. - Agents: an OpenAI-compatible tool loop with gpt-oss:120b on Ollama Cloud (gpt-oss:20b as a fallback when the main model stalls). The concierge has five shop tools; the desk uses the PayPal Agent Toolkit (
paypal-agent-toolkit1.11, Python, framework-freePayPalAPI.run) forget_order_details,list_disputes,get_dispute,get_shipment_tracking,list_transactions, plus shop tools. - Guard rails in code: the model picks SKUs and quantities; prices, shipping and tax come from the same quote function the checkout page uses; the spending limit is enforced by the bag and checkout tools; the agent cannot place or pay an order (the browser places it through the normal checkout endpoint when the buyer presses PayPal); the desk only proposes.
- Storefront: Next.js 16, a design system built on the counted-thread embroidery chart. Backend: FastAPI, SQLAlchemy, PostgreSQL, Caddy. 248 backend tests, 20 of them for PayPal and the agents (mock PayPal, scripted model).
What changed during the hackathon
UA Market existed before October as a storefront and fulfilment system with Stripe and Interac e-Transfer, with no PayPal and no AI. Everything above was built between 15 October and the submission. The public repository starts with a baseline commit (the shop on 17 September 2026) and every later commit is hackathon work.
Challenges
- A hosted model sometimes stalled for two minutes on one turn: each call is now capped and the last retry goes to a smaller model.
- With a budget filter, the search tool hid a $340 blouse and the model told the buyer the shop did not carry it. The tool now returns such pieces as "outside the price range", so the answer is "it costs more than your limit".
- A rehearsal hit our own rate limit on capture while PayPal's webhook had already captured the payment. The button now asks the order before reporting a failure.
What we learned
Agentic commerce works when the agent prepares and the human approves in a payment UI people already trust. The PayPal approval window is the consent step, and the code around the model holds the prices, the limits and the money.
What's next
Pay Later messaging and Venmo, locker pickup in the concierge, and a real catalogue from Ukrainian workshops (the current one is a demonstration catalogue, marked as such on every page).
Built With
- caddy
- cloudflare
- docker
- fastapi
- gpt-oss
- next.js
- ollama
- paypal
- paypal-agent-toolkit
- paypal-js-sdk-v6
- paypal-orders-v2
- paypal-webhooks
- postgresql
- python
- react
- sqlalchemy
- typescript
Log in or sign up for Devpost to join the conversation.