found.gf.cx scan tracking — log geo of every public QR hit (parked sketch 2026-05-27)
DARE.CO.UK · PARKED SKETCH · 2026-05-30
Mirrored from ~/.claude/.../memory/parked_sketch_found_gf_cx_scan_tracking_2026-05-27.md. This is a design sketch parked for future build — read for context, not as a current deliverable.
When a stranger scans a found.gf.cx QR (e.g. on a missing video tape), capture timestamp + Cloudflare’s edge-side geo (country / city / postal / lat-lon / ASN / TLS / RTT). Owner-side view shows where lost items got scanned — “tape-1992-wedding got scanned in Newark NJ at 14:32 yesterday from a Verizon mobile IP” — which is forensic gold if the item went missing in transit. KV-backed for MVP, D1 if volume grows. Privacy: hash IPs, surface a small “this page logs scans” disclosure
The use case Dan named (2026-05-27)
“Can we track and trace the found.gf.cx when it’s scanned, lets say it shows up in Newark, can we capture the IP address and reverse from that rough geo location. Sketch and park that idea for later.”
When a lost item goes missing in transit — Beta tape shipped to TapeMaster but the box vanishes — a QR scan event is forensic gold. CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF gives every request a rich edge-side geo profile via request.cf; logging that per scan turns each found.gf.cx page hit into a “the package was here on this date” data point.
What CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF gives us per request (free, no API call)
Same payload as the dare.co.uk/cf endpoint built earlier this session — every Worker request carries a request.cf object with ~25 fields:
colo— CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF data center (EWR, LAX, etc.)country+isEUCountrycity+region+regionCode(e.g. Doylestown, Pennsylvania, PA)postalCode+metroCodelatitude+longitude(geocoded from the IP)timezone(America/New_York)asn+asOrganization(AS701, Verizon Business — knows residential ISP vs mobile vs hosting)tlsVersion+tlsCipher(helps fingerprint legit-browser vs bot)httpProtocol(HTTP/2 / HTTP/3)clientTcpRtt(how close the scanner is to the edge — a proxy for “are they really in this city”)
Plus from request headers:
- cf-connecting-ip (the actual scanner IP — to be hashed before storage)
- user-agent (phone model + browser version)
- referer (usually empty for QR scans — phone camera launches direct, no referrer)
Data model
interface ScanEvent {
ts: string; // ISO timestamp — when the scan happened
slug: string; // which item was scanned (tape-1992-wedding)
ip_hash: string; // SHA-256 of (cf-connecting-ip + salt) — uniqueness
// without storing PII raw
country: string;
region: string;
city: string;
postal: string;
lat: string;
lon: string;
asn: number;
as_org: string; // "Verizon Business" / "Comcast Cable" / "AT&T Mobility"
colo: string; // CF edge that served (EWR, LAX)
ua: string; // user-agent (truncated to 200 chars)
protocol: string; // HTTP/2 / HTTP/3
rtt_ms: number; // client TCP RTT — distance proxy
}
Storage choice
| Option | Fit | Trade-off |
|---|---|---|
| KV | MVP — handful of lost items, ~10 scans/item lifetime | List-by-prefix only · no aggregation / range queries · cheap |
| D1 (SQLite at edge) | When scan volume grows (>100/item, or you want filter/aggregate queries) | One Worker → D1 call · queryable with SQL · still free tier · upgrade-when-needed |
| R2 | If you want raw NDJSON archive for offline analysis | Append-only object store · no live query path |
Start with KV. Key pattern: scan:<slug>:<iso-timestamp> → JSON-encoded ScanEvent. List by prefix scan:tape-1992-wedding: to get all scans for one item. Promote to D1 when first scan hit feels “interesting enough to want to query.”
Privacy + disclosure
The finder doesn’t expect to be tracked when they help out. Two moves to stay ethical:
- Hash the IP before storing. SHA-256 of
(cf-connecting-ip + salt)where the salt is a Worker secret. Hash gives “is this the same scanner as last time?” (collision-detectable) without storing PII raw. CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF geo is coarse anyway (city-level, not street), so the geo fields aren’t PII even unhashed. - One-line disclosure on the public page. Below the contact block on
renderFoundPage():“This page logs anonymous scan events (date · city · network provider) to help the owner trace the item if it went missing. No personal information is stored.”
Sincere, honest, doesn’t kill the reward-and-return UX.
Owner-facing surfaces
Three views to build (probably gated behind CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Access via svc.gf.cx since the operator dashboard already lives there):
- Per-item scan log —
svc.gf.cx/found-admin/<slug>orfound.gf.cx/admin/<slug>if Access is added there. Reverse-chrono list: “2026-05-27 14:32 · Newark NJ · Verizon Business · iPhone Safari · EWR colo · RTT 18ms”. - Map view (optional) — drop pins on a static map image. Use the Geoapify / MapTiler static-map API, or just render a simple SVG world/US map with markers. The point isn’t precision (CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF geo is city-level), it’s the “this tape was here” narrative.
- Aggregates — “scanned in 3 distinct cities”, “first scan: X · last scan: Y”, “scanned by 2 unique IPs” (via the hashes). Comparison across items: “tape-1992-wedding has 8 scans · tape-1996-cousins-bbq has 0 (still in the box)”.
Notification path
When a scan happens, optionally fire a notification:
- First-scan-of-the-day per item → ping owner (sms.to, Pushover, Resend email). Same notification stack as agent.gf.cx alerts. Don’t spam — one ping per item per day max.
- Cross-region scan → if a scan comes from a city we’ve never seen for this item before, fire higher-priority notification (item is moving — interesting).
- Owner can disable per-item —
RECORDS[slug].notify_on_scan: falsefield for items that don’t need pings (e.g. on-property tags that get scanned by household members daily).
Implementation sketch (~3 hours total)
| Step | Lift |
|---|---|
Add KV binding (SCAN_EVENTS) + Worker secret (SCAN_SALT) to wrangler.toml |
5 min |
Hash helper + log-event function in src/index.ts — runs in ctx.waitUntil() so it doesn’t slow the page response |
30 min |
Append to /api/scans JSON endpoint (Access-gated) listing recent events |
30 min |
Disclosure line in renderFoundPage() template |
5 min |
Owner-side admin page /admin/<slug> with scan log table + (optional) static map |
60 min |
| Notification webhook on scan event (POST to sms.to / Pushover / Resend) | 30 min |
Bot filter (skip scans where UA contains curl/wget/bot/crawler) |
15 min |
| Owner-self filter (skip scans where ASN matches Dan’s known home/mobile AS) | 15 min |
| Optional D1 upgrade for queryable storage | +1 hr if needed |
Cross-link with the broader stack
- agent.gf.cx scan-watcher module — once scan events are flowing into KV/D1, a future agent.gf.cx module can read them and surface a “Tapes in transit” card on the hub. Sibling pattern to the vendor-watcher in
project_agent_gf_cx_vendor_watcher_module_2026-05-26.md. Card shows last-scan-by-item, escalates to ALERT chip if an item gets a scan from an unexpected region. - pa.gf.cx/service-records/ dashboard — extend the stat row with a “Scans today” counter. Click-through to the per-item admin pages.
- Found-page disclosure — pair with the manifest cross-link pattern already in place: scanned items show “Part of shipment: [link]” + “This scan was logged”.
Resume triggers
- A real found.gf.cx item gets shipped → first natural use of the system → motivates building the scan log so you can actually see it land
- The Brooklyn-tape manifest’s first delivery → notification when TapeMaster scans a tape would close the chain-of-custody loop
- Volume on any item crosses 5+ scans → KV pagination starts mattering; promote to D1
Cross-refs
project_svc_gf_cx_qr_router_2026-05-26.md— sibling system; the scan-tracking pattern doesn’t apply to svc (those are gated, owner-only) but the data model + storage choice carry overproject_agent_gf_cx_vendor_watcher_module_2026-05-26.md— the daily-cron + notify pattern this would slot intofeedback_cf_workers_with_assets_run_worker_first.md— earlier worker pattern adjacent (different trap but same Workers-with-Assets surface)feedback_visual_verification_external_probe_pattern.md— adjacent verification discipline; the scan log itself becomes an external probe (“did the recipient open the box?”)feedback_no_unsanctioned_rpa_on_vendor_accounts.md— privacy-adjacent; hashing IPs is the equivalent discipline here (don’t accumulate PII you don’t need)
File location notes when implementing
- Worker code:
~/Code/found.gf.cx/src/index.ts(KV binding + scan-log function) - Templates:
~/Code/found.gf.cx/src/templates.ts(disclosure line inrenderFoundPage, newrenderAdminScanLogfor owner view) - Wrangler:
~/Code/found.gf.cx/wrangler.toml(SCAN_EVENTSKV namespace +SCAN_SALTsecret) - Tag-preview already prints labels pointing at
found.gf.cx/<slug>— no changes needed there. Every printed label becomes a scan-event source the moment this ships.
Origin
Dan, 2026-05-27, right after the edit/delete loop landed on svc.gf.cx: “Sketch and park that idea for later.” The bind of svc.gf.cx custom domain that just landed sets the precedent — found.gf.cx is next in line for the same treatment, and scan-logging is a natural piece of the post-bind public-facing build.