Inspiration
Pawpaid is a prototype for the payout side of cat and dog medical fundraising. A clinic estimate sets the limit for an animal case. The invoice is checked against that estimate before a human approves payment to a named clinic payee. The demo collects no donations and moves no real money.
In an overseas r/Pets post, Brilliant_Employ3661 wrote, "she may need surgery that could cost around $7,000. I do not have that kind of money." The owner had already put a $2,000 emergency visit for their cat Kairi on CareCredit. The $7,000 was a possible surgery estimate, not the final bill. A later edit said Kairi did not need surgery and the lower follow-up visit qualified for a payment plan. This account shows a funding gap before a medical decision; it does not establish that a fundraiser was used. Owner's post
In a separate overseas r/Assistance post, fundraiser Emotional_Lab_2529 offered the vet's phone and account number if "you'd feel more comfortable donating directly to the vet instead." This was the fundraiser's offered option, not a donor's own account or proof of a completed direct payment. A later comment in that thread asked how small donations would be matched to the bill. Fundraiser's post
These two accounts, a funding gap before a medical decision and a question about matching small donations to a bill, inform Pawpaid's design. Neither account says what donors want or how common these situations are. PayPal and finance approval are design choices, not requests made by those posters.
What it does
Pawpaid demonstrates paying a clinic invoice against a clinic estimate, with human approval, as one step in pet medical fundraising. It is built on a reimbursement engine and relabelled for pet cases. All data is synthetic.
One animal case is one seeded budget line, "Miso the cat | clinic estimate USD 100.00 | demo". The clinic invoice is a claim against that line. The payee is "Synthetic Pawhaven Animal Clinic", a synthetic clinic payee attached to the foster or rescue group account that submits the invoice. Finance approves a payout to that payee, not to the animal's owner.
The foster or rescue group signs in, chooses the animal case, and enters synthetic clinic invoice text. They review the clinic, date, USD total, and item amounts, resolve missing information, and confirm the fields before submitting. Finance reviews the invoice, approves the clinic payee by hand, and separately sends the payout. The earlier mock runs on 2026-11-11 described below used a mock payout adapter that stands in for the PayPal Payouts sandbox interface, so they made no PayPal sandbox call. The claim retains its approval, payout, and accounting state. Finance can check the existing payment, and the claimant can refresh its saved status.
Before accepting an invoice, the server rejects duplicates identified by the same invoice hash or the same clinic, date, and amount. It also rejects a submission when reserved funds, paid funds, and the new amount exceed the case's clinic estimate. Accepted submissions reserve their amount. These are in-system invoice controls, not a fundraising cap or cross-platform deduplication. They do not guarantee detection of altered or forged documents.
An uncertain payout keeps its reservation and does not trigger a second payout. Later checks query the existing payment. Finance cannot mark an uncertain payout Paid directly. The claim must first be in reconciliation hold, which can come from a verified provider event such as Unknown. Only then can finance settle the claim as Paid, mark it failed, or release the hold.
Automatic sync with the PayPal sandbox adapter marks Paid only when the matching item returns SUCCESS. A manual finance settlement can also produce Paid, but only after the claim is in reconciliation hold, so a Paid label alone does not show that PayPal reported SUCCESS. Automatic sync checks the item, batch, receiver, amount, and currency. PENDING, PROCESSING, UNCLAIMED, and ONHOLD remain pending. In the earlier mock runs on 2026-11-11, the mock adapter supplied the status.
The optional AI adapter drafts clinic invoice fields from text. A human reviews, approves, and pays. Live Gemini and its accuracy remain unverified in the recorded review evidence. A manual-entry demo does not prove AI operation.
Public case pages
Three public pages, /case, /case/updates, and /case/identity, open without sign-in in English and Traditional Chinese. They are static demo pages. The text, amounts, names, dates, and photos are fixed fictional data, and the pages do not call the server or collect anything. The four photos are AI-generated demo images, labelled "Demo photo" on the pages. The "Donate (demo)" button has no submit action, so clicking it does nothing and no payment is made.
- Case page: a demo case for a cat named Xiaobai shows her current condition, a clinic estimate with line items totalling USD 184.20, and a progress bar at 69 percent (USD 126.50 raised against the estimate, 8 demo donors). A second demo case, a dog named Heidou, shows 48 percent of a USD 120.00 estimate. The organizer has a "Verified name" label.
- Rescue updates: a timeline shows three entries for the same demo cat, newest first, under the same 69 percent progress summary. Each entry has a photo and a capture time.
- Verification and thanks: side-by-side cards show an organizer whose name is verified and one whose name is not yet verified. The unverified card has a Save as draft button and a disabled Publish case button, and the page says drafts are not saved and nothing goes public. A donor identity choice offers real name, anonymous, or display name. A thank-you list shows four named demo donors with amounts and one line totalling four anonymous donors.
Each page also has fixed demo screens for loading, error, and not-found states, and the verification page adds draft-saved, publish-locked, verified, verification-failed, and in-review screens. A URL parameter selects them. They show states only and do not come from server responses.
The pages do not read from or write to the payout engine. Their demo case (Xiaobai, USD 184.20 estimate) is separate from the seeded case the payout flows use (Miso the cat, USD 100.00 estimate).
Demo flows
Three earlier mock flows ran against the built server binary on 2026-11-11, with the mock payout adapter, manual entry, synthetic data, and a demo database. Each used the seeded case, Miso the cat, with a USD 100.00 clinic estimate. Amounts below are the server's cents. Flow 2 ran last on the same database.
- Invoice to paid. A USD 10.00 clinic invoice was submitted, approved for the clinic payee, and sent. The first payout call returned PayoutPending and the second returned Paid. Reserved went from 1000 to 0 and paid from 0 to 1000. The claim was still Paid after a 32-second wait across the server's maintenance tick.
- Duplicate and over the estimate. Submitting the same invoice again returned 409 DUPLICATE_RECEIPT. Eight more USD 10.00 invoices filled the case (paid 2000 and reserved 8000, against 10000 approved). A further USD 0.01 invoice returned 409 OVER_BUDGET.
- Uncertain payout. A synthetic timeout left the claim PayoutUnknown, with USD 10.00 reserved. After a server restart, a retry returned 503 ADAPTER_UNAVAILABLE ("synthetic provider batch not found; do not resend") and did not send a second payout. A finance disposition at that point returned 409 INVALID_TRANSITION ("claim has no unresolved reconciliation"). A signed mock Unknown event then put the claim into reconciliation hold, and the disposition settled it as Paid. Reserved went from 1000 to 0 and paid from 1000 to 2000.
The 2026-11-11 flows used a mock. A separate live smoke on 2026-11-12 made a real sandbox payout call. Neither run verifies live Gemini.
The separate 2026-11-12 live smoke ran against the hosted build from 14:43 to 14:45 Taipei. All 32 steps passed. One USD 0.01 synthetic claim progressed through Draft, Submitted, Approved, PayoutPending, and Paid using the sandbox payout API. The script sent one payout POST and recorded PayPal batch FXCR496HH36WW. The script had no manual finance settlement step. The claim and payout both ended Paid. A later PayPal sandbox API query returned SUCCESS for both the batch and the USD 0.01 item.
Innovation
Pawpaid applies payout controls to a pet medical case. The clinic estimate caps what invoices can claim, a human approves a named clinic payee, and an uncertain payout is not sent again.
An uncertain payout retains its reservation while the app queries the existing payout instead of sending a second one. In an earlier mock run on 2026-11-11, a retry after a restart returned 503 ADAPTER_UNAVAILABLE and did not send a second payout.
Automatic PayPal sync marks Paid only when a matching item returns SUCCESS, after checking the item, batch, receiver, amount, and currency. A manual finance settlement can also produce Paid, but only after the claim is in reconciliation hold. In an earlier mock run on 2026-11-11, a direct attempt on an uncertain payout returned 409 INVALID_TRANSITION until a verified Unknown event had created the hold. Pending and unclaimed items stay pending.
The server rejects identified duplicate invoices and invoices that would exceed the clinic estimate before accepting a claim. Its check includes reserved and paid amounts. Duplicate checks use the invoice hash or clinic, date, and amount within this system. These controls do not cap fundraising, deduplicate across platforms, or guarantee detection of altered or forged documents. The source implements this behavior, which ran in the demo flows above against a mock adapter. The recorded video is still pending.
AI can only draft clinic invoice fields from text. A human reviews, approves, and pays. AI cannot choose the payee. There is no claim of verified live Gemini use, extraction accuracy, or time saved.
How it uses PayPal and AI
PayPal Payouts is the payment step in the clinic payout workflow. The sandbox adapter records the payment intent before sending it, binds it to the approved invoice, and checks the returned batch and item against that intent. In sandbox mode the recipient comes only from server configuration. A successful batch alone does not prove that the clinic payee has been paid. Production and live payment modes fail at startup. The earlier mock runs on 2026-11-11 used an adapter with the same interface and made no PayPal sandbox calls. The separate 2026-11-12 live smoke called the sandbox payout API and ended with the claim and payout both Paid. A later PayPal sandbox API query returned SUCCESS for both the batch and the item.
The optional AI adapter calls Google Gemini REST with the model pinned to gemini-2.5-flash. It extracts clinic, date, total_cents, currency, and uncertain from synthetic clinic invoice text. The adapter clears flagged values for human correction. Non-USD input clears both currency and total and flags them. There is no currency conversion or image upload.
At clinic invoice entry, AI writes a draft only. It cannot submit a claim, approve it, select a payee, or send money. Item amounts remain human-entered, and the server computes their sum. PayPal handles the finance-approved payment step.
The deployment uses LLM_MODE=manual and makes no AI calls. Gemini is optional and restricted to a billing-disabled project with free-tier access. The AI budget is US$0. Provider failure or exhausted quota returns the user to manual entry and preserves existing fields. Live Gemini extraction and its accuracy remain unverified in the recorded review evidence.
How I built it
The packages still carry the earlier working name receipt-roots.
The application has a Rust server using Axum and SQLx, with a React 19 and TypeScript frontend built with Vite 7. Zod validates responses at the web HTTP boundary. Local development uses SQLite. Later validation gates used Postgres. A public sandbox build runs at https://receipt-roots.onrender.com. It uses PayPal sandbox only and synthetic demo data. Judge sign-in is required for the app. The three public demo pages (/case, /case/updates, /case/identity) do not require sign-in. Render free tier sleeps when idle, so an initial request can take about 10-15 seconds. This is a sandbox demo, not a production service.
To run the local build, install Rust 1.99.1 and Node 26.13.0 with npm. From the repository root, privately set JUDGE_CLAIMANT_PASSWORD and JUDGE_FINANCE_PASSWORD in the server's runtime environment. Also supply the PayPal sandbox credentials privately, as the README requires. Local runs require PAYPAL_MODE=sandbox; mock mode fails at startup. Do not put credentials in the repository. To try it without local setup, use the hosted sandbox with a judge account.
cargo build --locked -p receipt-roots-server
PAYPAL_MODE=sandbox LLM_MODE=manual cargo run --locked -p receipt-roots-server
In another terminal, from the repository root:
cd web
npm ci
npm run dev
Open http://127.0.0.1:5173 and sign in as judge-claimant or judge-finance with the corresponding password set by the local operator. This run uses PayPal sandbox and manual entry. It makes no Gemini call.
The PayPal sandbox configuration described in the README is required for local startup. Optional AI extraction requires LLM_MODE=gemini-free and the README's free-tier configuration. These provider modes require separate setup. Credentials stay in the runtime environment.
Challenges I ran into
Payment creation, recipient receipt, and application accounting are different states. Historical sandbox evidence recorded a successful batch whose item remained UNCLAIMED. Automatic PayPal sync checks the matching item before marking a claim Paid. A manual finance settlement to Paid is possible only after the claim is in reconciliation hold, and the demo run needed an extra verified Unknown event to get there. Uncertain outcomes retain their reservations.
Clinic invoice text extraction must keep suggested fields separate from an approved expense. A valid response shape cannot establish that an invoice is correct. Human review, required item amounts, and server validation remain part of submission even when extraction returns fields.
Accomplishments that I'm proud of
The implementation connects an approved animal case to a clinic invoice, manual finance approval, and an immutable payout intent. The AI adapter has no payment authority, and the request cannot supply a different recipient. The interface separates approval from sending the payout. It also separates finance payment checks from claimant status refreshes.
On 2026-11-11, the integration gate passed with 216 tests passed, 0 failed, and 2 ignored, including 38 Postgres tests. The two ignored tests are the live Gemini smoke and the PayPal sandbox smoke, so neither provider ran in the gate.
The separate 2026-11-12 live sandbox smoke passed 32 steps and ended with the claim and payout both Paid. It is not part of the 2026-11-11 gate. A later PayPal sandbox API query returned SUCCESS for both the batch and the item.
What I learned
The sandbox evidence shows why batch success and recipient payment need separate checks. The extraction contract also distinguishes returned values from flagged values that need human correction.
What's next
The public case pages above are static demos. Server storage for cases, donors, and updates still needs to be built. Donation intake also needs to be built, since the "Donate (demo)" button does nothing. The organizer verification flow still needs real draft and published states. The pages show these states as fixed screens, and no ID check or KYC integration exists. Saving the donor's identity choice and keeping the legal name as private data separate from the public display name also remain to be built. The pages describe this separation in one line of copy, say that no legal name is stored, and have no legal-name field.
Today, the animal case is a label on one seeded budget line. A claim has no animal ID, and the clinic estimate is that line's cap, not an uploaded estimate document. The work still to be built and verified includes linking each invoice to a specific animal and estimate, registering a clinic payee through a rescue or intermediary, and checking payout feasibility with real clinics.
Camera capture for on-site progress photos is planned for after the contest and is not built.
The remaining work includes live Gemini verification, a recorded walkthrough against the real PayPal sandbox, and review of the unresolved webhook intake and Postgres evidence gaps. The public sandbox is available. The separate live smoke has file records but no recorded video. Clean setup instructions and a private judge access arrangement still need confirmation.
Known limitations
This is a USD-only prototype for synthetic clinic invoice text, using the existing single-event data model. The animal case is a label on one seeded budget line, and the clinic estimate is that line's USD 100.00 cap. The daily 04:00 Taipei demo reset refills the available capacity to USD 100. Each invoice is capped at USD 10.00. The pet framing is copy only, and the payment logic is unchanged. The public case, updates, and verification pages are static fixtures. They show a case page, a progress bar, real-name labels, a donor identity choice, a thank-you list with amounts, and a photo timeline, but none of it is connected to the server. The pages describe the separation of legal name and public name in copy only, and say that no legal name is stored.
There is no donor checkout, live fundraising, or real clinic onboarding. The public case pages are static demos, and their "Donate (demo)" button does nothing and makes no payment. The raised amounts shown on these pages are fixed demo figures, not donations received. The clinic payee is a synthetic payee attached to the foster or rescue group account, not a verified clinic or live clinic payout account. A real launch would register clinic payees through a rescue or intermediary, and that is not built.
No real rescue, clinic, or donor uses this service. Names in the demo are synthetic. The photos on the public pages are AI-generated demo images, not real animals, and the rescue progress entries are fixed demo content. Real-name verification is not built, and no ID check or KYC integration exists. Save as draft and Publish case on the verification page save and publish nothing.
Payments are sandbox only. Local startup requires sandbox credentials and rejects mock mode. The earlier mock runs on 2026-11-11 remain historical evidence. The public, non-production demo at https://receipt-roots.onrender.com uses only PayPal sandbox and synthetic data, with required judge sign-in and possible initial waits of about 10-15 seconds after Render free tier idle sleep. Production and live payment modes fail at startup. Launching in Taiwan would require a partner association that holds a fundraising (勸募) permit. The legal treatment of fundraising for a specific animal still needs confirmation; a permit alone is not a claim that all launch requirements are resolved.
Waggle already pays vets directly after estimate and invoice review. Pawpaid makes no claim to be the first or only direct-to-vet path.
The prototype has no image upload or self sign-up. Payouts are limited to one per claim, at most US$10.00 each, and at most 20 attempts per Taipei calendar day across the site. Uncertain attempts keep their reservation and cannot be resent. Finance disposition can settle or release the hold only after the claim is in reconciliation hold.
The November 11 re-review of commit 7e6bd10 found 0 BLOCK, 0 MAJOR, and 5 MINOR items, and judged it mergeable for the sandbox only. It is not a finding that the app is ready to deploy or run in production. The review was a static read, and later commits in the pull request were outside it. The MAJ-1 load disclosure is closed. The MAJ-1 residual risk is accepted as previously decided, not fixed: during a flood, the outer rate limit can refuse genuine webhooks, and payout-ID matching is not authentication. The MAJ-2 acknowledgement and hold policy is still the owner's decision, unchanged since the earlier review. The five MINOR items remain open.
A later static hardening review of commit b8ee494 found 0 BLOCK, 0 MAJOR, and 3 MINOR items. It states, "This review does not establish public-deployment readiness" and is limited to sandbox scope. PR #2 merged as e5db938. MAJ-1 flooding risk remains accepted for sandbox use, not fixed. MINOR n1 remains open because inbox accounting still holds the shared lock. Commit 69abed4 changed the Nginx security headers and README/demo mock instructions. The headers were also seen on the live site. Those changes have not been re-reviewed. This is not a review of the deployment.
The live_gemini_free_smoke and sandbox_smoke tests remained ignored in the 2026-11-11 gate, so neither provider ran in that gate. The separate 2026-11-12 live smoke is not part of the gate. It made a real sandbox payout call and ended with the claim and payout both Paid. A later PayPal sandbox API query returned SUCCESS for both the batch and the item. Earlier historical sandbox evidence did not establish a matching item SUCCESS or Paid outcome. The deployment uses LLM_MODE=manual. Live Gemini and extraction accuracy remain unverified. The public build is a sandbox demo, not a production service.
Built With
- axum
- docker
- google-gemini
- nginx
- paypal-payouts
- postgresql
- react
- rust
- sqlite
- sqlx
- typescript
- vite
- zod