Inspiration
Buying in bulk is cheaper per unit. The catch is the minimum — suppliers sell by the case, and a case is usually more than one household wants.
The obvious workaround is to split a large order with other people. But then someone has to find those people, figure out whether everyone actually wants compatible products, compare supplier terms, collect commitments, receive the shipment, organize pickup, and deal with whoever changes their mind.
That coordination is the part I wanted to remove.
Pool starts from individual needs instead. People say what they already buy, independently. The agent looks across those declarations for combinations that could become a viable order, investigates the promising ones, and leaves everyone alone when the economics do not work.
The group never has to organize itself. Pool discovers when the group can exist.
What it does
You declare something you already buy — a coffee, three bags, roughly monthly, and whether another brand would do. That's all Pool needs to start.
Pool finds compatible demand in your community, evaluates supplier minimums and case structure, checks whether the economics can work, and checks each buyer's own constraints. Once a neighbour agrees to receive the shipment and be compensated for the handoff, Pool can finalize the landed cost.
A pool locks only when the buyer constraints, supplier terms, host arrangement, and platform economics all work at the same time.
Nobody creates the group. Pool discovers that the group can exist. If the product ever becomes "create a group and invite your friends," the core idea is gone.
The choice I didn't hard-code
In the demo, Pool has two plausible orders it could assemble.
Kestrel has more demand behind it: 23 bags across 8 people, and it's the coffee the member actually named. Pool costs it first, and deterministic code refuses it: $367.19 together versus $360.00 buying separately.
So the agent tries the other option.
Harbourstone has 18 compatible bags, exactly three whole cases, and $69.18 in savings. That order forms.
I didn't hard-code that choice. I tested five simple heuristics — first in the list, most demand, most people, fewest exclusions, and most headroom over the minimum — and all five prefer Kestrel.
The useful information only appears after Pool actually costs the candidate and sees what comes back.
How I built it
Strands is load-bearing, not decorative. Pool uses one strands.Agent, seventeen typed @tool functions, and a HookProvider that registers the model and tool lifecycle events.
The bounds live in the execution loop rather than in a prompt asking the model to behave. Tool-level bounds cancel the call so a run can wind down cleanly; run-level bounds stop the run and record the outcome.
I also wrote a custom strands.models.Model, a deterministic planner that implements the SDK's provider interface and emits the same kind of tool-use events the coordinator expects. It occupies the model position in the public demo: real Strands event loop, real hooks, real tools, real domain logic, zero model tokens.
The deployed AgentCore runtime puts strands.models.BedrockModel with Amazon Nova Lite in that same model position. Both routes share the same coordinator, hooks, tools, and deterministic domain logic.
The boundary is simple: the model decides what to try; deterministic code determines what is true.
Money, quantities, eligibility, timing, case fitting, policy verdicts, and final viability never come from model prose. A thirteen-check evaluator gates the final lock, and the agent cannot override it.
The application is deployed on AWS Lambda, DynamoDB, and CloudFront, with a separate Amazon Bedrock AgentCore Runtime using Nova Lite against the same underlying coordination system.
What happens when you attack it
I tried several failure cases against the deployed runtime on Nova Lite.
- Tell the agent to set a price. It reached
no_actionin one tool call — not because a filter caught the wording, but because no tool accepts a price. There is no argument through which the model can inject one. - Invoke the agent twice at once on one workspace. The system still produced exactly one pool. Coordination relies on DynamoDB conditional writes and idempotent domain mutations rather than an in-memory lock.
- Try to advance without a host. The agent called
issue_final_offerearly, receivedissued: false, and the run ended normally. It did not retry around the precondition. - Try to clear the workspace. The runtime's IAM role does not have a delete permission that would let it erase the partition. That operation is unavailable by construction.
Challenges
Cases do not divide evenly into demand
A supplier might sell six units per case while the community wants seventeen.
Rather than quietly buying the remainder and billing someone for it, Pool searches for a buyer set that fills whole cases exactly. If it cannot find one, the pool does not lock and explains why.
Landed economics are easy to get subtly wrong
The platform fee is a share of gross savings. Card processing has to be grossed up per buyer so the charge still covers the processor's cut of that same charge.
Computing it the obvious way under-recovers by a few cents. That sounds minor, but at transaction scale it becomes a silent subsidy — exactly the kind of error the deterministic pricing layer is supposed to prevent.
A number that was really two different answers
Late in development, I found a savings check reporting "net landed savings after all costs" while host pay was still zero because no host had accepted yet.
As a gate, the calculation was doing what it was supposed to do. As a sentence, it overstated what was actually known and printed a larger number beside the smaller one the member saw on the order.
The fix was not new arithmetic. It was naming the basis honestly: before a host accepts, the UI now says the savings figure is before host pay. Once host compensation is known, it can truthfully call the result after all costs.
What I learned
The useful question was not "can the model do this?"
It was: which decisions must never be the model's?
Writing that down made the tool surface, authorization rules, and tests much clearer.
I also learned that an agent doing nothing can be correct. If an order is not worth forming, nobody should have to reject it manually. no_action is therefore a recorded, first-class outcome rather than a failure.
And every check that gates a transaction eventually becomes a sentence a person reads. I had been reviewing those checks as booleans. The savings-label bug was a reminder that the explanation is part of the correctness.
What's next
The next step is a controlled pilot with operator-entered, verified supplier offers and strict order-value limits.
Before any real money moves, Pool would also need a clear merchant-of-record and funds-custody model, plus a real host payout rail before recorded compensation could honestly be called "paid."
What's real and what isn't
The community, households, and supplier quotes in the verification walkthrough are synthetic.
Payments and supplier purchases are simulated: no card is charged and no supplier is contacted.
Compatibility, case fitting, pricing, the agent loop, state transitions, and the records you can read back are real code running against that synthetic data.
No users, real-world savings, or traction are claimed.
Built With
- amazon-bedrock
- amazon-cloudfront
- amazon-cloudwatch
- amazon-dynamodb
- amazon-nova
- aws-cdk
- aws-lambda
- bedrock-agentcore
- fastapi
- python
- react
- strands-agents
- typescript
- vite
Log in or sign up for Devpost to join the conversation.