Sniffspot monthly fee reconciliation

Setup Guide

Automates the monthly check that Sniffspot’s payout matches what they actually owe. Replaces a manual click-through of ~50 reservations (three clicks each) with a single query against the page’s own data layer, producing a Quicken-ready spreadsheet and flagging any booking charged off-rate.

Built 2026-08-06 after a July reconciliation turned up two things a manual tally misses: refunded bookings that look identical to paid ones in the list view, and membership discounts that shift the commission base.

Why this exists

Sniffspot has under-paid before (an $8 discrepancy that took two months to recover). There is no monthly export in the host UI. The earnings breakdown is one click deep per reservation, so verifying a 50-booking month by hand is slow enough that most hosts almost certainly don’t bother.

1. The fee model (verified against all 49 July 2026 bookings)

Booking price:

(12 + 6 x (dogs - 1)) x hours

$12 for the first dog, $6 for each additional dog, for every hour — not just the first. This was the single biggest source of error in the manual estimates: a 2-dog / 2-hour booking is $36, not $42.

Deductions, in order:

Component Rule
Membership discount 0 / 5% / 10% of booking total, by tier
Sniffspot commission exactly 22.00% of the post-discount amount
Payment processing 0.00%–3.61%, varies by guest payment method
netToHost = amountDue - sniffpassHostDiscount - sniffspotFee - stripeFee

The 22% held on all 46 non-refunded July bookings with zero deviation — which is what makes it a usable audit invariant. Processing fees are erratic and frequently $0.00; that variance is guest payment method and is not host-controllable, so don’t chase it.

Membership tiers observed: none, basic (0%), bronze (5%), silver (10%). No gold seen as of July 2026. Any other value should be treated as new and investigated.

2. Data extraction — Apollo GraphQL, not clicking

The host reservations page is a React-on-Rails app with an Apollo GraphQL client exposed at window.__APOLLO_CLIENT__. Every Reservation object already carries the full fee breakdown, so there is no need to open individual reservations.

Direct POSTs to /graphql are rejected (returns {"error":{"message":"something wrong"}}) even with the csrf-token meta tag and credentials: 'same-origin'. Some other guard is in play. Go through the Apollo client instead — reuse the query document the page already built:

const c = window.__APOLLO_CLIENT__;
const oq = [...c.getObservableQueries('all').values()].find(q =>
  q.query?.definitions[0].name?.value === 'reservationsPaged' &&
  q.variables?.status?.[0] === 'past');

const r = await c.query({
  query: oq.query,
  variables: { page:1, limit:200, sortBy:'DATE', sortOrder:'DESC',
               asHost:true, status:['past'] },
  fetchPolicy: 'network-only'
});

window.__RES = r.data.reservationsPaged.collection;

limit: 200 reaches back roughly 12 months at current booking volume. The “Past” tab must be clicked first so the query document exists to borrow.

Fields on each Reservation:

id, datePart, date, time, length, quantity, price, amountDue,
sniffpassType, sniffpassHostDiscount, sniffpassDiscount,
sniffspotFee, stripeFee, netToHost, hostRefund, status,
user { firstname, lastInitial }

Note price is the per-hour base rate, not the booking total — use amountDue.

3. Refunds — the trap

Refunded bookings are visually identical to paid ones in the list view. They only reveal themselves in the per-reservation breakdown, which is exactly what nobody opens.

Detection: hostRefund === true, or netToHost < 1.

July 2026 had three (Rachel L. 7/1, Kelli A. 7/8, Rachel L. 7/24) totalling $48 of gross that returned $1.65. A manual tally counting them at face value would show a $48 shortfall and send you chasing a Sniffspot error that doesn’t exist.

Always exclude refunds from totals and list them separately.

4. Scheduled task (Claude, weekly)

Task name: Sniffspot fee reconciliation · Every Monday, ~12:00 PM · Runs on this computer.

There is no monthly cadence option (Manual / Hourly / Daily / Weekdays / Weekly only), so month selection lives inside the task prompt rather than in the schedule:

  • Day-of-month 1–10 → target the previous month, mark FINAL. This is the closeout. Late enough to capture end-of-month stragglers.
  • Day-of-month 11+ → target the current month to date, mark PRELIMINARY. Progress check only, no deposit comparison.

Walkthrough: Mon Aug 31 → August PRELIM. Mon Sep 7 → August FINAL, September excluded. Mon Sep 14/21/28 → September PRELIM. Mon Oct 5 → September FINAL. Every run covers exactly one month; two months never blend.

Output filenames encode month and status (sniffspot_2026_08_FINAL.xlsx, sniffspot_2026_09_PRELIM.xlsx) so a preliminary run can never overwrite a closeout already reconciled and filed.

The task stops and reports if it finds a logged-out state rather than producing a partial month. A missed week degrades to a notification, not a wrong number.

5. Browser session persistence

The task needs Chrome running and logged in. Two pieces:

Global Chrome setting is “Delete data sites have saved to your device when you close all windows” — good hygiene, but it wipes _sniffspot_session on close.

Carve out one exception at chrome://settings/content/siteDataAllowed to save data on your device → Add:

[*.]sniffspot.com

This is not the same list as the third-party-cookie allowlist under Privacy → Third-party cookies. Adding it there does nothing for clear-on-exit; that was the first attempt and it failed. Everything else still gets wiped on close.

Session cookie expiry runs ~30 days out (_sniffspot_session, checked 2026-08-06, expires 2026-09-05). Verify anytime in DevTools → Application → Cookies → https://www.sniffspot.com.

Startup page

Chrome → Settings → On startup → Open a specific page or set of pages:

https://www.sniffspot.com/host_account/reservation?status%5B0%5D=upcoming

Ensures the Apollo client is initialized whenever Chrome comes up.

6. Launcher script + cron

Opens Chrome five minutes before the task fires, only if it isn’t already running.

  • Script: /home/aztechguy/Projects/sniffspot/sniffspot-chrome.sh (mode 755)
  • Log: ~/.local/state/sniffspot-chrome.log
  • Schedule: user crontab, Mondays 11:55 AM
#Sniffspot weekly automation of gather financial data in chrome
55 11 * * 1 /home/aztechguy/Projects/sniffspot/sniffspot-chrome.sh
#!/usr/bin/env bash
set -uo pipefail

URL="https://www.sniffspot.com/host_account/reservation"
LOG="$HOME/.local/state/sniffspot-chrome.log"
mkdir -p "$(dirname "$LOG")"
log(){ printf '%s %s\n' "$(date '+%F %T')" "$*" >>"$LOG"; }

# Already running? Leave it alone.
if pgrep -x chrome >/dev/null 2>&1; then
  log "chrome already running — no action"
  exit 0
fi

# cron has no desktop context; lift it from gnome-shell
SHELL_PID=$(pgrep -u "$(id -un)" -x gnome-shell | head -1)
if [ -z "$SHELL_PID" ]; then
  log "no desktop session — skipping"
  exit 0
fi
while IFS= read -r -d '' kv; do
  case "$kv" in
    DISPLAY=*|WAYLAND_DISPLAY=*|XAUTHORITY=*|\
    DBUS_SESSION_BUS_ADDRESS=*|XDG_RUNTIME_DIR=*) export "$kv" ;;
  esac
done < "/proc/$SHELL_PID/environ"

setsid /usr/bin/google-chrome --new-window "$URL" >/dev/null 2>&1 &
log "launched chrome"

Two gotchas that cost real debugging time

$USER is not set under cron on Ubuntu. The original script used pgrep -u "$USER", which became pgrep -u "" and matched nothing — logging no desktop session — skipping even though GNOME was clearly running. Use $(id -un), which asks the system rather than trusting the environment. Failure mode is silent and the message actively points the wrong direction.

Redirect Chrome’s stderr to /dev/null, not the log. Chrome emits hundreds of benign Failed to create API on Chrome object and DEPRECATED_ENDPOINT lines per launch, which bury the script’s own entries. Both messages are noise and can be ignored. With >/dev/null 2>&1 the log becomes the audit trail it should be:

2026-08-06 10:55:06 launched chrome
2026-08-10 11:55:01 chrome already running — no action

pgrep -x chrome, not pgrep -f chrome. -x matches the process name exactly. -f would match the grep itself, any editor with “chrome” in a path, etc. Note chrome_crashpad_handler lingers after Chrome exits but does not match -x chrome, so it won’t produce a false “already running”.

Verify the cron path specifically

Running the script from a terminal proves nothing — an interactive shell already has DISPLAY and DBUS_SESSION_BUS_ADDRESS. The /proc/PID/environ lifting is only exercised under cron. Close Chrome, set a throwaway line a few minutes out, and confirm launched chrome appears.

7. Quicken invoice — the double-count trap

Four lines, and the membership discount appears exactly once:

Line July 2026 Category
Sniffspot bookings (billable gross) $852.00 Other Income
Membership discounts −$38.70 Discounts (Business)
Sniffspot commission −$178.93 Commissions & Fees
Payment processing −$16.48 Merchant Fees
Balance due $617.89

Gross is $900.00 across 49 reservations; $48.00 of that was refunded, leaving $852.00 billable.

The trap: adding the discount as an expense line without removing the existing revenue-deduction line subtracts it twice, producing $579.19 against an actual deposit of $617.90 — a $38.70 phantom shortfall. Either treatment is defensible (IRS Pub 334 sanctions both reducing sales and expensing the discount); pick one, stay consistent year to year, and never both.

Standing check: the invoice balance must equal the Stripe deposit into Amex business checking. If it doesn’t, a line is duplicated or missing — far faster than re-deriving the month.

Notes / things worth knowing

  • The 22% is the audit. Any non-refunded booking where sniffspotFee / (amountDue - sniffpassHostDiscount) ≠ 0.2200 is a real finding. Flag it with date and guest name.
  • Refunds are invisible in the UI list. They are the most likely cause of a manual tally disagreeing with the payout. Check these before suspecting Sniffspot.
  • 1099-K matching — unresolved. Confirm whether Sniffspot reports gross bookings or net payouts. The gap ($852 vs $617.89 for one month) is large enough that a mismatch against the return would draw an inquiry. If gross, the income line matches cleanly; if net, keep a reconciliation schedule.
  • System suspend is the remaining risk. A sleeping machine means Chrome is frozen and the run fails. Check with gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type — anything other than 'nothing' may skip a week.
  • SSO fallback. Sniffspot sign-in is Apple / Facebook / Google / email. If the session ever does expire and the Google handoff fails, [*.]google.com may also need the on-device-data exception. Not needed as long as _sniffspot_session persists.
  • Payout timing. Sniffspot pays once monthly; the July deposit landed 2026-08-04 via Stripe. The 1st–10th FINAL window is sized around this.
  • First scheduled run: 2026-08-10. Falls in the FINAL window, so it closes out July — a known answer ($617.89, 46 non-refunded bookings), which makes it a free correctness check on the whole pipeline.