Skip to content

Repository files navigation

Linq Strava Agent

A Strava running coach you access by text message. Connect your Strava once, then text the bot for your runs, training stats, heart rate, and personal records — and it'll nudge you to lace up. It even follows up on what it remembers ("how are the knees since those back-to-backs?").

Built on Linq Blue and powered by Claude (Anthropic). Same open-source pattern as the ai-agent-example, linq-resy-agent, and granola-agent.

What it does

  • Recent runs — "how were my runs this week?" → distance, pace, time per activity
  • Training stats — this week, recent 4 weeks, year-to-date, all-time totals
  • Heart rate — average & max HR per run, flags unusually high readings (never fabricates — says so when a run has no HR)
  • Best efforts / PRs — fastest 1 mile, 5k, 10k, etc. across recent runs, with all-time PRs flagged 🏅
  • Run reminders — "remind me to run every day" → a daily nudge + a personalized, context-aware check-in
  • Builds context over time — remembers injuries, goals, races, PRs, and cross-training, and brings them up later
  • Proactive — shows your real last run the instant you connect

Architecture

User ──iMessage──▶ Linq Blue ──webhook──▶ Receiver Lambda ──SQS──▶ Processor Lambda ──▶ Claude (tool-use loop)
                       ▲              (HMAC verify,           (FIFO)        │                     │
                       │           read receipt + typing,                   │     ◀── tools ◀─────┘
                       │            enqueue ~150ms)                         │     strava_recent_activities
                       │                                                    │     strava_activity_detail
User ◀─iMessage────────┴──────────────── Linq Blue ◀──────API────────────  │     strava_athlete_stats
                                                                            │     strava_best_efforts
  EventBridge (daily cron) ──▶ Reminder Lambda ──▶ personalized check-ins   │     strava_athlete_profile
                                + drains the waitlist when capacity opens    ▼     set_run_reminder
                                                                       DynamoDB (state, encrypted creds)

Three Lambdas:

  • Receiver — verifies the Linq webhook HMAC, fires read-receipt + typing instantly, enqueues to SQS, and serves the Strava OAuth pages (/auth/strava, /auth/strava/callback, /auth/setup).
  • Processor — SQS-triggered; runs the Claude tool-use loop against the Strava API and replies via Linq.
  • Reminder — EventBridge cron; sends personalized check-ins to subscribers and drains the capacity waitlist.

State lives in a single DynamoDB table; Strava tokens are AES-256-GCM encrypted at rest.

Quick Start

Prerequisites

Local development

npm install
cp .env.example .env   # fill in your keys
npm run dev            # Express server on :3000

# in another terminal, expose it:
cloudflared tunnel --url http://localhost:3000   # (or: ngrok http 3000)

Then point a Linq webhook at https://<your-tunnel>/linq-webhook:

linq webhooks create --url https://<your-tunnel>/linq-webhook --events message.received

Text your Linq Blue number to start.

Deploy to AWS Lambda

npm run build:lambda          # esbuild → dist/handlers/{receiver,processor,reminder}.js
cd infra
sam deploy --guided \
  --parameter-overrides \
    LinqApiToken=... AnthropicApiKey=... CredentialEncryptionKey=<64 hex> \
    LinqAgentBotNumbers=+1XXXXXXXXXX \
    StravaClientId=... StravaClientSecret=... \
    LinqWebhookSigningSecret=...

Generate the encryption key with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

The stack outputs an ApiUrl — create a Linq webhook pointing at ${ApiUrl}/linq-webhook, and set that domain as your Strava app's Authorization Callback Domain.

The @anthropic-ai/sdk ships as a Lambda layer. If you don't already have one in the account, run npm run build:layer and point AnthropicLayerArn at it.

Strava setup & scale

  1. Create an app at strava.com/settings/api. Set the Authorization Callback Domain to your deployed API Gateway domain.
  2. New apps are capped at 1 connected athlete; self-upgrade to 10 in the dashboard.
  3. Beyond 10, submit your app for review (requires 10 connected athletes first).

When at capacity, the agent waitlists new users (and still sends them generic run nudges) instead of showing Strava's 403 page — then auto-texts them a connect link the moment a slot opens. Set the cap via the STRAVA_ATHLETE_QUOTA env var.

Project Structure

src/
├── handlers/
│   ├── receiver.ts       # API Gateway: webhook (HMAC) + SQS enqueue + OAuth pages
│   ├── processor.ts      # SQS: Claude tool-loop + Strava + reply
│   └── reminder.ts       # cron: personalized check-ins + waitlist drain
├── claude/client.ts      # system prompt, tools, tool-use loop (prompt-cached)
├── strava/
│   ├── client.ts         # Strava API v3 client (OAuth + data endpoints)
│   ├── index.ts          # token refresh + metric formatting + best efforts
│   └── types.ts          # Strava type definitions
├── auth/                 # OAuth, encrypted creds, magic links, waitlist, reminder registry
├── linq/client.ts        # Linq Blue V3 API (messages, typing, read, reactions, contact card)
├── state/conversation.ts # conversation history + persistent user profiles
├── webhook/types.ts      # Linq webhook payload types
└── db/dynamodb.ts        # single-table DynamoDB helpers
infra/template.yaml       # SAM: 3 Lambdas + DynamoDB + SQS (FIFO) + EventBridge

Security

  • Strava tokens AES-256-GCM encrypted at rest; secrets passed as SAM NoEcho params (never in the repo).
  • Inbound webhooks HMAC-SHA256 verified (X-Webhook-Signature over {timestamp}.{body}) — forged requests are rejected.
  • Magic links: 32-byte random, single-use, 15-minute TTL.
  • Phone numbers redacted in logs.

Contact card avatar

The agent shares a Linq contact card so users save it as "Strava Coach" with the Strava logo (set via POST /v3/contact_card, shared once per chat via share_contact_card).

⚠️ Avatar format matters. Use a truecolor RGB(A) square PNG (≈512×512). Apple's contact-photo / iMessage avatar pipeline renders indexed/palette PNGs (colortype 3) inconsistently — they show on some devices and silently fall back to the gray monogram on others, which reads as "the logo disappeared." The canonical asset lives at public/strava-avatar.png (512×512, truecolor RGB). Verify a candidate with:

python3 -c "import struct; d=open('img.png','rb').read(); print('colortype', d[25])"  # want 2 (RGB) or 6 (RGBA), NOT 3

Note: iOS caches contact photos per-device — after updating, users may need to tap the shared card → Update Contact once.

License

MIT — Linq Technologies Incorporated.

Powered by Strava. Built on Linq + Claude.

About

A Strava running-coach agent accessible via iMessage. Tracks runs, stats & heart rate, and nudges you to lace up. Built on Linq Blue and Claude.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages