A vet's itemised estimate, held on the owner's PayPal or card. Extra work needs a second yes. At discharge exactly the bill is captured and the rest is released.
What it is, and who it is for
pawhold is for independent vet practices in the UK and the pet owners who hand over an animal for a procedure and come back to a bill. The practice writes a treatment estimate from its own clinical notes. The owner reads it on their phone, line by line in plain words, and authorises it: PayPal places a hold for the top of the estimate's range and nothing is charged. If the vet finds more to do mid-procedure, the practice sends an update and the extra is only chargeable once the owner authorises it as a second hold. At discharge the vet confirms the itemised bill, pawhold captures exactly that bill across the holds, and whatever the bill did not use is released to the owner.
Everything runs in GBP on PayPal's sandbox. No real money moves.
The PayPal platform, and what each part does here
pawhold calls PayPal's REST APIs from its own server through its own typed client. Every part below is built and tested, and all but one has been run against the sandbox itself: the exception is the reauthorisation, which PayPal only allows from a hold's fourth day.
- Orders v2 with
intent: AUTHORIZE. Each estimate the owner approves is one order that mirrors the estimate: its lines as the order's items, at their top quantities, in the owner's words. Approving it authorises the hold. An update is a second order for the whole revised estimate less what is already held, so the second hold is the increase and nothing else. - Payments v2, capture. At discharge each hold takes one final capture, never above the hold, for the bill lines its owner approved. A capture below the hold releases the remainder.
- Payments v2, void. A hold the bill does not touch is voided at discharge, and a visitor's standing holds are voided when their demo session ends.
- Payments v2, reauthorise. An hourly job reauthorises a PayPal-account hold once, between day 4 and day 29, so a long stay stays covered. A card hold is never reauthorised: once its three-day honour period is over the practice is told to ask the owner for a new hold. PayPal's sandbox refuses a reauthorisation inside the honour period (it answers
REAUTHORIZATION_TOO_SOON), so the demo shows this on a seeded case, labelled as seeded. - Payments v2, refund. After discharge the practice can refund part or all of a capture, with its reason on the case's timeline and the owner's receipt.
- Webhooks. A listener verifies every delivery against PayPal's signing certificate before storing it, with PayPal's verify-webhook-signature call as the fallback. A screen moves on PayPal's answer to the API call and then shows "Confirmed by PayPal" with the time when the webhook lands. A webhook for a state the case is not in is recorded and flagged, never applied.
- JS SDK v6. The owner's page draws PayPal's button and PayPal's card fields, so an owner without a PayPal account can authorise the hold with a card.
PayPal-Request-Idon every POST, derived from the case, the hold and the step, so a retried request never captures twice. A discharge stopped part way resumes without charging again.
The AI: what it does, and what it is not allowed to do
The model is OpenAI's gpt-6.1-sol, called from pawhold's server through the Responses API under a strict JSON schema. It has two jobs.
- Notes to an estimate. The vet types or pastes clinical notes. The model drafts the estimate's lines: an item from the practice's own price list, a quantity range, an "Only if" condition where a line depends on a finding, the exact words of the notes the line came from, and the line in the owner's words.
- The update, in the owner's words. The vet writes what was found. The model drafts the extra lines and a short explanation for the owner in plain words. The explanation is shown only if code finds in it no money, percentage or dose and no figure the vet did not write, and the owner always sees the vet's own words beside it.
Code checks every line before anyone sees it, and the checks are tests in the repository:
- The model never sets a price. The schema has no price field. Every line is priced by code from the practice's list, and so are the totals, the range and the regulator's trigger check.
- It cannot invent an item. A code that is not on the practice's list is dropped, and its words are shown to the vet as "No price list item matched".
- It cannot invent a quotation. A line whose quoted words are not in the notes is dropped and said so.
- It cannot put a figure in the owner's words. Wording that carries a currency amount, a percentage or a dose falls back to the price list item's own plain name.
- Nothing reaches the owner unreviewed. The vet sees each line beside the words it came from, edits it, and sends it. Sending records the vet's approval on the audit trail.
- The model never calls PayPal. Holds, captures, voids and refunds are made by code on a person's action, or by the hourly job's fixed rule.
- A failure is not hidden. If the model fails, or a practice passes its cap of 40 drafts an hour, the vet gets an empty estimate to fill from the price list, and every other part keeps working.
Thirty note sets written for the demo practice were graded live against gpt-6.1-sol on 12 October 2026: no invented item and no invented quotation in any set; 119 of 124 expected items found (recall 0.960); 141 of 146 drafted items ones a vet would expect or accept (precision 0.966). The recorded answers are replayed in CI.
The practice's day has an agent of its own, and it is a rule, not a model: when an update is still unanswered 30 minutes before the animal's theatre slot, pawhold holds the slot at once, undone by one click, and may propose moving the next procedure, which waits for a person's click.
The practice's day is drawn on Bryntum's Scheduler: the rooms are its resources and the bookings its events, each coloured by its case's money, so a vet sees at a glance which procedures are held, which are waiting for the owner and which have an update unanswered. The rule's proposals are drawn on the board itself: the slot pawhold is holding has a dashed frame and reads "Held by pawhold", and the procedure it proposes to move has a dotted frame and reads where it would go. Move it, Dismiss and Undo stand above the board, and Move it swaps the two bookings on the Scheduler. The page asks for changes every three seconds, so when an owner authorises an estimate on their phone the booking turns to "Held" on the practice's screen without a reload. The board is read only, and every booking on it is a link to its case that the Tab key reaches.
The Scheduler runs on Bryntum's trial, so its "Bryntum Trial Version" watermark is on the board. If the Scheduler does not load, takes more than ten seconds, throws, or shows that its trial has run out in a visitor's browser, the day is shown as a plain timeline with the same bookings, the same proposals and the same buttons, and the page never breaks.
The regulator's Order
pawhold is built around the estimate duties in the Competition and Markets Authority's Veterinary Services Market Investigation Order 2026.
The Order says a veterinary business "must give Pet Owners a written estimate of the reasonably likely costs of a Treatment Pathway" where "the total cost of the Treatment Pathway is reasonably likely to meet or exceed the Monetary Threshold" (Article 11(1)), which starts at "£500 inclusive of VAT" (Article 2). It "must provide a Pet Owner with an updated Written Estimate if the total estimated cost of the Treatment Pathway is reasonably likely to increase, from the amount in the original Written Estimate, by 20% or by an amount equal to the Monetary Threshold, whichever is lower" (Article 11(9)), and "must give Pet Owners Itemised Bills" (Article 12(1)). The Order's table of compliance dates gives 22 June 2027 for large veterinary businesses and 22 September 2027 for small ones, a small one being "a Veterinary Business with fewer than 15 FOPs and/or OOH Centres".
In pawhold the estimate is itemised from the practice's price list, and the update's trigger is computed by code from the Order's own words: an increase of 20% of the original written estimate, or of £500, whichever is lower, and the case says whether it is met. The Order asks for an updated written estimate. pawhold goes one step further, and this step is pawhold's own, not the Order's: the extra cannot be charged until the owner has authorised it.
The CMA's own summary for practices, last updated 22 September 2026: https://www.gov.uk/guidance/what-veterinary-businesses-and-vets-need-to-do-following-the-cmas-final-vets-report
What is seeded and what is live
Every visitor gets a demo practice of their own for seven days, which no one else sees.
- Seeded: the practice, its price list of 87 items, and fifteen cases at every stage of the money, from an estimate in draft to a refunded discharge. Seeded history says "Seeded for the demo" on its timeline. Its PayPal ids begin
SEEDEDand are never sent to PayPal, and a seeded hold cannot be charged. - Live: everything a visitor does. Authorising the estimate makes a real order and authorisation in PayPal's sandbox. A discharge captures and voids there. PayPal's webhooks confirm each step on the page. Drafting an estimate or an update asks the model.
The practice, Hollins Lane Veterinary Surgery, is fictional. Its name, its price list and its cases were written for this entry, and no real practice's name, prices or patients appear.
Every step of a case is written to an append-only audit trail and read back on the case's timeline in plain words: who acted, what happened to the money, PayPal's ids, whether PayPal's webhook confirmed it, and for a model step the model's id and its checked output.
Tools used
- PayPal developer platform, sandbox: Orders v2, Payments v2 (capture, void, reauthorise, refund), webhooks with signature verification, JS SDK v6 (PayPal button and card fields).
- AI: OpenAI
gpt-6.1-solthrough the Responses API with a strict JSON schema, for the estimate's draft and the update's draft. Code checks every line it returns. - Application: Next.js 16 (App Router), React 19, TypeScript, zod.
- Schedule: Bryntum Scheduler 7.3.7 with its React wrapper, on Bryntum's trial, for the practice's day.
- Data: Postgres 17 with Drizzle ORM. Money is kept in whole pence, and the money rules and the append-only audit trail are held by the database as well as by code.
- Hosting: Render: a web service, a Postgres database and an hourly cron job, on paid plans.
- Tests: Vitest, fast-check for property tests, Playwright with axe for the browser walks, and an integration suite against PayPal's sandbox.
- APIMatic Context Plugin for PayPal: used to review the PayPal client, not to write it. The client calls PayPal over REST and was written from PayPal's API reference before the plugin was installed. On 13 October 2026 the coding agent loaded the plugin's eight TypeScript skills, read the PayPal Server SDK's 2.5.0 source as the first skill directs, and compared the client with both, point by point. The review found one defect and fixed it (an error from PayPal whose body carries no debug id lost the id PayPal sends in the
Paypal-Debug-Idresponse header) and added nineteen more tests that hold the client's behaviour where the skills name a trap. The five smaller differences those tests first held were fixed the next day, each test first (unknown statuses, a malformed error body, ids in the path, explicit nulls, one deadline per call). The SDK was not added as a dependency.
Try it: testing instructions
The address is https://pawhold.onrender.com. There is no sign-up, no download, no key to bring and nothing to pay. The only login a visitor meets is PayPal's own sandbox login.
- PayPal sandbox buyer login (a GB personal sandbox account, published for testing as the rules ask; it moves no real money): email
[email protected], passwordA1@{*7iO. - Test card for the card fields (PayPal's own GB test card):
Card 4462 6030 4234 3024, expiry 12/30, security code 123, any name.
The owner's page shows both as well, and the repository's README has them under "The sandbox login and the test card".
Two minutes as the owner
- Open https://pawhold.onrender.com in a fresh browser window.
- Click Try it as the owner.
- Read Otis's estimate from Hollins Lane Veterinary Surgery: each line in plain words with "What the vet wrote" under it, the likely range, and "Testing in PayPal's sandbox" with the login and the test card.
- Click the PayPal button, log in to PayPal's window with the sandbox buyer login above, and approve. Or type the test card into the card fields and click Authorise £1,010.00.
- See Held: "Done. £1,010.00 is held on your PayPal. You'll get the final bill at discharge." A few seconds later the page adds "Confirmed by PayPal" and the time: PayPal's webhook has arrived.
Then as the practice
- Open https://pawhold.onrender.com and click Try it as the practice. It is the same practice: Otis now shows Held £1,010.00.
- Read today's cases: one or more at every money state.
- Click Bramble, then Show PayPal and model details on a row of the timeline.
- Open Otis and click Discharge: the itemised bill against what the owner authorised, with "£876.00 will be captured from the hold. £134.00 will be released to Priya."
- Click Confirm and charge £876.00, and again in the dialog. pawhold captures £876.00 at PayPal and releases the rest.
- Click New case, fill it in, type or paste clinical notes and click Draft estimate: the model drafts each line from your words, priced from the practice's list.
Only a hold you authorised yourself can be charged. To try an update, open Otis while it is held, describe a finding under Add an update, send it, and click Try it as the owner again.
Links
- Hosted demo: https://pawhold.onrender.com
- Repository: https://github.com/BrianIsaac/pawhold (Apache-2.0)
- Video: https://youtu.be/ktTieJGmkjw
What it does not do
- It is a sandbox demonstration: GBP only, one fictional practice per visitor, no real money.
- It does not claim the Order's approval or compliance with it. It is built around the Order's estimate duties.
- The model drafts and the vet decides. No estimate reaches an owner until the vet has reviewed and sent it, the owner always sees the vet's own words beside the model's, and the model never moves money.
- A reauthorised hold is shown on a seeded case, because PayPal's sandbox refuses a reauthorisation inside a hold's first three days.
Built With
- bryntum
- drizzle
- next.js
- openai
- paypal
- paypal-js-sdk
- paypal-orders-api
- paypal-payments-api
- paypal-webhooks
- playwright
- postgresql
- react
- render
- typescript
- vitest
- zod
Log in or sign up for Devpost to join the conversation.