TL;DR for judges

  • Problem: a meeting point that looks fair on a map often isn't. The geometric midpoint can mean a 38 minute subway ride for one friend and 17 for another.
  • What I built for Shipaton: MeetSpot as a macOS app with a RevenueCat business model. One free search a day. After that, a RevenueCat Paywall configured in the dashboard, a Test Store purchase, and a server-side meetspot_pro entitlement check that lifts the limit.
  • What the video shows: three friends with commute limits, the midpoint rejected on real transit times, the paywall, the purchase, the automatic rerun, and the Pro badge read from CustomerInfo.
  • Where the code is: desktop/ (Electron app and packaging), public/meetspot_finder.html (Web SDK, paywall, Pro badge), app/payment/revenuecat.py (REST entitlement check), tests/test_revenuecat.py (12 tests).

Inspiration

Every meetup I plan turns into the same argument: who has to travel the farthest? I started MeetSpot in 2025 as an open-source web tool that finds a midpoint and ranks venues around it. Testing it with real New York addresses showed me the flaw. A straight-line midpoint ignores rivers, bridges and subway lines, so the "middle" can still be unfair.

What it does

  • Everyone enters a starting point (Google Places autocomplete) and, optionally, the longest commute they will accept. Then you pick what you want: coffee, dinner, a library.
  • MeetSpot computes the center plus a few nearby candidate centers, then asks the Google Routes API (computeRouteMatrix) for real transit times from every person to every candidate. A candidate that breaks anyone's limit is rejected, and the results page shows why. In screenshot 2 the geometric center loses because two people would ride 38 minutes against limits of 30 and 35.
  • Venues around the accepted center are ranked on rating, popularity, distance and fit, with a map.
  • The first search each day is free. The second one opens the RevenueCat Paywall. After a Test Store purchase the app reruns the search by itself, and the header switches from "Go Pro" to "MeetSpot Pro".

How I used RevenueCat

  • The paywall sits at the product's natural limit. The server already answered need_payment once the daily free search was used. In the macOS app that response now opens presentPaywall() from @revenuecat/purchases-js 1.67.1 instead of a dead end.
  • Offering, products and paywall live in the dashboard. Entitlement meetspot_pro, three Test Store products (Lifetime, Yearly, Monthly) in the default offering, and a RevenueCat Paywall. No prices are hardcoded in the app.
  • The server decides, not the client. The app sends only its RevenueCat app user id (X-RC-App-User-Id). FastAPI calls RevenueCat REST GET /v1/subscribers/{id} with a secret key and skips the quota only if meetspot_pro is active. A failed or timed-out lookup falls back to the normal limit, so an error never unlocks anything. Only "yes" answers are cached (60 seconds), so the retry right after a purchase sees the new entitlement.
  • CustomerInfo drives the UI. On launch the app calls getCustomerInfo(). An active meetspot_pro shows a "MeetSpot Pro" badge and hides the free-search counter; otherwise a "Go Pro" button opens the same paywall. The badge is display only. The server still checks on every search.
  • Pricing. I set the Test Store prices before I settled on pricing, so the demo shows placeholders ($99.99 lifetime, $79.99 yearly, $9.99 monthly). For a real launch I would charge $3.99 a month, $19.99 a year and $39.99 for lifetime. People plan meetups a few times a month, not every day, so a cheap monthly plan and a lifetime option at twice the yearly price fit how the app gets used. Lifetime is the highlighted package in the demo because Test Store subscriptions expire after about 25 minutes.

How I built it

  • desktop/: Electron 44 shell. The preload sets window.MEETSPOT_PLATFORM = "macos", so the shared web UI knows purchases go through RevenueCat. npm run package builds MeetSpot.app with its own icon (@electron/packager, ad-hoc signed).
  • public/meetspot_finder.html: anonymous app user id in localStorage, lazy-loaded Web SDK, paywall, automatic retry, Pro badge.
  • app/payment/revenuecat.py: entitlement check over httpx with a 5 second timeout; handles grace periods and lifetime purchases.
  • api/index.py: quota gate in find_meetspot, plus /api/config/revenuecat for the public key.
  • app/tool/google_directions_client.py and _verify_commute_fairness() in app/tool/meetspot_recommender.py: one computeRouteMatrix call covers every person and every candidate.
  • tests/test_revenuecat.py: 12 tests against a mocked RevenueCat (active, expired, grace period, missing, HTTP error, timeout, no key, caching, quota bypass with and without the header). I broke each guarded behavior on purpose once and confirmed the matching test failed.

What existed before and what I built for Shipaton

  • Before: MeetSpot as an open-source web app, since 2025: geocoding, midpoint, venue ranking, maps, and a web-only payment flow.
  • During the Shipaton window (Aug 31): the real commute-time fairness check.
  • For this submission: everything macOS and RevenueCat. That covers the Electron app and its packaging, the paywall and purchase flow, the server-side entitlement check, the CustomerInfo Pro badge, the tests, and a fix for locations added with "+" never getting autocomplete.

Challenges

  • After the free quota ran out, a click on Search was still caught by the web build's credits flow, so the RevenueCat paywall never opened. My scripted demo had used requestSubmit(), which hid the bug. Switching the script to real clicks exposed it, and I fixed it before recording.
  • Caching a "no" would have blocked the search that runs right after a purchase, so only positive results are cached.
  • On the Google Maps path, a location added with "+" never got autocomplete, which broke three-person meetups. I hit it while recording the demo and fixed it.

What I learned

Putting the purchase where the product already hits its limit felt more natural than inventing a premium feature. Keeping the trust decision on the server let the client code stay simple.

Known limitations and what's next

  • The app user id is anonymous and local to the device, so anyone who learns that id could reuse its entitlement. Next I want to tie the RevenueCat app user id to a MeetSpot account (the repo already has SMS auth in app/auth).
  • Move from Test Store to RevenueCat Web Billing (Stripe) to take real payments at the prices above.
  • Developer ID signing and notarization, so the .app can be shared.

How to run and test a purchase

  1. pip install -r requirements.txt, then set AMAP_API_KEY / GOOGLE_MAPS_API_KEY and an LLM key as in the README Quick Start.
  2. Set REVENUECAT_PUBLIC_KEY (Test Store public key) and REVENUECAT_SECRET_KEY (v1 secret key), then run uvicorn api.index:app.
  3. cd desktop && npm install && npm start, or npm run package to build MeetSpot.app.
  4. Run one search (free), then a second one. The paywall opens. Pick a package and choose "Test valid purchase". The search reruns and goes through, the header shows "MeetSpot Pro", and the customer shows meetspot_pro active in the RevenueCat dashboard (Sandbox data).

Built With

Share this project:

Updates

Submission history