Mojo Bridge
Linear: RYR-236 — sync_log volume alerting (2026-07-23 vault→Linear audit) Verification correction (2026-07-24): sync_log volume alert is ALREADY LIVE —
alert_sync_log.sql+ terraform data-transfer + monitoring policy ENABLED (verified 2026-07-24). (RYR-236 canceled)Mojo Dialer → BigQuery sync. Captures setter dialing activity + the full owner/property/phone graph Mojo holds, so the setter-program deliverables can join Mojo data to FUB on normalized phone. Lives at
~/rylobasic/systems/setter-program/mojo-bridge/.
Why
Mojo has no public API but its SPA at app201.mojosells.com/v2/rest/* is stable and session-cookie authed. The 12,425 contacts on account 689102 carry MLS metadata, AVM history, multi-phone/multi-email arrays with per-channel DNC flags, and full MLS remarks — none of which surface in the Mojo↔FUB native integration. Without this bridge, every call our setters will make lives only inside Mojo's UI, with no programmatic way to track dial volume, talk time, or outcome attribution against FUB consults.
The bridge buys time. The long-term answer is Custom Dialer (Twilio + STT + Vertex, ~Sept 2026). Until then, BQ becomes the system of record while Mojo keeps doing the four things it does well: power dialing, skip tracing, FSBO/Expired feeds, MLS metadata enrichment.
Current state
- Status: operational AND capturing real dialing — the 6/10 "no dialing ever captured / 0 rows" finding is RESOLVED. Live-verified 2026-07-16 (direct BQ queries):
call_detail2,679 rows (6/15→7/14 span, 457 in the trailing 7d at the 7/16 check),v_setter_calls2,679,v_setter_productivity54 rows across 4 setters (Viola Hajdini 414 calls last 7d, Rafferty Ojeba 43, plus Ry + Samuel Padilla sessions through 7/15). Setters ARE on account 689102 and downstream is no longer dark. - Last update: 2026-07-16 (RYR-175 item 3 verification)
- Blocked on: nothing
- Unblocked by the above (was "deferred until dialing data exists"): the consumers — agent-newsletter dials, consults-dashboard, team-coaching setter views, flywheel scoreboard setter inputs. Tracked via RYR-175 / RYR-172.
- Schema note:
v_setter_productivitycolumns areagent_name/session_date/total_calls/total_appts/total_leads/seconds_clocked/calls_per_hour/appt_rate.
2026-06-30:
- Recording mirror was never actually working — fixed. The audio download URL omitted the trailing slash before ?record_id; Mojo 301-redirected to the slash form and requests dropped the session cookie across the redirect → every download 401'd. 0 of 71 recordings had ever mirrored. The old code masked it by aborting at the first 401 (looked like one dead recording, 85712249). Root-caused live (slash → 200 audio/mpeg; no-slash → 301 → 401), fixed in endpoints.py (MR !958), deployed (rev pos-mojo-bridge-00010), and verified: 71/71 backfilled on first run, skiplist empty. Also hardened mirror_recordings to stay loud — a total failure always raises session_expired, never silently skiplisted (MR !958). This was the real cause of the daily mojo-sync-log-alert. Docs corrected (MR !960).
- Setter Call Reviews built (manual one-off, 2026-06-30). Per-setter Drive folders of every 1-min+ recorded call, each call its own sub-folder containing a transcript Doc + the recording mp3. Lives in ry@rycolston.com Drive under "Listing Legends — Setter Call Reviews" (parent folder 1OcOKAL7a4nkbHdGkWWUB-ePH7WlQTlxP) → sub-folders Viola Hajdini — Call Reviews (13 calls), Rafferty Ojeba — Call Reviews (3), Ry Colston — Call Reviews (2). ryan.colston@orchard.com has editor access on the parent (cascades to all sub-folders). Setters NOT yet shared (await Ry's OK; would be violahajdiniii@gmail.com / raffertyojeba@gmail.com, reader). Rafferty's 3 are hand-cleaned/named; the rest were auto-built via the transcription pipeline below. NOT a recurring system yet — manual build.
- Transcription pipeline (proven this session, NOT productionized). Recording mp3 (8 kHz mono, gs://pospj-480915-mojo-bridge/recordings/{record_id}.mp3) → Google Speech-to-Text v1 longrunningRecognize: encoding=MP3, sampleRateHertz=8000, model=latest_long, enableAutomaticPunctuation=true, diarizationConfig{2 speakers}. Gotchas: (1) the phone_call enhanced model returns EMPTY — use latest_long; (2) user-cred REST calls need header x-goog-user-project: pospj-480915 or 403 quota-project; (3) gcloud ml speech doesn't expose punctuation/diarization flags → hit the REST API. speech.googleapis.com enabled on pospj-480915 (2026-06-30, MANUAL — not Terraform-managed, no IaC home; no drift). Auto-punctuation is reliable; diarization is hit-or-miss on 8 kHz (clean speaker split on ~9/15 longer calls, collapsed to 1 speaker on shorter → fall back to a punctuated paragraph, no fake labels). Speaker→name mapping is a pitch-phrase heuristic. Within STT free tier (60 min/mo) at this volume. For consistent speaker labels + full productionization (mirror_recordings → STT → LLM-clean → Drive doc, daily), the upgrade is an LLM cleanup step ("Lever 2"). Scripts were ad-hoc (scratchpad), not committed.
2026-06-18:
- Native "Expired Data" subscription RETIRED (decision 2026-06-18). The feed has been silent since 2026-04-26 (posting_history vendor "Mojo Expired"); rather than fix it, Ry decided the manual NTREIS→Mojo import (below) replaces it — more control over which searches/cohorts load. Let the $50/mo subscription lapse/cancel; do NOT flag the silent feed as a problem going forward.
- Interim — revived the manual NTREIS→Mojo import. Pulled the Alberto (1,306) + Brandon 0-30 (80) + Brandon 1yr+ (588) Cancelled/Expired/Withdrawn saved searches into Mojo-ready CSVs (with postal code) for hand-import to calling lists. Now repeatable: data-platforms/ntreis/skills/update-ntreis/export_search_to_mojo.py + runbook data-platforms/ntreis/docs/ntreis-to-mojo-manual-import.md (MR !649). A Ryan | Cancelled/Expireds saved search also exists, not yet pulled.
- mojo-bridge ingest bugfix shipped — session_summary "Total" footer row was failing the BQ load and time_sheet was crashing the MERGE on a minute-granularity key collision; fixed + deployed (MR !647/!648, rev pos-mojo-bridge-00006).
2026-06-10 audit + additions:
- Audit finding — no dialing ever captured. call_detail 0 rows across 17 green runs; roster has only Ry. Anthony's dials are NOT on account 689102.
- Audit finding — Mojo Expired feed silent since 2026-04-26. Verify subscription.
- Audit finding — account holds 1,655 contacts across 4 main lists (Mojo Training 50, Test 0, Sample|Alberto 1,356, Vacant Land 249); the 12,425 figure from 5/25 is stale.
- Contacts ingestion live: POST /mojo-cold-load?limit=N resumable backfill + endpoint=contacts slice in daily_ingest (new ids ∪ ids called in window, cap 500/run).
- Recording mirror live: daily 07:00 CT scheduler mojo-mirror-recordings; every unmirrored row → gs://pospj-480915-mojo-bridge/recordings/, URI written back.
- BQ views mojo_raw.v_setter_calls + v_setter_productivity created (were repo-only).
What's live as of 2026-05-26:
- Scaffold at ~/rylobasic/systems/setter-program/mojo-bridge/ (MR !198 merged)
- Cloud Run service pos-mojo-bridge at https://pos-mojo-bridge-uvqirrk5la-uc.a.run.app — image :ab28bc12, revision 00001-sv6 (MR !200)
- Cloud Scheduler pos-mojo-bridge-mojo-daily-ingest daily 06:00 CT → POST /mojo-daily-ingest
- Tfvars + Terraform at infra/terraform/pos-mojo-bridge/ (MR !201)
- Drift coverage — check-tfvars-drift.sh now governs 8/8 services including mojo-bridge (MR !202)
- Session-refresh runner — re-mints MOJO_SESSION_COOKIE when sync_log shows session_expired, driving Playwright through Mojo login. Originally the Mac-mini launchd agent com.rycolston.mojo-session-refresh (MR !203, creds from 1P item mojosells.com in POSPJ vault). Migrated to the mojo-session-refresh Cloud Run Job 2026-06-21 (image :e9a6ec79, fired by Cloud Scheduler mojo-session-refresh-sched every 15 min, self-gates on sync_log via _refresh_needed(), creds from Secret Manager MOJO_USERNAME/MOJO_PASSWORD). The mini launchd agent (com.rycolston.mojo-session-refresh) was deleted 2026-06-24 (Phase 3 cleanup; had been disabled cold-standby). See infra/cloud-run-jobs/mojo-session-refresh/README.md.
- Cloud Monitoring alert policy mojo-bridge: non-ok rows in sync_log — scheduled query daily 06:45 CT writes to mojo_raw.sync_log_alerts, RAISEs on non-empty, alert fires email to ry@rycolston.com (MR !204, synthetic-row tested end-to-end)
- B4 auto-apply detects hyphenated service names (MR !205 — bug surfaced by mojo-bridge being the first multi-word pos-* service)
- GCS bucket gs://pospj-480915-mojo-bridge/ with 2-year lifecycle on raw/
- 12 mojo_raw.* BQ tables + mojo_raw.sync_log_alerts (alert dest)
- Secret Manager MOJO_SESSION_COOKIE — auto-refreshed by the mojo-session-refresh Cloud Run Job when stale (was the Mac-mini runner pre-2026-06-21)
- Deployed daily-ingest smoke-test 2026-05-26 08:34 CT: 5/5 endpoints OK, 1 time_sheet + 76 posting_history rows landed via the actual Cloud Run service (run_id 34484651-d268-4506-88a9-c783c8cfa0a4)
What's NOT live (parked, not blocking):
- Schema drift detection (status=schema_drift rows in sync_log).
- Volume-based alerting (green runs with 0 rows are invisible to the current alert).
- Consumers (agent-newsletter, consults-dashboard, team-coaching, handbook) — deferred until dialing data exists.
Next 3 actions
Get dialing onto account 689102DONE — resolved by live evidence 2026-07-16: call_detail 2,679 rows across 4 setters (see Current state). Replaced by: wire the first consumer (agent-newsletter dials / flywheel scoreboard setter inputs, RYR-175/RYR-172).- Verify the Mojo Expired feed subscription — note the 2026-06-18 decision above already RETIRED it (let it lapse); confirm the cancellation actually happened, then drop this line.
- Add a volume expectation to the sync_log alert (setter worked + 0 call_detail rows = alert) — now actionable since real dialing volume exists to baseline against.
Decisions log
- 2026-05-25 — Bridge over replacement. Don't replace Mojo with OpenPhone or similar. OpenPhone isn't a power dialer and replacing Mojo means replacing 4 products (dialer + skip-trace + FSBO/Expired feed + MLS enrichment), not 1. The bridge makes BQ the system of record while Mojo keeps doing its job. Custom Dialer (Twilio) remains the long-term replacement on its existing roadmap.
- 2026-05-25 — Setters on Ry's Mojo account, not separate. Account 689102 has Owner-level access; setters get added as agents under it so all dialing flows through one bridge.
- 2026-05-25 — Cookie refresh runs on Mac mini, not Cloud Run.
Mojo's WAF likely challenges unfamiliar IPs on login; residential IP from Mac mini is unchallenged. Cloud Run egress untested but assumed risky.SUPERSEDED 2026-06-21: the "Cloud Run egress assumed risky" premise was untested speculation and turned out wrong. Headed Chromium underxvfb-runfrom a GCP IP logs into Mojo cleanly — proven by the livemojo-session-refreshCloud Run Job (no WAF challenge observed). Refresh now runs on Cloud Run; the mini launchd agent was deleted 2026-06-24 (Phase 3 cleanup). - 2026-05-25 — Raw archive to GCS before BQ write. Every endpoint's raw JSON is archived before any transform runs. If a load fails, we replay from GCS without re-hitting Mojo. Reliability over throughput throughout.
- 2026-05-26 — Reuse
pos-cloud-functionsSA, not dedicated. The runtime SA already has secretmanager.secretAccessor + bigquery.dataEditor + storage.admin at project scope. A dedicatedpos-mojo-bridge-sais cleaner but YAGNI right now; can split later without service downtime. - 2026-05-26 — Bootstrap tfvars in separate MR.
reject-hand-edit-tfvarsblocks non-auto-bump branches from touchingpos-*/terraform.tfvars. Scaffold MR ships without tfvars; anauto-bump/manual-pos-<svc>-bootstrapMR pins the first SHA after the build job lands the image. Documented ininfra/cloud-run-services/deploys.md"Bootstrapping a new pos-* service". - 2026-05-26 — Script-style scheduled query (no destination_table_name_template). The alert SQL is a multi-statement script that handles
CREATE OR REPLACEitself and RAISEs on non-empty. Without removingdestination_dataset_idfrom the DTS config, BQ DTS pre-validates the (empty) script's "default dataset" against the config's destination and rejects with "Dataset specified in the query ('') is not consistent with Destination dataset 'mojo_raw'". Script-style configs omit destination_dataset_id entirely. - 2026-05-26 — Alert via "RAISE → DTS error log → log-based metric → policy". BQ DTS has no native "email when result is non-empty". Workaround: have the SQL itself RAISE when the result is non-empty, making the transfer run report ERROR. Cloud Logging captures that ERROR, a log-based metric counts those entries, and a Monitoring alert policy emails on metric > 0. End-to-end tested with a synthetic
session_expiredrow.
Related projects
- Custom Dialer — the long-term replacement
- Appointment Setter Hiring — setters who'll dial through this account
- Marissa Tah Setter Program — the program that consumes this data
- Setter Workbook System — handbook.rycolston.com setter UI
- BQ Bulletproof — engineering standards this bridge follows
Open issues
- GitLab: label
mojo-bridge(to be created when first issue lands). - MR record: !198 (scaffold, 2026-05-25), !200 (Cloud Run + Terraform + CI, 2026-05-26), !201 (bootstrap tfvars), !202 (drift coverage), !203 (refresh-runner schema + 1P item fix), !204 (Cloud Monitoring alert policy), !205 (auto-apply regex hyphens fix), !428 (contacts ingestion + recording mirror + mirror scheduler + BQ views, 2026-06-10).
- Known oddity:
GET /healthzreturns Google GFE-styled 404 despite being declared inentrypoint.pyand listed in/openapi.json. ProductionPOST /mojo-daily-ingestandPOST /mojo-mirror-recordingswork. Non-blocking. Likely GFE-layer path interception — investigate when there's time.
References
- Endpoint catalog:
~/rylobasic/systems/setter-program/mojo-bridge/docs/endpoints.md - Reliability design + open items:
~/rylobasic/systems/setter-program/mojo-bridge/docs/reliability.md - Session refresh design:
~/rylobasic/systems/setter-program/mojo-bridge/docs/refresh.md - Session refresh runner — Cloud Run Job (live):
~/rylobasic/infra/cloud-run-jobs/mojo-session-refresh/README.md. Original Mac-mini install/ops (launchd agent deleted 2026-06-24; runbook kept for reference):~/rylobasic/systems/setter-program/mojo-bridge/scripts/session-refresh/README.md - Property attr type code map:
~/rylobasic/systems/setter-program/mojo-bridge/docs/property_attr_types.md - Cloud Run service home:
~/rylobasic/infra/cloud-run-services/pos-mojo-bridge/ - Terraform:
~/rylobasic/infra/terraform/pos-mojo-bridge/(pos_mojo_bridge.tfservice + scheduler;alert.tfDTS + monitoring) - Bootstrap flow for new pos- services:
~/rylobasic/infra/cloud-run-services/deploys.md("Bootstrapping a new pos- service") - Alert SQL:
~/rylobasic/systems/setter-program/mojo-bridge/sql/alert_sync_log.sql