Skip to content
pythonjeffPublic

About

SMS based AI Agent. Hyperpersonal.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

224 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Palmer

CI

The AI that texts like a friend who actually pays attention.

Palmer is a personal AI delivered entirely over SMS. No app to download, no interface to learn — just text it. It knows who you are, remembers what you care about, and reaches out when something worth knowing happens.


What Palmer does

Knows you

Every exchange teaches Palmer something new. Your city, your job, your teams, your interests — it builds a picture of you and uses it the way a real friend would: casually, in context, without citation. The longer you text, the sharper it gets.

Sends your morning

Each morning Palmer texts you a short, personalized briefing on what you actually care about — weather, local traffic, markets, sports, news. Not a newsletter. One text, just the things that matter, from today.

Watches for things

Tell Palmer to watch for something — a geopolitical event, a company move, an athlete's health update, anything — and it runs that in the background. When it hits, you get a text. No feed to check.

Sets reminders

Natural-language reminders that arrive when you need them. "Remind me Friday morning to prep for the meeting." Done.

Checks in, occasionally

Every week or two, one text about one thing: how your team did, a story on a subject you follow, or something you mentioned — an interview, a move, a rough week. Not scripted, not daily. Anything you set a watch on still arrives when it moves; nothing else arrives on Palmer's own initiative.

Live scores, if you ask

Follow a team and it shows up in your morning text and on your page. Want texts during the game? Say so, and pick a level: the big moments only (lead changes, a late score, the final) or every score. Palmer offers it once when you mention a team, and never turns it on for you.

Sees photos

Send Palmer a picture and it'll actually respond to what's in it — a menu, a whiteboard, a receipt, a dog. It's using vision, not guessing from a filename.

Answers anything

Crypto prices, stock quotes, weather forecasts, sports scores, current events, city traffic, drive times with live traffic — all through the same text thread, no switching apps.

Stays out of the way

Tell Palmer to pause the morning texts, drop a topic, forget a fact about you, or change when it sends — all by texting. No settings screen.


Personality

Palmer is dry, quick, and observant. It's not an assistant and it's not a brand — it's a voice. It reads subtext, matches your energy, calls out patterns, and knows when to drop a GIF. Built on Claude, it converses like a person, not a product.


Stack

Layer Technology
Conversation + reasoning Claude Sonnet 4.6
Fast inference (extraction, scoring) Claude Haiku 4.5
SMS/MMS Twilio
Web server FastAPI on Heroku
News + web search Tavily
Weather NWS (US), Open-Meteo (elsewhere)
Traffic + routing TomTom (Traffic Flow, Traffic Incidents, Routing, Search geocoding)
Product prices + sale watches SerpAPI Google Shopping
Crypto prices CoinGecko
Stock prices yfinance
GIFs Giphy
Background jobs APScheduler (reminders 1m · mornings 5m · watches 30m · missing-data asks 60m · check-ins every 2h tick, paced in days · live scores 2m, opt-in per team · price watches twice daily · flight watches daily)
Database Heroku Postgres

Running Palmer

1. Clone and install

git clone https://github.com/pythonjeff/Palmer.git
cd Palmer
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt

2. Environment

cp .env.example .env
# Fill in the variables below
Variable Where to get it
ANTHROPIC_API_KEY console.anthropic.com
TWILIO_ACCOUNT_SID twilio.com dashboard
TWILIO_AUTH_TOKEN twilio.com dashboard
TWILIO_PHONE_NUMBER Your Twilio number (+15551234567)
TAVILY_API_KEY app.tavily.com
GIPHY_API_KEY developers.giphy.com (free)
TMDB_API_KEY themoviedb.org (free, non-commercial) — Opening section
TICKETMASTER_API_KEY developer.ticketmaster.com (free, 5k calls/day) — Opening section
TOMTOM_API_KEY developer.tomtom.com (free tier, ~2,500 requests/day)
SERP_API_KEY serpapi.com (free plan is 250 searches/mo; paid from $50/mo for 5,000)
APP_URL Your deployed app URL (for Twilio status callbacks)
DATABASE_URL Postgres connection string (auto-set by Heroku; omit locally to use SQLite)
PALMER_CONTACT_EMAIL Contact address sent in the User-Agent to the National Weather Service, which requires one

3. Run locally

uvicorn main:app --reload

4. Expose for Twilio (local dev)

ngrok http 8000
# Set Twilio webhook → https://<ngrok-url>/sms (POST)

Deploy to Heroku

heroku create
heroku addons:create heroku-postgresql:essential-0
heroku config:set ANTHROPIC_API_KEY=... TWILIO_ACCOUNT_SID=... TWILIO_AUTH_TOKEN=... \
  TWILIO_PHONE_NUMBER=... TAVILY_API_KEY=... GIPHY_API_KEY=... \
  TOMTOM_API_KEY=... APP_URL=...
heroku config:set WEB_CONCURRENCY=1
git push heroku main

Set your Twilio SMS webhook to https://<your-app>.herokuapp.com/sms (POST).
Set your Twilio status callback to https://<your-app>.herokuapp.com/sms-status (POST).

Morning briefings are sent by APScheduler inside the web dyno (checked every 5 minutes) at each user's chosen local time — 7am by default, changeable by texting Palmer. No Heroku Scheduler job is required; if one exists running python send_morning.py, it's a harmless redundant backup (the per-user sent-date guard prevents double-sends).


Preview endpoints

GET /preview?phone=+15551234567         # generate morning briefing without sending

Development

pip install -r requirements-dev.txt
pytest                      # ~1,400 tests, all offline, a few seconds
ruff check .                # lint

The tests need no real keys. A few modules read their API key at import time, so set placeholder values (TAVILY_API_KEY=x, TOMTOM_API_KEY=x, SERP_API_KEY=x, the Twilio and Anthropic vars) or copy .env.example to .env. PALMER_NO_SCHEDULER=1 keeps the job loop from starting when main is imported in a shell or a one-off script. CI runs the same two commands on every push.

Design notes — the reasoning behind each subsystem and the production incidents that shaped it — live in CLAUDE.md.


Architecture

  • WEB_CONCURRENCY=1 is required. In-memory phone locks and APScheduler only work correctly in a single process.
  • Per-phone threading.Lock serializes inbound messages so conversation history never interleaves under concurrent requests from the same number.
  • Source quality is one gate, applied at the search call (sources.py). Every news fact and link Palmer sends — watch alerts, the morning briefing, Palmer Home, and conversational web search — goes through datafeeds._search_raw / _search, which filter before returning, so no surface can drift to its own standard. Four gates: a blocklist dropping press-release wires (prnewswire, globenewswire) and republishing aggregators (msn.com, biztoc, news.google.com) outright; a relevance floor that gives trusted sources 0.15 of slack, because Tavily's score measures query-text match and that is exactly what an SEO content farm is built to win; tier ordering from trusted_sources.json (tier 1 = wires and premier newsrooms plus .gov/.edu, tier 2 = mainstream and reputable specialists, tier 3 = everything else) sorting (tier, -score) so a wire report beats a higher-scoring blog; and corroboration requiring ≥ 2 distinct domains OR ≥ 1 tier-1 source before any unprompted alert fires. Palmer Home additionally drops tier 3 entirely — an untrusted row is worse than no row on a short curated list. Edit trusted_sources.json to add or remove sources; no code change needed.
  • Watches run every 30 minutes via APScheduler but only alert on major breaking developments — dated results from the last 12 hours, a strict criticality gate, a HEAD reachability check so a dead top link falls through to the next result, per-watch cooldown (default 4 hours), and dedup against the last alerted event.
  • Traffic uses TomTom. Morning briefings auto-include a short city snapshot (get_city_traffic): geocode → parallel Traffic Flow + Traffic Incidents in a city bounding box → Haiku drafts one natural line, or skips silently on failure. On demand, users can ask for city conditions or point-to-point drive times (get_travel_time, live traffic vs. free-flow). Landmark destinations (White House, Fenway, LAX) get resolved to street addresses by Sonnet before geocoding — TomTom's geocoder is a mapping API, not a search engine, and mis-ranks landmark names. A regular commute is saved with set_commute (geocoded once on save, with an optional leave time); the page's Commute card and the morning text are then routed for that departure via TomTom departAt, and say whether the number is a forecast for it or live traffic.
  • Price watches use SerpAPI Google Shopping (shopping.py): user texts "watch these sneakers under $80" → add_price_watch tool saves to price_watches → run_price_watches job runs twice a day on a cron (00:00 and 16:00 UTC — a cron rather than an interval so a deploy cannot reset the clock). Each run: SerpAPI Google Shopping query on the product name, Haiku picks the cheapest genuine match from the top candidates (guards against firing on unrelated accessories/refurbs), first successful check establishes a baseline silently, subsequent checks alert when the target is hit or the price has moved more than $2 in either direction (a flat bar, deliberately not a percentage, so a $12 grocery item and a $1,200 laptop are held to the same standard), then re-baseline. Cooldown defaults to 12h. Fails silently on any SerpAPI error — never surfaces "shopping tool failed" to the user.
  • Proactive outbound is scheduled: mornings at each user's local time (5-min tick, catch-up window, per-day guard), check-ins on a 2h tick but paced in days (followup.GAP_DAYS; Haiku picks one subject from their threads, their team's game or the page's headlines by echoing it exactly, Sonnet drafts, 1–7pm window), and missing-data asks every 60 min for users onboarded without a city so mornings can target them (7-day cooldown, US-daytime UTC window; DATA_ASK_DRY_RUN=1 to preview).
  • Reminder delivery uses FOR UPDATE SKIP LOCKED on Postgres — safe for multiple scheduler ticks, no double-sends.
  • Twilio webhook signatures (HMAC-SHA1) validated on every inbound request. All DB queries parameterized and scoped to phone number.
  • Tool routing is hard: get_weather → NWS/Open-Meteo only, get_price → CoinGecko/yfinance only, traffic tools → TomTom only, product price watches → SerpAPI only, web search → Tavily news mode. No overlap, no hallucinated data.

License

Copyright © 2026 Jeff Larson. All rights reserved. The source is published for reading and evaluation; see LICENSE.

About

SMS based AI Agent. Hyperpersonal.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages