Golf bot — Diamond Oaks Country Club playbook¶
Captured: 2026-06-08 (walkthrough + autonomous dry-run via chrome-devtools MCP)
Booking engine: IBS Vision (goibsvision.com/WebRes/Club/DiamondOaks/)
Auth bridge: SSO cookie from diamondoaksclub.com login → goibsvision session
Account: D0322-001 (Johnathan Offringa, spouse acct) — creds in ~/.claude/.env
Step-by-step HTTP flow¶
Step 1 — Login (diamondoaksclub.com)¶
POST https://www.diamondoaksclub.com/public/member-login-116.html
Form: User=D0322-001 & Password=<env GOLF_CLUB_PASS>
→ 302 → /member-home-162.html
Cookies set: ASP session on diamondoaksclub.com
Step 1b — SSO bridge to goibsvision.com ⚠️ NEW (was missed in v1)¶
GET https://www.diamondoaksclub.com/my-account/online-tee-times-175.html
→ 302 → goibsvision.com/WebRes/Club/DiamondOaks/LoginWithToken/{ONE-TIME-TOKEN}?returnUrl=/WebRes/Club/DiamondOaks/Browse
→ 302 → /Browse (now authenticated, session cookie set)
/WebRes/Club/DiamondOaks/Login but uses DIFFERENT credentials than the club site (we don't have those, don't need them). Diamond Oaks login alone does NOT cookie-bridge automatically; the online-tee-times-175.html URL is required as the explicit hop.
Step 2 — Search slots (goibsvision.com)¶
POST /WebRes/Club/DiamondOaks/BrowseTeeTimes
Form:
CriteriaDate = M/D/YYYY (e.g. "6/14/2026")
CriteriaTime = "7:00 AM" (literal, 15-min increments 5:00 AM - 7:00 PM)
NumberOfPlayers = "1"-"5"
Holes = "9" | "18"
Criteria.Facilities.Count = "1"
Facilities[0].IsChecked = "true"
Facilities[0].FacilityID = "b9ce4000-5c96-439c-9e76-316ae97295ea" (Diamond Oaks — constant)
Facilities[0].ConsoleFacilityID = "ffaafc71-69c1-4842-98f1-4f5cd17a6046" (constant)
Facilities[0].IsFavorite = "False"
→ HTML response with N <form class="resultForm"> blocks, one per available slot
Step 3 — Parse slot HTML¶
Each <form class="resultForm"> inside <div class="browseresult"> has:
<td class="time">6:40 AM</td> <!-- slot time, scrape this -->
<form action="/WebRes/Club/DiamondOaks/BookingStart" method="post" id="book-N">
TargetSlotDate 20260609 (YYYYMMDD)
TargetSlotTime 0700 (HHMM, 24h — search criteria, not slot time)
FacilityID b9ce4000-... (Diamond Oaks UUID)
Holes 18
NumberOfPlayers 1
SlotID {uuid-per-slot} ← unique to each slot
FacilityIDs b9ce4000-...~ (UUID + trailing ~)
<input type=submit value="Book">
</form>
Bot logic:
1. Filter slots where <td class="time"> parsed ≤ 12:00 noon
2. Sort ascending, pick earliest
3. Capture that slot's SlotID
Step 4 — Initiate booking¶
POST /WebRes/Club/DiamondOaks/BookingStart
Form: the slot's full input set (TargetSlotDate, TargetSlotTime, FacilityID, Holes, NumberOfPlayers, SlotID, FacilityIDs)
→ 302 → GET /WebRes/Club/DiamondOaks/ReservationDetails
Step 5 — Commit¶
POST /WebRes/Club/DiamondOaks/ReservationDetails
Form (#reservationForm):
ReservationRecord.ReservationID = "00000000-0000-0000-0000-000000000000"
BrowseSettings.DesktopEmployeeID = "7a5fdbd9-5a6b-40c0-8411-61aa424be054"
BrowseSettings.MobileEmployeeID = "ecb9966c-934a-43a2-80bd-d6e5b864ee68"
IsMainReserver = "True"
IsNew = "True"
EmailAddress = "johnojiggs@gmail.com"
ReservationRecord.Notes = ""
→ 302 → GET /BookingFinal
→ 302 → GET /BookingConfirmation/{8-CHAR-ID}?isNew=true
2VIU13KO). IBS auto-sends native confirmation email.
Cancellation (idempotency / safety undo)¶
POST /WebRes/Club/DiamondOaks/Profile/CancelReservation
Headers: X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded
Form: club=DiamondOaks & resID={UUID}
→ 200 JSON
resID is a UUID, distinct from the 8-char URL ID. ⚠️ Critical: the UUID exposed in the BookingConfirmation page hidden inputs is NOT the cancel UUID. IBS uses a separate UUID specifically tied to the cancel action.
Scrape the cancel UUID from /Profile/Reservations via the inline onclick="showCancelWindow('UUID')" handler. Pattern:
// JS to scrape the cancel resID for a given conf_id from /Profile/Reservations
const html = document.body.innerHTML;
const idx = html.indexOf(confId);
const forward = html.substring(idx, idx + 8000); // cancel UUID is ~3-5K chars after conf_id (past the player list)
const m = forward.match(/showCancelWindow\(\s*'([0-9a-f-]{36})'/i);
const resID = m ? m[1] : null;
⚠️ Always check the cancel response's ErrorMessages array. HTTP 200 + non-empty ErrorMessages = failed cancel (e.g., ["InvalidReservation"]). Successful cancel returns {"CancellationNumber":"<8-char-id>","ErrorMessages":[],...}.
Anti-bot reality¶
goibsvision.com has CF gating on anonymous traffic, BUT auth flows through diamondoaksclub.com SSO → goibsvision.com/LoginWithToken/{tok} → /Browse. The SSO token bypasses the turnstile challenge. Verified 2026-06-09 06:54:52 — fully headless auto-login via ~/.claude/scripts/golf_autologin.py completes in ~3s with no challenge fired (see memory: reference_golf_autologin_works.md).
Mitigations:
- Preferred: golf_autologin.py runs Playwright with channel: "chrome" + persistent profile, drives the SSO chain unattended. No human in the loop.
- Persist cookies between bot runs (profile dir at ~/golf-bot/chrome-profile/)
- Fallback ONLY IF auto-login starts failing: playwright-extra + puppeteer-extra-plugin-stealth, or true headed manual prewarm
Script remains tolerant of session expiry → on 403/challenge response, retry via _do_login SSO chain before paging Devin.
Open question: 5-player capacity¶
UI dropdown allows NumberOfPlayers=1-5. BUT the ReservationDetails hidden field shows capacity=4. Suggests one of:
- Tee slot has hard max 4, dropdown allows 5 but booking will reject
- "Foursome standard" + 1 additional via Add Player flow (which IBS may handle differently)
- Diamond Oaks specifically allows 5 (Devin's verbal claim) — UI hint of 4 is generic platform default
Validation needed at first live run. Bot should:
- Try NumberOfPlayers=5 first
- If rejected, retry with NumberOfPlayers=4 and flag "5-player rejected, booked 4" in notification email
Notification design (email-only path)¶
Account-of-record email = johnojiggs@gmail.com. IBS native confirmation goes there.
For Devin to receive:
- Option A (simplest): Bot sends Devin an independent "Booked Sat 6/13 at 6:40 AM, confirmation 2VIU13KO" via gmail-personal MCP. Diamond Oaks's own email goes to Johnathan; Devin gets the bot's note.
- Option B: Set up Gmail forwarding from johnojiggs@gmail.com → devindavidson7@gmail.com (requires Johnathan's cooperation, ~3 min in his Gmail settings).
- Option C: Add Devin's email as CC in IBS Profile / Edit Profile / Notification settings (TBD if supported).
Default = A. Bot's notification format:
Subject: [GOLF] Booked Sat 6/13 06:40 AM — 2VIU13KO
Diamond Oaks tee time confirmed.
Date: Saturday, June 13, 2026
Time: 6:40 AM
Tee: Tee 1
Players: 5
Holes: 18
Confirmation: 2VIU13KO (resID: <UUID>)
Native confirmation also sent to johnojiggs@gmail.com.
Cancel: bot will reverse with `python golf_cancel.py 2VIU13KO`.
When TFV approves (SMS path)¶
Swap the gmail-personal send for ~/.claude/scripts/twilio_send.py:
GOLF_NOTIFY_CHANNEL=email|sms. Flip to sms once TFV_STATUS=approved.
Cloud edition deployed 2026-06-11¶
Status: live + tested. GH Actions cron + CF Worker + CF Pages PWA at:
- PWA: https://golf-bot.pages.dev
- Worker: https://golf-bot-api.devindavidson7.workers.dev
- Repo: https://github.com/StampReady/golf-bot
- Source:
FounderOS/ventures/golf-bot-app/ - Re-deploy:
cd FounderOS/ventures/golf-bot-app && bash deploy.sh(idempotent)
Schedule in GH Actions:
- book.yml cron 0 11 * * 3 Wed UTC (06:00 CT) → Sat booking
- book.yml cron 0 11 * * 4 Thu UTC (06:00 CT) → Sun booking
- health.yml cron 0 14 * * * (09:00 CT) auth probe
Workflow starts 1h early; golf.py --poll-until 11:59:55 (UTC) sleeps until 06:59:55 CT, polls until 07:00:01 CT slot release.
Laptop tasks DELETED 2026-06-11. Cloud is sole booker. First scheduled cron fire is Wed 2026-06-17 06:00 CT (books Sat 6/20). If that fails, laptop bot code at ~/.claude/scripts/golf.py is still intact and can be re-scheduled via golf_install_tasks.bat.
Schedule (Windows Task Scheduler — legacy, soon-to-be-deleted)¶
Two tasks, both as Devin's user, both wake the system if asleep:
| Task name | Trigger | Books for |
|---|---|---|
| GolfBot-Sat | Wed 06:59:55 CT | Sat (3 days out) |
| GolfBot-Sun | Thu 06:59:55 CT | Sun (3 days out) |
Pre-run step (-5 sec): w32tm /resync to sync system clock with NTP. Slot release at exactly 7:00 AM CT — even a 2-second drift loses the slot.
Install method: XML ONLY. The legacy schtasks /Create /SC WEEKLY /ST 06:59:55 /IT CLI form is broken — it strips seconds, leaves Power Management at "Stop On Battery / No Start On Batteries", and has no WakeToRun flag. Both 6/10 (Sat task) and 6/11 (Sun task) silently failed to fire due to that. Use ~/.claude/scripts/golf_install_tasks.bat which calls schtasks /Create /F /XML golf_task_{sat,sun}.xml. XML must include:
- <WakeToRun>true</WakeToRun>
- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
- <StartWhenAvailable>true</StartWhenAvailable>
Verify post-install:
Get-ScheduledTask -TaskName "GolfBot-Sat","GolfBot-Sun" | Select-Object TaskName,
@{n='WakeToRun';e={$_.Settings.WakeToRun}},
@{n='NextRun';e={(Get-ScheduledTaskInfo $_.TaskName).NextRunTime}}
Resilience gap: WakeToRun handles sleep + battery, but NOT full shutdown or hardware failure. For true zero-machine-dependency operation, deploy bot to a cloud worker (next iteration — session state in ~/golf-bot/chrome-profile/ would need to move).
Critical files (to be built)¶
~/.claude/scripts/golf_book.py— main booking script~/.claude/scripts/golf_cancel.py— cancel-by-conf-id helper~/.claude/scripts/golf_session.py— login + cf_clearance bootstrap~/golf-bot/logs/YYYY-MM-DD.log— run-by-run logs- Task Scheduler XML exports — versioned in
FounderOS/scripts/
Captured during walkthrough (raw evidence)¶
- Login form snapshot: uid=3_24 (user), uid=3_28 (pass), uid=3_31 (Login)
- Calendar shows 8-day forward window (Mon→Mon)
- Booking IDs seen:
MRYZB6TR,2VIU13KO(both cancelled by Devin manually during walkthrough) - Slot UUIDs captured for 4 slots on Tue 6/9:
0fd9fa4b-...,f18e0984-...,bb433040-...,03407ea3-... - Cancellation POST body inspected at reqid=557:
club=DiamondOaks&resID=af421924-8cdc-4570-962e-ad71c2fb1222