Inspiration

When you pay by card at a merchant that runs on Braintree or PayPal's unbranded card processing, that merchant keeps its own saved copy of your card. Vaults are per-merchant by design, so a streaming service, a coffee app, and a gym you quit two years ago can each still charge you — and you have no one place to see those cards or take them back. When a charge looks unfamiliar, you get a soft descriptor and a dispute form, not an explanation.

PayPal-branded checkout already gives customers a wallet with visibility and protection. Plain card payments through Braintree don't, even though the tokens live in PayPal's vaults. I built Keyring as a sketch of how PayPal could extend that trust to every card saved at its merchants, without merging or deleting anything on the customer's behalf.

What it does

Keyring is a key the consumer holds. Creating one issues a random 256-bit secret, shown once and kept in the browser; the server stores only its SHA-256 hash. Merchants link a saved card to the key's public handle, not to an email or phone number. You get one view of every saved card across merchants, and can re-read each merchant's vault from PayPal to prove the cards are really there.

You can lock a card (reversible; in this demo Keyring blocks new charges before they reach PayPal) or revoke it (permanent: DELETE the payment token from that merchant's PayPal vault — 204, then 404). A later charge with the deleted token is rejected by PayPal with 403. Consent is required for every action, including a two-step revoke and a warning that it does not cancel a subscription. Keyring never locks or revokes on its own.

When a charge looks off, a logistic-regression model scores how much it "looks like you," and Google Gemini explains it in plain English, recommends one action, and answers "Ask about this charge." Gemini cannot change the score or execute an action. A new charge that scores below 50% raises a proactive alert (banner, bell, optional desktop notification) with Lock, Revoke, and "This was me." Nothing runs until you tap it.

In the demo, StreamCo+'s $15.49 renewal and a $6.25 Bean & Brew latte stay quiet. IronWorks Gym's $249 reactivation fee after 425 dormant days scores 18% and alerts, recommending a lock. StreamCo+'s $89.99 off-cycle "upgrade" also alerts.

How we built it

I built Keyring solo as a TypeScript and Express app against the PayPal sandbox, with help from AI assistants including Grok Bot. Every PayPal call is real: OAuth 2.0, Vault v3 setup-tokens and payment-tokens (one vault customer.id per merchant), Orders v2 charges with payment_source.card.vault_id, and DELETE of the payment token. After a revoke, PayPal rejects a charge with the deleted vault_id (403 NOT_AUTHORIZED / PERMISSION_DENIED). Every call appears in an on-screen API log with PayPal's debug id. Credentials stay in server-side environment variables, never in the repo or the browser.

The recognizer is a standardized logistic regression with 10 features, trained in Python (scikit-learn) on 20,000 synthetic saved-card charges. Held-out ROC AUC is 0.997 on that synthetic data only — not real-world performance. Explanations come from Gemini (gemini-3.6-flash, falling back to gemini-3.5-flash-lite, then a labeled template). The server rejects any answer that mentions a dollar amount not in the facts it sent, and the API always returns executed: false.

The UI is Keyring's own product, in official PayPal blues, with unmodified official PayPal marks, a "Powered by PayPal" lockup, and a footer stating that Keyring is independent and not endorsed by PayPal. I audited contrast against WCAG AA.

Challenges we ran into

A third party cannot truly index saved cards across merchants. In this prototype all three demo merchants share one sandbox REST app, each with its own vault customer id. A production cross-merchant index would have to live at PayPal, keyed by the handle. The sandbox token list can lag 15–30 seconds after a save, so vault checks fall back to GET-by-id and say so in the UI.

Lock cannot be enforced by PayPal at authorization here, so Keyring enforces it before the API call and I label that limit. Grounding checks and executed: false keep Gemini from inventing amounts or acting on a card. If a model's free-tier quota runs out, Keyring falls back to the labeled template so the demo still runs.

Accomplishments that I'm proud of

A working, consent-first control panel on real PayPal sandbox APIs, not a mock vault. The split of labor I'm happiest with: the model scores, Gemini explains, and the cardholder decides. Proactive alerts over Server-Sent Events that still never auto-execute. An end-to-end smoke test against the running sandbox, eleven screenshots in the repo, and a private SOP manual.

What I learned

Consumer trust for unbranded card vaults is a PayPal-shaped problem: the tokens already live in PayPal's vaults, but the customer has no key. Separating scoring from language, and language from action, is how I was willing to put AI next to payments without giving it the power to move money or delete a card. Keyring stays Keyring; PayPal's marks stay official and unmodified.

What's next for Keyring

In production I would replace the browser bearer secret with a passkey (WebAuthn) signature per action, and a real lock would need PayPal-side enforcement at authorization. Real cross-merchant indexing has to be a PayPal capability, keyed by the handle. I would also train and evaluate the recognizer on real, permissioned charge data — the 0.997 AUC is synthetic only. Later, notify the cardholder off-device when an alert fires.


Built With

(Paste as tags, max 25. Name PayPal and the AI tools explicitly — the hackathon asks that both be impossible to miss.)

  • PayPal
  • PayPal REST API
  • PayPal Vault API
  • PayPal Orders API
  • Google Gemini
  • scikit-learn
  • Python
  • Node.js
  • Express
  • TypeScript
  • JavaScript
  • HTML
  • CSS
  • Server-Sent Events
  • Playwright

Try it out

How I used PayPal

(For any “how did you use PayPal?” or similar additional-details question.)

Keyring is built on the PayPal sandbox, and PayPal is central to it: the saved cards in the demo are real Vault v3 payment tokens, and the charges are real Orders v2 payments.

  • Auth: POST /v1/oauth2/token (client credentials). The app refuses to run against anything but the sandbox.
  • Save a card without a purchase: POST /v3/vault/setup-tokens (card + a per-merchant customer.id) → POST /v3/vault/payment-tokens (type: SETUP_TOKEN). Each demo merchant gets its own vault customer id (for example kr-stream-…, kr-beanbr-…), mirroring real per-merchant vaults. In this prototype the three merchants share one sandbox REST app; in production they would be separate merchants.
  • Prove the card exists: GET /v3/vault/payment-tokens?customer_id=…, then GET /v3/vault/payment-tokens/{id} if the list hasn't indexed yet (sandbox lag is about 15–30 seconds).
  • Charge a saved card: POST /v2/checkout/orders with payment_source.card.vault_id and stored_credential (merchant-initiated, recurring or unscheduled). Completes immediately, with capture ids.
  • Revoke: DELETE /v3/vault/payment-tokens/{id} (204), then GET again to confirm (404). A later charge attempt with the deleted vault_id is rejected by PayPal (403 NOT_AUTHORIZED / PERMISSION_DENIED).
  • Lock is enforced by Keyring before calling PayPal. A production lock would need PayPal-side enforcement at authorization; I call that out in the UI and README.
  • Every call is shown in the UI's PayPal API log with PayPal's debug id. Official, unmodified PayPal marks and a "Powered by PayPal" attribution are used per PayPal's developer branding rules, with an independence disclaimer.

How I used AI

(The official “What to Submit” list asks you to identify which tools — model or AI service — were used and explain how. This also covers a “how did you use AI?” field.)

Two layers, each with one job:

  1. Logistic regression (scikit-learn), trained on 20,000 synthetic saved-card charges (reproducible, seeded). It scores "looks like you" from 1–99%, with a verdict, top reasons, and a list of verifiable facts. Held-out ROC AUC 0.997 is on that synthetic data only and is not real-world performance. Gemini never changes this score.
  2. Google Gemini (gemini-3.6-flash, fallback gemini-3.5-flash-lite) writes a 2–3 sentence explanation, recommends one action, and answers "Ask about this charge." If Gemini is unavailable or fails grounding, a labeled deterministic template is shown instead. I also used AI assistants (including Grok Bot) while building the project; they are not part of the running app.

Gemini sees only merchant names, amounts, dates, card brand and last 4, and the recognizer's output. It never sees full card numbers, vault tokens, PayPal ids or credentials, or the customer's key. The server rejects answers that propose a disallowed action or that mention a dollar amount not present in the facts. Recommended actions are never auto-executed (executed: false); the cardholder has to confirm lock or revoke, including the two-step revoke and subscription warning.

Sandbox / test instructions for judges

Live demo (easiest): https://keyring-paypal.onrender.com

There's nothing to install. It runs on the real PayPal sandbox with Gemini turned on. It's hosted on Render's free plan, so if nobody has used it for a while, the first page load can take about 50 seconds while it wakes up. Everyone who visits shares one demo, so click Reset demo in the header to start fresh. If Gemini's free daily quota runs out, explanations switch to a clearly labeled template fallback. The ML score, the alerts, and every PayPal call still work.

Or run it locally from the public repo (Node 20+ required):

git clone https://github.com/whatsgooglyKP/keyring-paypal.git
cd keyring-paypal
cp .env.example .env
# Add a PayPal SANDBOX REST client id and secret (developer.paypal.com → Apps & Credentials → Sandbox).
# Optionally add GEMINI_API_KEY (without it, explanations use the labeled template fallback).
npm install
npm start          # http://localhost:3001
npm run smoke      # end-to-end test against the running server (resets the demo)
Share this project:

Updates

posted an update —

This is just a working submission at this point (date: 11/10/2026). I don't have the PayPal domain knowledge to know if this will be considered useful, but I'm excited at the first part of the build which was a production-grade fraud detection model.

Submission history