Inspiration
A small tour operator sells a Lisbon weekend as one package: a hotel, two airport cars, a tile painting class and a fado dinner, paid for with one PayPal payment. Two days before the trip, the hotel cancels. The cars were timed to that hotel. The class was picked because it was a short walk away. Someone now has to compare replacement hotels, rework the times, ask the traveler to approve a new price, and make sure the traveler is never charged for a trip that no longer exists. Today that is a phone call, a spreadsheet and a refund form. We wanted the plan and the one package payment to stay in step when a supplier fails.
What it does
RoamReprise is a working prototype for one package: one Lisbon weekend, sold by one operator, paid with one PayPal payment. The suppliers are simulated, the payments are real PayPal sandbox calls with test money.
The traveler picks what they like (tile art, fado, viewpoints, markets), a spending ceiling, and what a repair should protect: the lowest total, or their activities. RoamReprise builds a two day plan from simulated offers. A globe flies in and becomes the itinerary on a Bryntum Scheduler, with estimated travel time between stops kept as buffers. Stops the AI picked show its short reason, and a note like "slow mornings" moves the plan.
Before paying, the traveler can ask for one change in plain words, like "Make Sunday morning slower" or "Swap the castle for coffee". The AI proposes one change from the listed options, code checks it, the Scheduler slides the stops, and the quote waits for approval again. The AI never sets a price or a payment field.
The ceiling is a limit, not a charge. PayPal authorizes the exact approved quote (for example $428.00 under a $600.00 ceiling). Nothing is charged while the bookings are only held. A five step strip on the money panel shows what the traveler allows, what the agent proposes, what PayPal holds right now, what was captured and what was refunded.
Then the hotel falls through. Code checks every other hotel in the simulated inventory: availability, the ceiling, opening hours and estimated travel times. Each usable replacement card leads with its new total and the difference, the activities it drops, how many stops move and by how much, and what accepting does to the current hold. In the recorded run the cheaper hotel ($397.00) drops the castle walk and the dearer one ($474.00) keeps it. With "lowest total" the agent picks the cheaper one; with "my activities" it picks the one that keeps every activity, or says no replacement fits the ceiling. Nothing changes until the traveler accepts.
When the traveler accepts, the money follows the plan:
- The old PayPal authorization is voided. It was never captured, so nothing was charged for the broken trip.
- The traveler approves the new total, and PayPal authorizes it.
- When every simulated booking is confirmed, the app captures it.
- In the demonstrated case, the operator cancels one paid activity after capture. The slot is replanned and the traveler is refunded the cancelled price minus the replacement price ($58.00 class, $8.00 replacement, $50.00 refund).
The receipt page starts with one plain paragraph about the money and one line with the net result (in the recorded run: $428.00 released and never charged, $397.00 captured, $50.00 refunded, $347.00 net charged). Below it are the PayPal ids and the signature verified webhooks the app matched to each payment.
No real hotel or tour is booked: bookings go to a clearly labelled Simulated supplier. A table in the repository (docs/SCENARIOS.md) shows six repair and payment cases, each reproduced by a test.
How we built it
- Cloudflare Workers and Durable Objects. Each trip has its own Durable Object, so payment writes for one trip happen one at a time.
- PayPal Orders v2 (create with AUTHORIZE, authorize), Payments v2 (capture with final capture, void, refund, and a read for each), and Webhooks with signature verification. Every write carries a PayPal-Request-Id that is stored before the call. If an answer is lost, the app shows "Checking payment", blocks new charges, and reads PayPal to settle it.
- Bryntum Scheduler (trial) for the two day timeline: travel time drawn as a buffer before each stop, dependencies between stops, and a repair preview that shows the old times as ghosts and the moved items.
- three.js for the globe, drawn from Natural Earth land data (public domain).
- Workers AI, IBM Granite 4.0 H Micro, picks among options the code has already checked. Code owns every time, price and state. Its use is capped each day. Without a live answer the app uses a cached answer from earlier that day, a recorded answer for the default trip, or a rule pick, and each one is labelled on screen.
Challenges we ran into
Payments and bookings do not succeed or fail together. A hold can succeed while a hotel fails, and a capture answer can get lost on the network after PayPal already acted. We kept them as separate states on screen and wrote a test for each failure path.
Accomplishments that we're proud of
The recovery path works with real sandbox money movements: void before capture, capture only when everything is confirmed, refund after capture.
What we learned
A plan people trust shows what failed, what changed, and what happened to their money, in the same place.
What's next
Today it is one Lisbon weekend, simulated inventory, estimated travel times and one demonstrated activity cancellation. Next is the same recovery logic behind a real supplier connection, for one operator and one city first. The money rules stay as they are: hold first, void when the plan changes, capture only when every booking is confirmed, refund what a supplier cancels.
Built With
- bryntum
- cloudflare-workers
- durable-objects
- ibm-granite
- paypal
- three.js
- typescript
- workers-ai