• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
nbbaier

nbbaier

chicago-resident-days

Public
Like
chicago-resident-days
Home
Code
10
.claude
1
backend
7
docs
3
.vtignore
AGENTS.md
CLAUDE.md
README.md
biome.json
deno.json
format.ts
Environment variables
2
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
Code
/
Search
README.md

Chicago Free Museum Days

A Bluesky bot that posts weekly updates about free museum days in Chicago, built on Val Town.

πŸ€– Follow the bot: chicagomuseumdays.bsky.social

Overview

Many Chicago museums offer free admission days for Illinois residents, but finding this information is scattered and hard to track. This project:

  1. Scrapes free museum day data from Choose Chicago
  2. Normalizes dates (handles ranges, recurring patterns like "every Wednesday")
  3. Stores data in SQLite with manual override support
  4. Posts weekly summaries to Bluesky every Monday morning
  5. Serves data via a public JSON API

Architecture

Choose Chicago (HTML)
    ↓
scrapeChooseChicago() β†’ [{institution, rawLines[], url}]
    ↓
normalizeDates() β†’ [{institution, date, time, proof_required, notes}]
    ↓
loadToSQLite() β†’ scraped_free_days table
    ↓
    β”œβ”€β”€ bot.cron.ts β†’ Bluesky post (weekly)
    └── api.http.ts β†’ JSON API (on-demand)

Val Structure

FileTypeDescription
db.tsScriptSQLite database functions
scrapeChooseChicago.tsScriptHTML scraper using cheerio
utils.tsScriptDate normalization with chrono-node
scraper.cron.tsCronMonthly scraper orchestrator
bot.cron.tsCronWeekly Bluesky poster
api.http.tsHTTPPublic JSON API

API Endpoints

Once deployed, the HTTP val exposes:

EndpointDescription
GET /API documentation
GET /free-days?start=YYYY-MM-DD&end=YYYY-MM-DDFree days in date range
GET /upcomingNext 7 days
GET /todayToday's free days
GET /institutionsList all museums
GET /healthHealth check

Example Response

{ "data": [ { "institution": "Art Institute of Chicago", "date": "2025-01-27", "time": "11am-close", "proof_required": "Illinois residents with ID", "notes": null } ], "count": 1 }

Database

Two SQLite tables with override support:

  • scraped_free_days β€” Auto-populated by scrapers, replaced on each run
  • free_days_overrides β€” Manual corrections that take precedence

Uses institution_key (slugified name) for reliable matching across sources. Overrides can also cancel incorrect scraped entries via is_cancelled.

Setup

1. Deploy to Val Town

Copy the backend files to your Val Town project. Files ending in .cron.ts become cron vals, .http.ts becomes an HTTP val.

2. Set Environment Variables

In Val Town secrets, add:

BLUESKY_HANDLE=your-handle.bsky.social
BLUESKY_PASSWORD=your-app-password

Tip: Use a Bluesky App Password instead of your main password.

3. Initialize Data

Run scraper.cron.ts manually to populate initial data.

4. Configure Cron Schedules

ValScheduleCron Expression
scraper.cron.tsFirst Monday of month0 14 1-7 * 1 (UTC)
bot.cron.tsEvery Monday 8am CentralSee note below

Timezone Note: Val Town cron runs in UTC. For 8am Central Time:

  • Winter (CST, UTC-6): 0 14 * * 1
  • Summer (CDT, UTC-5): 0 13 * * 1

Date Parsing

The utils module handles various date formats:

  • Specific dates: "January 15" β†’ 2025-01-15
  • Date ranges: "January 5 - February 28" β†’ expands to individual dates
  • Recurring: "every Wednesday" β†’ generates dates for next 12 months
  • Weekdays: "weekdays only" β†’ generates Mon-Fri for next 12 months

Lines are classified as either date-ish or note-ish using pattern matching, since Choose Chicago mixes dates and eligibility requirements in the same lists.

Dependencies

  • cheerio β€” HTML parsing
  • chrono-node β€” Natural language date parsing
  • @atproto/api β€” Bluesky SDK
  • hono β€” Lightweight web framework

Contributing

Data corrections can be added via the free_days_overrides table:

import { addOverride } from "./db.ts"; await addOverride("Art Institute of Chicago", "2025-02-15", { time: "10am-5pm", notes: "Presidents Day special hours", override_reason: "Confirmed via museum website", });

To cancel an incorrect scraped entry:

await addOverride("Some Museum", "2025-03-01", { is_cancelled: true, override_reason: "Event was cancelled per museum announcement", });

Maintenance

Monthly:

  • Review scraper logs for parsing failures
  • Add overrides for unparseable dates

Quarterly:

  • Audit data against official museum websites
  • Update parser patterns if needed

Resources

  • Implementation Spec β€” Full technical details
  • Val Town Docs
  • Choose Chicago Source

License

MIT

Code
.claudebackenddocs.vtignoreAGENTS.mdCLAUDE.mdREADME.mdbiome.jsondeno.jsonformat.ts
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
Β© 2026 Val Town, Inc.