Inspiration

I've spent my career in payments, working with fintechs and merchants on both ends of the market. On one end: very large companies and top-tier financial institutions. On the other: startups, scale-ups and the small businesses they serve. Working across many industries, I got to see each player's problems from its own side of the table.

One gap came up again and again. Large buyers get treated like partners. They negotiate volume prices, get payment terms, and have procurement teams and banking relationships behind them. A small business buying the same goods gets the list price and pays up front. The product is the same and so is the supplier, but the price isn't, only because of scale.

Take a chain coffee shop and the independent café down the street. They buy the same oat milk. The chain pays less because it orders by the pallet. The café orders a few cases a week, so it pays the "alone" price on every supplier's tier sheet.

Put ten cafés together and they're a pallet buyer too. Small businesses already know this. What stops them is trust and admin, and I've seen both sides of that:

  • Shops won't send money to a group order that might never happen.
  • Suppliers won't give a volume price to buyers they can't collect from. They don't want ten small invoices to chase, or disputes they have to settle by hand.
  • Payment providers already have the tools to fix this (holding funds, splitting payouts, invoicing, refunds). Small businesses rarely get those tools put together for them.

Shoal is my answer. It's a short buying round where PayPal holds each shop's money, AI agents negotiate, and code decides every amount. The goal is to give a small business the same buying power and payment protection a large company gets as standard.

What it does

  1. A shop joins a round. It sees the price showing now plus delivery (a per-drop fee and a per-unit fee). PayPal authorizes that amount. The money is reserved, not charged.
  2. The tiers step down as shops join. On the receipt-paper round, 1,100 rolls unlock the 800+ tier at $1.13 a roll, down from $1.43 alone.
  3. Two AI agents negotiate. One speaks for the shops and one for the supplier (each runs on Claude or Gemini). They have 8 turns to agree a price below the current tier, within the supplier's published concession policy.
  4. The round closes.
    • If the group reached a tier, PayPal captures each shop at the group price, never more than it reserved. Each shop gets a PayPal invoice in the supplier's name, recorded as paid.
    • If the group missed the first tier, every authorization is voided and nobody is charged.
  5. The supplier ships. A PayPal Payout sends 90% to the supplier, and Shoal holds back 10%.
  6. A shop reports a problem (missing, damaged or short goods, with an optional photo). Claude proposes a refund, a person approves it, and PayPal refunds the shop from the holdback. The refund is recorded on that shop's invoice.
  7. Every order is delivered or resolved, and the rest of the holdback goes to the supplier.

A retail benchmark from Channel3 shows what the same product costs elsewhere, so a shop can see the saving is real.

How I built it

  • Next.js 16 and React 19, written in TypeScript and hosted on Render.
  • PayPal: Orders and Payments (authorize, capture, void, refund), Payouts and Invoicing, on the PayPal Server SDK and the React PayPal JS button.
  • Claude (Anthropic SDK) runs claim triage and pack sizing for the benchmark. Claude or Gemini can run either side of the negotiation, set by an environment variable.
  • AG Grid runs the supplier's allocation grid and the purchase-order CSV export.
  • Bryntum Scheduler draws the round timeline, one lane per shop.
  • Channel3 supplies retail offers for the benchmark. Claude only says whether each offer is the same product and how many units are in the pack. Code turns that into a unit price and takes the median.

The rule I built around: the AI proposes, code decides. No agent output reaches PayPal without passing a check in lib/.

  • lib/guard.ts checks every offer against the supplier's tiers and concession policy. If the volume $$q$$ is within 10% of the next tier's minimum $$V_{t+1}$$, the supplier gives up to half the price gap. It never goes below its absolute floor:

$$ p_{\min} = \max\Big(p_{\text{floor}},\; p_t - \big\lfloor \tfrac{1}{2}\,(p_t - p_{t+1}) \big\rfloor\Big) \quad \text{when } q \ge 0.9\,V_{t+1} $$

Prices are in whole cents, and the concession rounds down so the supplier never gives more than its share. On the receipts round, $$q = 1{,}150$$ rolls against the 1,200-roll tier, so $$p_{\min} = 113 - \lfloor 11/2 \rfloor = 108$$ cents. The agents settle between $1.08 and $1.13.

  • lib/round.ts works out reservations, delivery and captures in integer cents. A shop that buys $$u$$ units at group price $$p_g$$ pays

$$ \text{capture} = \min\big(\text{reserved},\; u \cdot p_g + d_{\text{drop}} + u \cdot d_{\text{unit}}\big) $$

  • lib/settle.ts splits payouts and caps each refund at what the shop paid for goods and what's still held back.
  • lib/deal.ts signs quotes and deals, so a price can't change between the screen and PayPal.

Verified end to end on the hosted demo against the PayPal sandbox:

Step Amount
Reserved: 50 rolls at $1.13 + $10.00 delivery $66.50
Agreed price $1.08 a roll
Captured ($2.50 never taken) $64.00
Paid to the supplier at shipping (90%) $57.60
Refunded from the holdback for 5 damaged rolls $5.40
Rest of the holdback paid to the supplier $1.00

The buyer's and the supplier's PayPal activity show the same amounts. On the oat-milk round, one real shop was below the first tier, so the round failed and PayPal shows the $247.20 authorization voided.

The demo video is made from the live app, so every number on screen is a real sandbox transaction. Playwright drives Chrome through the walkthrough, Gemini text-to-speech reads the script, and Remotion renders the video with subtitles.

Challenges I ran into

  • Authorizations expire. PayPal honors an authorization for three days, so a round lasts three days. Longer rounds would need saved payment methods (PayPal vault) and a charge at close.
  • Agents will try to break the rules, and that's useful. The shop agent knows the supplier's floor and opens just under it to test it. The guard rejects that offer, and the rejection is shown in the negotiation. I had to make the agents keep the negotiation moving:
    • A reply that makes no move is asked once more before the turn is lost.
    • If Gemini is overloaded, Claude makes that move, and the turn says so.
  • Not every product is on Channel3. Retail offers come in different pack sizes, so Claude reads each listing for pack size. It never supplies a price, and with fewer than 3 usable offers there's no benchmark. Channel3 doesn't stock oat milk, so that round says it has no benchmark.
  • The invoice shows as a second line. Shoal records each invoice as paid, so the buyer's PayPal activity shows an "Invoice Received" line next to the payment. It's a record of the same payment, not a second charge, so the receipt shows each shop's single capture to make that clear.
  • A public demo that strangers can't break. Each browser gets its own copy of both rounds, and the negotiation and triage calls are rate-limited, so one visitor can't spoil another's demo or drain the model credits.

What I learned

  • Putting agents inside fixed rules worked better than asking them to be careful. Once the guard owned every price, the agents could negotiate hard, and the worst result was a rejected offer.
  • PayPal's authorize-then-capture flow is what makes group buying work: a shop can commit with nothing charged, and a failed round costs nobody anything.
  • Doing all money in integer cents, with a test for each rule, saved me from rounding bugs at every step from reservation to refund.

What's next

  • PayPal multiparty, so shops pay the supplier directly and Shoal takes a platform fee without holding funds.
  • Invoices from the supplier's own PayPal account.
  • Persistent storage, logins and PayPal webhooks, so a round stays correct if a browser closes mid-payment.

Built With

  • ag-grid
  • ai-agents
  • anthropic
  • bryntum
  • channel3
  • claude
  • eslint
  • gemini
  • gemini-tts
  • github-actions
  • google-genai
  • nextjs
  • node.js
  • paypal
  • paypal-js-sdk
  • paypal-orders
  • paypal-payouts
  • playwright
  • react
  • remotion
  • render
  • tailwindcss
  • typescript
Share this project:

Updates

Submission history