Lines 198-238markdown
198 "end_time_local": "13:30",
199 "duration_minutes": 90,
205If during skill execution you cannot find a confirmation code on the post-book page, fall back to navigating to `https://www.rec.us/account/reservations` and reading the most-recently-created entry; its `id` UUID prefix is the canonical confirmation code.
207### 7. Session teardown
209No explicit release step — there is nothing to release. The session persists across calls keyed by `profile`/`proxy`. Batching the whole warm-up → date-pick → slot → login → book flow into the single `commands` array of step 5 keeps the Firebase session cookies together through to the booking POST and saves round-trips. If you do split the flow, repeat the same `profile`/`proxy` on each call to reconnect to the same logged-in session; a call that drops or changes it lands in a different, logged-out session.
211## Site-Specific Gotchas
213- **The API at `api.rec.us/v1` is public and unauthenticated for read endpoints.** No CORS shenanigans, no rate-limiting observed during skill generation, no captcha. `availability`, `schedule`, `price`, `sports`, `activities`, `organizations/{id}`, `locations/{id}` all return live JSON to anonymous callers. Use it. The Next.js front-end is a thin shell over this API.
214- **The site is not bot-protected.** No Akamai, no Cloudflare turnstile, no PerimeterX. A plain `browserless_agent` call (no `proxy` arg) loads the org listing fully hydrated; stealth is not needed and a residential proxy buys nothing but geo-stability. Skip the `proxy` arg entirely for this site.
215- **Login is Firebase Identity Toolkit, not a rec.us-owned endpoint.** The auth POST is:
217 POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword
218 ?key=AIzaSyCp6DCwnx-6GwkMyI2G1b8ixYs4AXZc-7s
HighSecret Pattern
Google API key in build/src/skills/sites/rec.us/sf-court-booking-c8l5z9/SKILL.md
build/src/skills/sites/rec.us/sf-court-booking-c8l5z9/SKILL.mdView on unpkg · L218 219 Body: {"returnSecureToken": true, "email": "...", "password": "...",
220 "clientType": "CLIENT_TYPE_WEB"}
222 Failure response is HTTP 400 `{"error":{"code":400,"message":"INVALID_LOGIN_CREDENTIALS"}}`. The Firebase Web API key in the URL is a public client identifier (not a secret) and was stable as of skill generation — pin it but treat key changes as a low-priority maintenance item, not a credential leak.
223- **Clicking the parent time-slot button does NOT reliably open the booking modal.** The button has two children: a "{H}:MM" paragraph and a div of duration cells ("60", "90"). Click the **duration cell** (`StaticText: "60"` or `"90"`), not the outer button. Verified: clicking the outer button left the page unchanged; clicking the inner duration text opened the modal correctly.
224- **Click "Log in" in the page header BEFORE clicking "Book" in the modal.** If you click Book unauthenticated, rec.us opens a _signup_ modal (with email, confirm-email, first/last name, phone, password, address fields). There is a "Already have an account? Log In" link to switch — but pre-authing avoids the back-and-forth entirely.
225- **Reservation windows differ per court at the same location.** At Alice Marble, Courts 1–3 are reservable 7 days in advance with 90-min fixed slots starting on 90-min boundaries (7:30, 9:00, 10:30, 12:00, 1:30, 3:00, 4:30); Court 4 is reservable only **2 days** in advance with 60-min slots on the hour. The `defaultReservationWindowDays` field on each court in the availability response tells you which.
226- **Reservations open at a fixed local time, not 24h ahead.** `reservationReleaseTimeLocal` defaults to `08:00:00` (Court 4 at Alice Marble: `12:00:00`). Trying to book outside the release window returns an error from the booking POST — there's no client-side guard. For the skill, check that `(date - today_local) ≤ defaultReservationWindowDays` AND that the current local time is past `reservationReleaseTimeLocal` if booking the maximum-window day.
227- **Booking limits are enforced server-side, not surfaced in the UI.** Alice Marble's `playGuidelines` field on `/v1/locations/{id}?publishedSites=true` says "may not book more than 1 court in a day or 3 courts in a calendar week" — but the booking modal still shows a "Book" button to users who would exceed it. The POST will fail with a (currently unverified) error response; document this as `reason: "booking_limit_exceeded"` in your output.
228- **Schedule slot keys use `"HH:MM, HH:MM"` with a comma-space separator**, and the `dates` map key is `"YYYYMMDD"` (no separator). Don't confuse with ISO format. Times are local to the location's `timezone` (`America/Los_Angeles` for all SF locations).
229- **`RESERVABLE` windows are contiguous, not pre-split.** A `"12:00, 16:30": {"referenceType":"RESERVABLE"}` entry means 4.5h of contiguous reservability, not a single slot. The booking-policy on the court (in `availability.courts[].config.bookingPolicies`) specifies fixed-slots — e.g. `{startTimeLocal:"12:00:00", endTimeLocal:"13:00:00"}` × N — that you intersect with the contiguous block to enumerate sub-slots. The UI does this client-side.
230- **Court selector defaults to "Court 1" but you must verify the chosen court is actually `RESERVABLE` at the chosen time** — the modal's court dropdown lists _all_ courts at the location, not just available ones. Pick the court from your step 4 enumeration, then switch the modal's combobox if it didn't preselect correctly.
231- **Pickleball + Tennis can share a court.** At Buena Vista, the location-availability shows the same physical court with `sports: [{name:"Tennis"},{name:"Pickleball"}]`. The schedule's `RESERVABLE` block is shared — a tennis reservation at 12:00 blocks pickleball at 12:00 on that court. Filter by `sport.name` _and_ by the schedule.
232- **URL filter param**: `?activityId={uuid}` on the org landing page filters the listing client-side. Useful for screenshots but not strictly required by the API path — the schedule API already returns per-sport data.
233- **Stripe checkout is in-page, not redirected.** A card-on-file (added during account creation) is used silently; no Stripe.js redirect to checkout.stripe.com observed. If the user has no card, expect the modal to show a "Add payment method" step before the final Book button.
234- **Per-location `noReservationText: "Not Reservable"` means walk-up-only courts.** A few locations (e.g. some Upper Noe slots) show "No free spots available - Check back soon" indefinitely — that's the location's status, not a transient stockout. Surface as `reason: "location_not_reservable"`.
235- **The org's `/discovery/programmed` endpoint returned `[]` during testing** — it's intended for programs/classes, not court reservations. Don't waste time on it.
236- **`_next/data/{buildId}/...` SSR data endpoints return only `{pageProps: {selectedTabId: null}}` for location pages** — the page does all real fetching client-side via `api.rec.us`. Don't try to scrape Next.js data routes for availability.
237- **Confirmation code format is not verified end-to-end.** The skill-generation run did not have a real account, so the post-booking confirmation page was not captured. The format documented above (8 hex chars matching the reservation UUID prefix) is inferred from `referenceId` UUID conventions visible in the schedule API. A future agent with a real account should verify and update this skill.