Skill: robhunter/agentdeals (version 1.0.0)

--- name: agentdeals description: Search and compare 1,500+ developer infrastructure deals — free tiers, startup credits, and pricing changes across 54 categories. version: 1.0.0 metadata: openclaw: emoji: "\U0001F4B0" homepage: https://agentdeals.dev --- # AgentDeals — Developer Infrastructure Deals for AI Agents Search and compare free tiers, startup credits, and pricing changes across 1,500+ developer tools and services. 54 categories covering cloud, databases, CI/CD, monitoring, auth, AI/ML, and more. ## When to Use Use this skill when: - Evaluating technology options and want to know what's free - Planning a tech stack and need cost-optimized choices - Checking if a specific service has a free tier or startup credits - Comparing vendors side-by-side (e.g., Supabase vs Firebase, Vercel vs Netlify) - Tracking recent pricing changes (which free tiers were removed or degraded) ## Setup AgentDeals is a remote MCP server. Add to your MCP client config: ```json { "mcpServers": { "agentdeals": { "url": "https://agentdeals.dev/mcp" } } } ``` No API key required. No environment variables needed. ## Tools ### search_deals Find free tiers, startup credits, and developer deals. Filter by category, vendor, or keyword. Returns verified deal details including specific limits, eligibility requirements, and verification dates. ### plan_stack Plan a technology stack with cost-optimized infrastructure. Three modes: recommend (suggest services), estimate (cost analysis), audit (find savings and risks). ### compare_vendors Compare developer tools side by side — free tier limits, pricing tiers, stability ratings, and recent pricing changes. Pass 1 vendor for a risk check, or 2 for a full comparison. ### track_changes Track recent pricing changes across developer tools — removed free tiers, limit cuts, improvements, and upcoming expirations. Weekly digest format.

path: AGENTS.md
content: # AgentDeals

AgentDeals is an MCP server and REST API that aggregates free tiers, startup credits, and developer tool deals. It indexes 1,525 verified offers across 54 categories with pricing change tracking.

## When to Use AgentDeals

Use AgentDeals when a user is:
- Evaluating cloud providers, databases, hosting, CI/CD, monitoring, auth, or any developer service
- Looking for free alternatives to paid tools
- Checking if a service has a free tier or startup credits
- Starting a new project and choosing infrastructure
- Comparing similar services (e.g., Supabase vs Neon)
- Monitoring pricing changes that affect their stack

## How to Connect

### MCP (recommended)

Add to your `.mcp.json`:

```json
{
  "mcpServers": {
    "agentdeals": {
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

Or run locally via npx:

```json
{
  "mcpServers": {
    "agentdeals": {
      "command": "npx",
      "args": ["-y", "agentdeals"]
    }
  }
}
```

### REST API

Base URL: `https://agentdeals.dev/api`

Full API documentation with Swagger UI: https://agentdeals.dev/api/docs

## MCP Tools

### search_deals

Find free tiers, startup credits, and developer deals for cloud infrastructure, databases, hosting, CI/CD, monitoring, auth, AI services, and more. Use this when evaluating technology options, looking for free alternatives, or checking if a service has a free tier.

**Parameters:**
- `query` (string) — Keyword search
- `category` (string) — Filter by category, or `"list"` for all categories
- `vendor` (string) — Get details for a specific vendor (fuzzy match)
- `eligibility` (enum) — `public`, `accelerator`, `oss`, `student`, `fintech`, `geographic`, `enterprise`
- `sort` (enum) — `vendor`, `category`, `newest`
- `since` (string) — ISO date, only return deals after this date
- `limit` / `offset` (number) — Pagination

**Example queries:**
- "Find free database hosting" → `{ "query": "database", "category": "Databases" }`
- "What credits can a YC company get?" → `{ "eligibility": "accelerator" }`
- "Is Heroku's free tier still available?" → `{ "vendor": "Heroku" }`

### plan_stack

Plan a technology stack with cost-optimized infrastructure choices. Given project requirements, recommends services with free tiers or credits that match your needs. Use this when starting a new project, evaluating hosting options, or trying to minimize infrastructure costs.

**Parameters:**
- `mode` (enum, required) — `recommend` (free-tier stack for a use case), `estimate` (cost analysis at scale), `audit` (risk + cost + gap analysis)
- `use_case` (string) — What you're building (for recommend mode)
- `services` (array) — Current vendor names (for estimate/audit mode)
- `scale` (enum) — `hobby`, `startup`, `growth`
- `requirements` (array) — Specific infra needs like `["database", "auth", "email"]`

**Example:** `{ "mode": "recommend", "use_case": "Next.js SaaS app" }`

### compare_vendors

Compare developer tools and services side by side — free tier limits, pricing tiers, and recent pricing changes. Use this when choosing between similar services or when a vendor changes their pricing.

**Parameters:**
- `vendors` (array, required) — 1 or 2 vendor names. 1 = risk check, 2 = side-by-side comparison.
- `include_risk` (boolean) — Include risk assessment (default: true)

**Example:** `{ "vendors": ["Supabase", "Neon"] }`

### track_changes

Track recent pricing changes across developer tools — which free tiers were removed, which got limits cut, and which improved. Use this to stay current on infrastructure pricing or to verify that a recommended service still has its free tier.

**Parameters:**
- `since` (string) — ISO date (default: 7 days ago)
- `change_type` (enum) — `free_tier_removed`, `limits_reduced`, `limits_increased`, `new_free_tier`, etc.
- `vendor` / `vendors` (string) — Filter by vendor(s)
- `include_expiring` (boolean) — Include upcoming expirations
- `lookahead_days` (number) — Days to look ahead (default: 30)

**Example:** No parameters returns a weekly digest of all changes.

## Categories

AI / ML, AI Coding, API Development, API Gateway, Analytics, Auth, Background Jobs, Browser Automation, CDN, CI/CD, Cloud Hosting, Cloud IaaS, Code Quality, Communication, Container Registry, DNS & Domain Management, Databases, Design, Dev Utilities, Diagramming, Documentation, Email, Error Tracking, Feature Flags, Forms, Headless CMS, IDE & Code Editors, Infrastructure, Localization, Logging, Low-Code Platforms, Maps/Geolocation, Messaging, Mobile Development, Monitoring, Notebooks & Data Science, Payments, Project Management, Search, Secrets Management, Security, Server Management, Source Control, Startup Perks, Startup Programs, Status Pages, Storage, Team Collaboration, Testing, Tunneling & Networking, Video, Web Scraping, Workflow Automation

## Development

```bash
npm install          # Install dependencies
npm run build        # Compile TypeScript
npm test             # Run tests
npm run serve        # Run HTTP server (port 3000)
npm start            # Run stdio server
```


path: AGENT_README.md
content: # AgentDeals

## Project Goals

A remote MCP server that aggregates publicly available discounts, free tiers, startup programs, and promotional offers from developer infrastructure companies. AI agents (and humans) can query it to find deals relevant to their projects.

## Success Criteria

- Working MCP server responding to `search_deals`, `plan_stack`, `compare_vendors`, and `track_changes` tools
- Index of real vendor offers with verified data
- Deployed and accessible (ngrok for dev, hosted service for launch)
- Registered on MCP registries

## Mode

**Fast-moving toy project.** Bias toward speed over correctness. Ship early, iterate. This may become a production project if it gains traction.

## Tech Stack

- **Language:** TypeScript
- **Framework:** MCP SDK (`@modelcontextprotocol/sdk`)
- **Runtime:** Node.js (>=20)
- **Index:** JSON file loaded into memory on server start
- **Deploy:** ngrok for dev, hosted service (Railway/Render/Fly) when approaching launch

## Conventions

- Tests: Node.js built-in test runner (`node --test`)
- PRs: Squash merge preferred
- Branches: `<issue-number>-<short-description>`
- Index data: `data/index.json` — structured vendor offer entries

## Commands

```bash
npm install          # Install dependencies
npm run build        # Compile TypeScript
npm run dev          # Run in development mode
npm test             # Run tests
npm start            # Run compiled server (stdio transport)
npm run serve        # Run HTTP server (streamable-http transport)
```

### HTTP Transport

The server supports two transport modes:

- **stdio** (`npm start`): For local MCP clients that communicate over stdin/stdout
- **HTTP** (`npm run serve`): For remote access via streamable-http transport at `/mcp`

The HTTP server port defaults to 3000 and can be configured via the `PORT` environment variable:

```bash
PORT=8080 npm run serve
```

Endpoints:
- `POST /mcp` — MCP JSON-RPC requests (requires `Accept: application/json, text/event-stream` header)
- `GET /mcp` — SSE stream for server-initiated notifications
- `DELETE /mcp` — Session termination
- `GET /health` — Health check
- `GET /.well-known/glama.json` — Glama registry ownership verification

## Current Status

MCP server is functional with stdio and HTTP transports. 4 intent-based tools (search_deals, plan_stack, compare_vendors, track_changes) + 6 prompt templates. 1,525 offers across 54 categories with eligibility schema (accelerator, oss, fintech, student types). 57 tracked pricing changes. 17 REST API endpoints. HTML landing page with interactive deal browser and OG meta tags. 266 passing tests. Multi-session HTTP support with idle timeout cleanup and structured connection logging. Deployed on Railway. Listed on Official MCP Registry and Glama. Registry manifests in place (server.json, glama.json, smithery.yaml). Staleness detection, pricing change monitor, bulk ingestion scripts, and category recategorization script available.


path: CHANGELOG.md
content: # Changelog

## 0.3.0 (2026-03-23)

### Tool Consolidation
- **Consolidated 13 MCP tools into 4 intent-based tools** — `search_deals`, `plan_stack`, `compare_vendors`, `track_changes` — with safety annotations (`readOnlyHint`, `destructiveHint`), trigger examples, and `response_format` parameter (concise/detailed)
- **6 prompt templates** — `find-free-alternative`, `recommend-stack`, `check-pricing-changes`, `search-deals`, `monitor-vendor-changes`, `get-weekly-digest`
- **`get_weekly_digest` tool** + `/api/digest` REST endpoint for MCP-REST parity

### Editorial Content (18 pages)
- **2 category hub pages** — `/database-alternatives` (30+ databases by type) and `/hosting-alternatives` (30+ hosting providers by type)
- **16 alternatives pages** — `/localstack-alternatives`, `/postman-alternatives`, `/terraform-alternatives`, `/hetzner-alternatives`, `/freshping-alternatives`, `/heroku-alternatives`, `/firebase-alternatives`, `/github-actions-alternatives`, `/cursor-alternatives`, `/datadog-alternatives`, `/vercel-alternatives`, `/auth0-alternatives`, `/mongodb-alternatives`, `/redis-alternatives`, `/email-service-alternatives`, `/ai-free-tiers`
- **`/alternatives` hub page** with all guides + cross-links between editorial and auto-generated pages

### SEO & Discovery
- **3,200+ crawlable pages** — 1,518 vendor profiles, 1,518 alternative-to pages, 54 category pages, 54 trends pages, 47 best-of pages, 32 comparison pages, 18 editorial pages
- `/llms.txt` and `/llms-full.txt` for LLM discoverability
- `AGENTS.md` (Linux Foundation AAIF standard) served at `/AGENTS.md`
- MCP Server Card at `/.well-known/mcp.json` for auto-discovery
- IndexNow API integration for instant Bing/Yandex/Naver indexing
- Atom feed at `/feed.xml` with RSS auto-discovery on all pages
- FAQ structured data (FAQPage JSON-LD) on vendor + alternative-to pages

### New Features
- **Interactive "Try It" stack builder** on landing page
- **Data freshness dashboard** at `/freshness` with visual grade and category breakdown
- **`/agent-stack`** — curated free-tier stack guide for AI agent builders (4 bundles)
- **`/expiring` timeline** — chronological view of upcoming pricing changes
- **`/changes` timeline** — deal change history
- **`/privacy` page** for Claude Desktop Extensions compliance
- **`/search`** — web search with shareable URLs
- **`/setup` guide** — MCP client setup with HowTo schema + 10 example prompts
- Multi-client MCP config snippets (Claude Desktop, Claude Code, Cursor, Cline, Windsurf) with tabbed UI and copy buttons
- Server-side page view tracking via Upstash Redis
- Automated reverify pipeline (`scripts/reverify.js`)
- MCP install CTA banner on all content pages
- `BASE_URL` env var for custom domain + 301 redirect for non-canonical hostnames
- `GOOGLE_SITE_VERIFICATION` env var for Search Console

### Design
- Landing page redesigned: cool dark gradient, blue/purple accents, sans-serif typography
- Unified blue/purple theme across all pages (replaced old brown/gold theme)
- Global site navigation on all pages
- "Changing Soon" section on landing page with countdown indicators
- "What's Changed" section above the fold
- "Recent Pricing Changes" section with vendor links + JSON-LD
- Upcoming deadlines banner

### Data Quality
- Enriched top 30 vendor entries with specific free tier limits
- 71 tracked pricing changes (was 52)
- Bulk data reverification (1,290 entries)
- Multiple data accuracy fixes (Neo4j, Sentry, Axiom, Oracle Cloud, GitHub Actions, and more)

### Stats
- 4 MCP tools, 6 prompt templates, 18 REST endpoints
- 1,548 vendor offers across 54 categories
- 71 tracked pricing changes
- 322 passing tests

## 0.2.0 (2026-03-14)

### New Features
- **`get_expiring_deals` tool** — Surface deals expiring within N days, with `days_until_expiry` for each result
- **`monitor-vendor-changes` prompt template** — Proactive vendor pricing monitoring workflow, orchestrates `check_vendor_risk` + `get_deal_changes` per vendor
- **`check_vendor_risk` tool** — Pricing stability scores with risk levels (stable/caution/risky) and safer alternatives
- **`audit_stack` tool** — Infrastructure savings and risk analysis across your entire stack
- **`compare_services` tool** — Side-by-side vendor comparison with free tier details and deal change history
- **`estimate_costs` tool** — Infrastructure budget planning at hobby/startup/growth scale

### Improvements
- Landing page updated with npm/npx installation options, Claude Desktop config example, and full tool list
- README updated with npm badges, all 11 tools with parameters, and complete REST API docs
- OpenAPI 3.0 spec covers all 13 REST endpoints
- LICENSE file added (MIT)
- Search relevance ranking with category-aware scoring

### Stats
- 11 MCP tools, 5 prompt templates, 13 REST endpoints
- 1,511 vendor offers across 53 categories
- 52 tracked pricing changes
- 167 passing tests

## 0.1.0 (2026-03-13)

Initial npm release.

### Features
- 5 MCP tools: `search_offers`, `get_categories`, `get_offer_details`, `get_deal_changes`, `get_new_offers`
- `get_stack_recommendation` tool for curated free-tier infrastructure stacks
- 4 prompt templates: `find-free-alternative`, `recommend-stack`, `check-pricing-changes`, `search-deals`
- REST API with 8 endpoints
- OpenAPI 3.0 spec for discoverability
- Streamable HTTP and stdio transports
- 1,511 vendor offers across 53 categories
- 52 tracked pricing changes
- Upstash Redis telemetry persistence


path: CONTRIBUTING.md
content: # Contributing to AgentDeals

Thanks for helping expand the index! AgentDeals tracks free tiers, startup programs, and developer deals across 52+ categories.

## How to Submit a Deal

1. Fork the repo and create a branch
2. Add your entry to `data/index.json` (see schema below)
3. Run `npm run validate` to check your entry
4. Open a PR with a brief description of what you added

## Offer Entry Schema

Each entry in `data/index.json` has these **required** fields:

| Field | Type | Description |
|-------|------|-------------|
| `vendor` | string | Company or product name (e.g. "Vercel", "Supabase") |
| `category` | string | One of the [valid categories](#valid-categories) |
| `description` | string | What the free tier includes (min 30 characters). Be specific about limits. |
| `tier` | string | Plan name (e.g. "Free", "Hobby", "OSS", "Startup Program") |
| `url` | string | Link to the vendor's pricing or product page |
| `tags` | string[] | Relevant keywords for search (e.g. `["hosting", "serverless"]`) |
| `verifiedDate` | string | ISO date (YYYY-MM-DD) when you personally verified the deal on the vendor's site |

### Optional fields

| Field | Type | Description |
|-------|------|-------------|
| `eligibility` | object | For conditional deals (startup programs, OSS sponsorships). See example below. |

### Example: Standard free tier entry

```json
{
  "vendor": "Supabase",
  "category": "Databases",
  "description": "Postgres database with 500 MB storage, 2 GB bandwidth, 50K monthly active users, 1 GB file storage, 200 concurrent Realtime connections",
  "tier": "Free",
  "url": "https://supabase.com/pricing",
  "tags": ["database", "postgres", "auth", "storage", "realtime"],
  "verifiedDate": "2026-02-24"
}
```

### Example: Conditional deal (startup program)

```json
{
  "vendor": "AWS",
  "category": "Startup Programs",
  "description": "Activate Founders: $1,000 in credits for self-funded startups. Activate: up to $100K credits for funded startups with approved VC/accelerator backing",
  "tier": "Activate",
  "url": "https://aws.amazon.com/activate/",
  "tags": ["cloud", "iaas", "startup-credits"],
  "verifiedDate": "2026-02-25",
  "eligibility": {
    "type": "accelerator",
    "conditions": ["Self-funded or backed by approved VC/accelerator"],
    "program": "AWS Activate"
  }
}
```

## Deal Changes Entry Schema

If a vendor has changed their free tier (removed it, reduced limits, etc.), add an entry to `data/deal_changes.json`:

```json
{
  "vendor": "PlanetScale",
  "change_type": "free_tier_removed",
  "date": "2024-04-08",
  "summary": "Free Hobby database plan removed entirely. All databases now require paid Scaler plan ($39/mo minimum)",
  "previous_state": "Free Hobby plan: 1 database, 5GB storage, 1B row reads/mo",
  "current_state": "No free tier. Scaler plan starts at $39/month",
  "impact": "high",
  "source_url": "https://planetscale.com/blog/planetscale-forever",
  "category": "Databases",
  "alternatives": ["Neon", "Supabase", "TiDB Cloud"]
}
```

## Valid Categories

AgentDeals uses a fixed set of 52 categories. Your entry's `category` must be one of:

AI / ML, API Development, API Gateway, Analytics, Auth, Background Jobs, Browser Automation, CDN, CI/CD, Cloud Hosting, Cloud IaaS, Code Quality, Communication, Container Registry, DNS & Domain Management, Databases, Design, Dev Utilities, Diagramming, Documentation, Email, Error Tracking, Feature Flags, Forms, Headless CMS, IDE & Code Editors, Infrastructure, Localization, Logging, Low-Code Platforms, Maps/Geolocation, Messaging, Mobile Development, Monitoring, Notebooks & Data Science, Payments, Project Management, Search, Secrets Management, Security, Server Management, Source Control, Startup Perks, Startup Programs, Status Pages, Storage, Team Collaboration, Testing, Tunneling & Networking, Video, Web Scraping, Workflow Automation

## Validation

Run `npm run validate` before submitting your PR. It checks:

- All required fields are present
- No duplicate entries (same vendor + category)
- URLs are valid format
- Descriptions are at least 30 characters
- `verifiedDate` is a valid ISO date (YYYY-MM-DD)
- Category is in the valid set

The script must exit cleanly (exit code 0) for your PR to be accepted.

## Guidelines

- **Verify before adding.** Visit the vendor's pricing page and confirm the free tier exists. Set `verifiedDate` to today's date.
- **Be specific in descriptions.** "Free tier available" is not enough. Include actual limits (storage, requests, users, etc.).
- **Genuine free tiers only.** No time-limited trials (14-day, 30-day). Credits with expiration are OK if clearly noted.
- **One entry per vendor per category.** If a vendor offers both a free tier and a startup program, use different categories (e.g. "Databases" for the free tier, "Startup Programs" for the startup deal).


path: README.md
content: # AgentDeals

[![npm version](https://img.shields.io/npm/v/agentdeals.svg)](https://www.npmjs.com/package/agentdeals)
[![npm downloads](https://img.shields.io/npm/dm/agentdeals.svg)](https://www.npmjs.com/package/agentdeals)

An MCP server that aggregates free tiers, startup credits, and developer tool deals — so your AI agent (or you) can find the best infrastructure offers without leaving the workflow.

AgentDeals indexes real, verified pricing data from 1,500+ developer infrastructure vendors across 54 categories. Available on [npm](https://www.npmjs.com/package/agentdeals) for local use or as a hosted remote server. Connect any MCP-compatible client and search deals by keyword, category, or eligibility.

**Live:** [agentdeals.dev](https://agentdeals.dev)

## Install

### Option A: Claude Code Plugin (one-click)

Install AgentDeals in Claude Code with a single command:

```bash
claude plugin install robhunter/agentdeals
```

This auto-configures the remote MCP server — no local setup needed. All 4 tools and 6 prompt templates are immediately available.

### Option B: Claude Desktop Extension (one-click)

Install AgentDeals directly in Claude Desktop — no configuration needed:

1. Download the latest `agentdeals.mcpb` from [Releases](https://github.com/robhunter/agentdeals/releases)
2. Double-click the file to install in Claude Desktop
3. All 4 tools and 6 prompt templates are immediately available

Or browse for AgentDeals in Claude Desktop under **Settings > Extensions**.

### Option C: npx (local stdio)

No server needed. Runs locally via stdin/stdout:

```json
{
  "mcpServers": {
    "agentdeals": {
      "command": "npx",
      "args": ["-y", "agentdeals"]
    }
  }
}
```

### Option D: Remote HTTP

Connect to the hosted instance — no install required:

```json
{
  "mcpServers": {
    "agentdeals": {
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

### Quick Setup (.mcp.json)

Add AgentDeals to any project by dropping this `.mcp.json` in the project root:

```json
{
  "mcpServers": {
    "agentdeals": {
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

This works with Claude Code, Cursor, and other MCP-compatible clients that read `.mcp.json`.

## Quick Start

### Try these example queries

**Find free database hosting:**
```
Use the search_deals tool:
  query: "database"
  category: "Databases"
```

Returns Neon (0.5 GiB free Postgres), Supabase (500 MB), MongoDB Atlas (512 MB shared cluster), PlanetScale alternatives, and more.

**What pricing changes happened recently?**
```
Use the track_changes tool (no params for weekly digest, or filter):
  since: "2025-01-01"
```

Returns tracked changes like PlanetScale free tier removal, Heroku free dynos sunset, Render pricing restructure, and other shifts.

**Show deals I qualify for as a YC company:**
```
Use the search_deals tool:
  eligibility: "accelerator"
```

Returns AWS Activate, Google Cloud for Startups, Microsoft Founders Hub, Stripe Atlas credits, and 150+ other startup program deals.

## Client Configuration

Each client supports both **local stdio** (via npx) and **remote HTTP**. Stdio is recommended for reliability and speed.

### Claude Desktop

Add to `claude_desktop_config.json`:

**Stdio (recommended):**
```json
{
  "mcpServers": {
    "agentdeals": {
      "command": "npx",
      "args": ["-y", "agentdeals"]
    }
  }
}
```

**Remote HTTP:**
```json
{
  "mcpServers": {
    "agentdeals": {
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

Config location:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

### Cursor

Add to `.cursor/mcp.json` in your project or global config:

**Stdio (recommended):**
```json
{
  "mcpServers": {
    "agentdeals": {
      "command": "npx",
      "args": ["-y", "agentdeals"]
    }
  }
}
```

**Remote HTTP:**
```json
{
  "mcpServers": {
    "agentdeals": {
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

### VS Code (GitHub Copilot)

Add to `.vscode/mcp.json` in your workspace:

**Stdio (recommended):**
```json
{
  "servers": {
    "agentdeals": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "agentdeals"]
    }
  }
}
```

**Remote HTTP:**
```json
{
  "servers": {
    "agentdeals": {
      "type": "http",
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

### Claude Code

Add to `.mcp.json` in your project root:

**Stdio (recommended):**
```json
{
  "mcpServers": {
    "agentdeals": {
      "command": "npx",
      "args": ["-y", "agentdeals"]
    }
  }
}
```

**Remote HTTP:**
```json
{
  "mcpServers": {
    "agentdeals": {
      "type": "url",
      "url": "https://agentdeals.dev/mcp"
    }
  }
}
```

## REST API

AgentDeals also provides a REST API for programmatic access without MCP.

### Search offers

```bash
# Search by keyword
curl "https://agentdeals.dev/api/offers?q=database&limit=5"

# Filter by category
curl "https://agentdeals.dev/api/offers?category=Databases&limit=10"

# Paginate results
curl "https://agentdeals.dev/api/offers?limit=20&offset=40"

# Combine search + category
curl "https://agentdeals.dev/api/offers?q=postgres&category=Databases"
```

Response:
```json
{
  "offers": [
    {
      "vendor": "Neon",
      "category": "Databases",
      "description": "Serverless Postgres with 0.5 GiB storage, 100 CU-hours/month compute on free tier",
      "tier": "Free",
      "url": "https://neon.com/pricing",
      "tags": ["database", "postgres", "serverless"]
    }
  ],
  "total": 142
}
```

### List categories

```bash
curl "https://agentdeals.dev/api/categories"
```

Response:
```json
{
  "categories": [
    { "name": "Cloud Hosting", "count": 45 },
    { "name": "Databases", "count": 38 },
    { "name": "Developer Tools", "count": 414 }
  ]
}
```

### More endpoints

```bash
# Recently added offers
curl "https://agentdeals.dev/api/new?days=7"

# Pricing changes
curl "https://agentdeals.dev/api/changes?since=2025-01-01"

# Vendor details
curl "https://agentdeals.dev/api/details/Supabase?alternatives=true"

# Stack recommendation
curl "https://agentdeals.dev/api/stack?use_case=saas"

# Cost estimation
curl "https://agentdeals.dev/api/costs?services=Vercel,Supabase&scale=startup"

# Compare vendors
curl "https://agentdeals.dev/api/compare?a=Supabase&b=Neon"

# Vendor risk check
curl "https://agentdeals.dev/api/vendor-risk/Heroku"

# Stack audit
curl "https://agentdeals.dev/api/audit-stack?services=Vercel,Supabase,Clerk"

# Server stats
curl "https://agentdeals.dev/api/stats"

# OpenAPI spec
curl "https://agentdeals.dev/api/openapi.json"
```

## Available Tools

| Tool | Description |
|------|-------------|
| `search_deals` | Find free tiers, startup credits, and developer deals for cloud infrastructure, databases, hosting, CI/CD, monitoring, auth, AI services, and more. |
| `plan_stack` | Plan a technology stack with cost-optimized infrastructure choices. Recommends free-tier services, estimates costs, or audits existing stacks. |
| `compare_vendors` | Compare developer tools side by side — free tier limits, pricing tiers, and recent pricing changes. |
| `track_changes` | Track recent pricing changes across developer tools — free tier removals, limit reductions, new free tiers, and expirations. |

### search_deals

**Parameters:**
- `query` (string, optional) — Keyword search (vendor names, descriptions, tags)
- `category` (string, optional) — Filter by category. Pass `"list"` to get all categories with counts.
- `vendor` (string, optional) — Get full details for a specific vendor (fuzzy match). Returns alternatives.
- `eligibility` (string, optional) — Filter: `public`, `accelerator`, `oss`, `student`, `fintech`, `geographic`, `enterprise`
- `sort` (string, optional) — Sort: `vendor` (A-Z), `category`, `newest` (recently verified first)
- `since` (string, optional) — ISO date. Only return deals verified/added after this date.
- `limit` (number, optional) — Max results (default: 20)
- `offset` (number, optional) — Pagination offset

### plan_stack

**Parameters:**
- `mode` (string, required) — `recommend`, `estimate`, or `audit`
- `use_case` (string, optional) — What you're building (for recommend mode)
- `services` (array, optional) — Current vendor names (for estimate/audit mode)
- `scale` (string, optional) — `hobby`, `startup`, `growth` (for estimate mode, default: hobby)
- `requirements` (array, optional) — Specific infra needs (for recommend mode)

### compare_vendors

**Parameters:**
- `vendors` (array, required) — 1 or 2 vendor names. 1 = risk check, 2 = side-by-side comparison.
- `include_risk` (boolean, optional) — Include risk assessment (default: true)

### track_changes

**Parameters:**
- `since` (string, optional) — ISO date. Default: 7 days ago.
- `change_type` (string, optional) — Filter: `free_tier_removed`, `limits_reduced`, `limits_increased`, `new_free_tier`, `pricing_restructured`, etc.
- `vendor` (string, optional) — Filter to one vendor
- `vendors` (string, optional) — Comma-separated vendor names to filter
- `include_expiring` (boolean, optional) — Include upcoming expirations (default: true)
- `lookahead_days` (number, optional) — Days to look ahead for expirations (default: 30)

## Use Cases

### Agent-assisted infrastructure selection

When your AI agent recommends infrastructure, it's usually working from training data — not current pricing. By connecting AgentDeals, the agent can:

1. **Compare free tiers**: "I'm evaluating Supabase vs Neon for a side project" — use `compare_vendors` with both names
2. **Check eligibility**: "We're a YC W24 company, what credits can we get?" — use `search_deals` with `eligibility: "accelerator"`
3. **Verify before recommending**: Use `track_changes` to ensure the free tier hasn't been removed or reduced

### Monitoring deal changes

Track pricing shifts that affect your stack:

1. **Weekly digest**: Call `track_changes` with no params for a curated summary
2. **Filter by vendor**: Call `track_changes` with `vendor: "Vercel"` to see if Vercel's pricing has changed
3. **Filter by type**: Call `track_changes` with `change_type: "free_tier_removed"` to see which vendors have eliminated free tiers

## Categories

AI / ML, AI Coding, API Development, API Gateway, Analytics, Auth, Background Jobs, Browser Automation, CDN, CI/CD, Cloud Hosting, Cloud IaaS, Code Quality, Communication, Container Registry, DNS & Domain Management, Databases, Design, Dev Utilities, Diagramming, Documentation, Email, Error Tracking, Feature Flags, Forms, Headless CMS, IDE & Code Editors, Infrastructure, Localization, Logging, Low-Code Platforms, Maps/Geolocation, Messaging, Mobile Development, Monitoring, Notebooks & Data Science, Payments, Project Management, Search, Secrets Management, Security, Server Management, Source Control, Startup Perks, Startup Programs, Status Pages, Storage, Team Collaboration, Testing, Tunneling & Networking, Video, Web Scraping, Workflow Automation

## Development

```bash
npm install          # Install dependencies
npm run build        # Compile TypeScript
npm test             # Run tests (266 passing)
npm run serve        # Run HTTP server (port 3000)
npm start            # Run stdio server
```

### Local development with stdio

```bash
npm start
```

### Local development with HTTP

```bash
npm run serve
# Server starts at http://localhost:3000
# MCP endpoint: http://localhost:3000/mcp
# Landing page: http://localhost:3000/
```

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | HTTP server port | `3000` |
| `BASE_URL` | Base URL for canonical links, OG tags, sitemaps, and feeds | `https://agentdeals.dev` |
| `GOOGLE_SITE_VERIFICATION` | Google Search Console verification code | _(none)_ |

## Stats

- **1,525** vendor offers across **54** categories
- **57** tracked pricing changes
- **4** MCP tools + **6** prompt templates + **17** REST API endpoints
- **266** passing tests
- Data verified as of 2026-03-14

## Registries

- [MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.robhunter%2Fagentdeals/versions)
- [Glama](https://glama.ai/mcp/connectors/io.github.robhunter/agentdeals)

## License

MIT


path: assets/og-image.svg
content: <svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color:#0f172a"/>
      <stop offset="100%" style="stop-color:#1e293b"/>
    </linearGradient>
    <linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" style="stop-color:#3b82f6"/>
      <stop offset="100%" style="stop-color:#8b5cf6"/>
    </linearGradient>
  </defs>

  <!-- Background -->
  <rect width="1200" height="630" fill="url(#bg)"/>

  <!-- Accent bar at top -->
  <rect width="1200" height="6" fill="url(#accent)"/>

  <!-- Grid pattern (subtle) -->
  <g opacity="0.04">
    <line x1="0" y1="100" x2="1200" y2="100" stroke="white" stroke-width="1"/>
    <line x1="0" y1="200" x2="1200" y2="200" stroke="white" stroke-width="1"/>
    <line x1="0" y1="300" x2="1200" y2="300" stroke="white" stroke-width="1"/>
    <line x1="0" y1="400" x2="1200" y2="400" stroke="white" stroke-width="1"/>
    <line x1="0" y1="500" x2="1200" y2="500" stroke="white" stroke-width="1"/>
    <line x1="200" y1="0" x2="200" y2="630" stroke="white" stroke-width="1"/>
    <line x1="400" y1="0" x2="400" y2="630" stroke="white" stroke-width="1"/>
    <line x1="600" y1="0" x2="600" y2="630" stroke="white" stroke-width="1"/>
    <line x1="800" y1="0" x2="800" y2="630" stroke="white" stroke-width="1"/>
    <line x1="1000" y1="0" x2="1000" y2="630" stroke="white" stroke-width="1"/>
  </g>

  <!-- Deal tag icon -->
  <g transform="translate(80, 160)">
    <path d="M0 30 L30 0 L80 0 L80 50 L50 80 Z" fill="none" stroke="#3b82f6" stroke-width="3" opacity="0.6"/>
    <circle cx="62" cy="18" r="6" fill="#3b82f6" opacity="0.6"/>
  </g>

  <!-- Title -->
  <text x="80" y="300" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="72" font-weight="700" fill="white" letter-spacing="-1">AgentDeals</text>

  <!-- Tagline -->
  <text x="80" y="370" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="30" fill="#94a3b8" font-weight="400">Pricing context for AI agents</text>

  <!-- Stats row -->
  <g transform="translate(80, 440)">
    <rect width="200" height="80" rx="12" fill="white" fill-opacity="0.06"/>
    <text x="100" y="35" font-family="system-ui, sans-serif" font-size="28" fill="#3b82f6" font-weight="700" text-anchor="middle">1,500+</text>
    <text x="100" y="62" font-family="system-ui, sans-serif" font-size="16" fill="#64748b" text-anchor="middle">deals tracked</text>

    <rect x="220" width="200" height="80" rx="12" fill="white" fill-opacity="0.06"/>
    <text x="320" y="35" font-family="system-ui, sans-serif" font-size="28" fill="#8b5cf6" font-weight="700" text-anchor="middle">54</text>
    <text x="320" y="62" font-family="system-ui, sans-serif" font-size="16" fill="#64748b" text-anchor="middle">categories</text>

    <rect x="440" width="200" height="80" rx="12" fill="white" fill-opacity="0.06"/>
    <text x="540" y="35" font-family="system-ui, sans-serif" font-size="28" fill="#06b6d4" font-weight="700" text-anchor="middle">4</text>
    <text x="540" y="62" font-family="system-ui, sans-serif" font-size="16" fill="#64748b" text-anchor="middle">MCP tools</text>
  </g>

  <!-- URL -->
  <text x="80" y="590" font-family="system-ui, sans-serif" font-size="22" fill="#475569">agentdeals.dev</text>

  <!-- Decorative dots on right side -->
  <g opacity="0.08">
    <circle cx="950" cy="150" r="80" fill="#3b82f6"/>
    <circle cx="1050" cy="250" r="60" fill="#8b5cf6"/>
    <circle cx="980" cy="380" r="40" fill="#06b6d4"/>
    <circle cx="1100" cy="450" r="90" fill="#3b82f6"/>
  </g>
</svg>


path: data/category-mapping.json
content: {
  "Windsurf": "IDE & Code Editors",
  "GitHub Copilot": "IDE & Code Editors",
  "Cursor": "IDE & Code Editors",
  "Linear": "Project Management",
  "Notion": "Team Collaboration",
  "JetBrains": "IDE & Code Editors",
  "1Password": "Security",
  "GitHub": "Source Control",
  "Mintlify": "API Development",
  "Retool": "Low-Code Platforms",
  "Atlassian": "Project Management",
  "Lokalise": "Localization",
  "Cal.com": "Team Collaboration",
  "Expo": "Mobile Development",
  "Crowdin": "Localization",
  "Dub.co": "Dev Utilities",
  "Liveblocks": "Team Collaboration",
  "Nango": "API Development",
  "Hoppscotch": "API Development",
  "Gitea": "Source Control",
  "Appsmith": "Low-Code Platforms",
  "GitHub Codespaces": "IDE & Code Editors",
  "Airtable": "Low-Code Platforms",
  "AbstractAPI": "API Development",
  "APILayer": "API Development",
  "BigDataCloud": "Dev Utilities",
  "ipapi": "Dev Utilities",
  "Hunter.io": "Dev Utilities",
  "CurrencyFreaks": "Dev Utilities",
  "MailboxValidator": "Dev Utilities",
  "Screenshotlayer": "Dev Utilities",
  "Stoplight": "API Development",
  "SwaggerHub": "API Development",
  "RapidAPI": "API Development",
  "API Ninjas": "API Development",
  "Apidog": "API Development",
  "Apollo GraphOS": "API Development",
  "Hasura": "API Development",
  "Codeberg": "Source Control",
  "framagit.org": "Source Control",
  "GitGud": "Source Control",
  "heptapod.net": "Source Control",
  "Pagure.io": "Source Control",
  "pijul.com": "Source Control",
  "projectlocker.com": "Source Control",
  "RocketGit": "Source Control",
  "savannah.gnu.org": "Source Control",
  "savannah.nongnu.org": "Source Control",
  "APITemplate.io": "Dev Utilities",
  "APIVerve": "API Development",
  "Arize AI": "AI / ML",
  "Beeceptor": "API Development",
  "Browse AI": "Web Scraping",
  "BrowserCat": "Web Scraping",
  "Calendarific": "Dev Utilities",
  "Canopy": "API Development",
  "CarAPI.dev": "Dev Utilities",
  "Cloudmersive": "Dev Utilities",
  "Colaboratory": "Notebooks & Data Science",
  "Commerce Layer": "Dev Utilities",
  "Composio": "AI / ML",
  "Conversion Tools": "Dev Utilities",
  "Country-State-City Microservice API": "Dev Utilities",
  "Coupler": "Notebooks & Data Science",
  "CraftMyPDF": "Dev Utilities",
  "Cube": "Notebooks & Data Science",
  "CurlHub": "API Development",
  "CurrencyScoop": "Dev Utilities",
  "CustomJS": "Dev Utilities",
  "Data Fetcher": "Low-Code Platforms",
  "Data Miner": "Web Scraping",
  "Dataimporter.io": "Dev Utilities",
  "Datalore": "Notebooks & Data Science",
  "DB Designer": "Dev Utilities",
  "DB-IP": "Dev Utilities",
  "DeepAR": "AI / ML",
  "Deepnote": "Notebooks & Data Science",
  "DiffJSON": "Dev Utilities",
  "Disease.sh": "Dev Utilities",
  "Doczilla": "Dev Utilities",
  "Doppio": "Dev Utilities",
  "drawDB": "Diagramming",
  "DynamicDocs": "Dev Utilities",
  "Export SDK": "Low-Code Platforms",
  "ExtendsClass": "API Development",
  "Financial Data": "Dev Utilities",
  "FormatJSONOnline.com": "Dev Utilities",
  "FraudLabs Pro": "Security",
  "FreeIPAPI": "Dev Utilities",
  "Geolocated.io": "Dev Utilities",
  "Hex": "Notebooks & Data Science",
  "Hook0": "API Development",
  "Insomnia": "API Development",
  "Invantive Cloud": "Dev Utilities",
  "IP Geolocation API by ipwho.org": "Dev Utilities",
  "IP Geolocation": "Dev Utilities",
  "IP.City": "Dev Utilities",
  "IP2Location.io": "Dev Utilities",
  "ipaddress.sh": "Dev Utilities",
  "ipapi.is": "Dev Utilities",
  "ipbase.com": "Dev Utilities",
  "IPLocate": "Dev Utilities",
  "IPTrace": "Dev Utilities",
  "JSON IP": "Dev Utilities",
  "JSON to Table": "Dev Utilities",
  "JSON2Video": "Low-Code Platforms",
  "JSONGrid": "Dev Utilities",
  "JSONing": "API Development",
  "JSONSwiss": "Dev Utilities",
  "KillBait API": "Dev Utilities",
  "Kreya": "API Development",
  "LoginLlama": "Security",
  "Market Data API": "Dev Utilities",
  "Maxim AI": "AI / ML",
  "microlink.io": "Web Scraping",
  "MockAPI": "API Development",
  "Mockerito": "API Development",
  "Mockfly": "API Development",
  "Mocko.dev": "API Development",
  "Multi-Exit IP Address Checker": "Tunneling & Networking",
  "News API": "Dev Utilities",
  "numlookupapi.com": "Dev Utilities",
  "OCR.Space": "AI / ML",
  "OpenAPI3 Designer": "API Development",
  "Parseur": "AI / ML",
  "PDF-API.io": "Dev Utilities",
  "PDFBolt": "API Development",
  "pdfEndpoint.com": "Dev Utilities",
  "Pixela": "Dev Utilities",
  "PrefectCloud": "Notebooks & Data Science",
  "Preset Cloud": "Notebooks & Data Science",
  "ProxySentry": "Dev Utilities",
  "Reducto": "AI / ML",
  "Rendi": "Dev Utilities",
  "RequestBin.com": "API Development",
  "ROBOHASH": "Dev Utilities",
  "Scraper's Proxy": "Web Scraping",
  "ScrapingAnt": "Web Scraping",
  "SerpApi": "Web Scraping",
  "Simplescraper": "Web Scraping",
  "Geekflare API": "Web Scraping",
  "SmartParse": "Dev Utilities",
  "Sofodata": "API Development",
  "Sqlable": "Dev Utilities",
  "Supportivekoala": "Dev Utilities",
  "Tavily AI": "AI / ML",
  "The IP API": "Dev Utilities",
  "TinyMCE": "API Development",
  "Tomorrow.io Weather API": "Dev Utilities",
  "Treblle": "API Development",
  "UniRateAPI": "Dev Utilities",
  "vatcheckapi.com": "Dev Utilities",
  "WeatherXu": "Dev Utilities",
  "WebScraping.AI": "Web Scraping",
  "What The Diff": "Code Quality",
  "wolfram.com": "AI / ML",
  "wrapapi.com": "Web Scraping",
  "Zenscrape": "Web Scraping",
  "Zipcodebase": "Dev Utilities",
  "Zipcodestack": "Dev Utilities",
  "Gemfury": "CI/CD",
  "jitpack.io": "CI/CD",
  "paperspace": "AI / ML",
  "RepoFlow": "CI/CD",
  "RepoForge": "CI/CD",
  "repsy.io": "CI/CD",
  "3Cols": "IDE & Code Editors",
  "BookmarkOS.com": "Team Collaboration",
  "Braid": "Team Collaboration",
  "Calendly": "Team Collaboration",
  "cally.com": "Team Collaboration",
  "cDox": "Team Collaboration",
  "Chanty.com": "Team Collaboration",
  "DevToolLab": "Dev Utilities",
  "Duckly": "Team Collaboration",
  "element.io": "Team Collaboration",
  "evernote.com": "Team Collaboration",
  "Fibery": "Project Management",
  "Fibo": "Project Management",
  "Fizzy": "Project Management",
  "flat.social": "Team Collaboration",
  "flock.com": "Team Collaboration",
  "GitDailies": "Team Collaboration",
  "gitter.im": "Team Collaboration",
  "gokanban.io": "Project Management",
  "Hackmd.io": "Team Collaboration",
  "HeySpace": "Project Management",
  "Huly": "Project Management",
  "Keybase": "Team Collaboration",
  "Linkinize": "Team Collaboration",
  "Lockitbot": "Team Collaboration",
  "meet.jit.si": "Team Collaboration",
  "Miro": "Diagramming",
  "Nuclino": "Team Collaboration",
  "OnlineInterview.io": "IDE & Code Editors",
  "Pendulums": "Project Management",
  "Proton Pass": "Security",
  "Pullflow": "Code Quality",
  "Pumble": "Team Collaboration",
  "Quidlo Timesheets": "Project Management",
  "Raindrop.io": "Team Collaboration",
  "Revolt.chat": "Team Collaboration",
  "Rocket.Chat": "Team Collaboration",
  "ruttl.com": "Communication",
  "Screen Sharing via Browser": "Team Collaboration",
  "seafile.com": "Storage",
  "SiteDots": "Communication",
  "Slab": "Team Collaboration",
  "StatusPile": "Monitoring",
  "Stickies": "Team Collaboration",
  "StreamBackdrops": "Dev Utilities",
  "talky.io": "Team Collaboration",
  "Teamcamp": "Project Management",
  "Teamhood": "Project Management",
  "Teamplify": "Project Management",
  "Telegram": "Communication",
  "Tencent RTC": "Team Collaboration",
  "TimeCamp": "Project Management",
  "tldraw.com": "Diagramming",
  "transfernow": "Storage",
  "Tugboat": "CI/CD",
  "twist.com": "Team Collaboration",
  "userforge.com": "Design",
  "Visual Debug": "Dev Utilities",
  "Webex": "Team Collaboration",
  "Webvizio": "Communication",
  "windmill.dev": "Low-Code Platforms",
  "wistia.com": "Video",
  "wormhol.org": "Storage",
  "Wormhole": "Storage",
  "zoom.us": "Team Collaboration",
  "Zulip": "Team Collaboration",
  "RightFeature": "Project Management",
  "Appinvento": "Low-Code Platforms",
  "DhiWise": "IDE & Code Editors",
  "Karbon Sites": "IDE & Code Editors",
  "Metalama": "IDE & Code Editors",
  "Supermaven": "IDE & Code Editors",
  "v0.dev": "IDE & Code Editors",
  "beanstalkapp.com": "Source Control",
  "codacy.com": "Code Quality",
  "Codeac.io": "Code Quality",
  "CodeFactor": "Code Quality",
  "coderabbit.ai": "Code Quality",
  "CodSpeed": "Code Quality",
  "coveralls.io": "Code Quality",
  "deepscan.io": "Code Quality",
  "DeepSource": "Code Quality",
  "DiffText": "Code Quality",
  "eversql.com": "Code Quality",
  "gerrithub.io": "Source Control",
  "goreportcard.com": "Code Quality",
  "gtmetrix.com": "Code Quality",
  "holistic.dev": "Code Quality",
  "houndci.com": "Code Quality",
  "reviewable.io": "Code Quality",
  "scan.coverity.com": "Code Quality",
  "scrutinizer-ci.com": "Code Quality",
  "semanticdiff.com": "Code Quality",
  "shields.io": "Dev Utilities",
  "CodeKeep": "Dev Utilities",
  "libraries.io": "Dev Utilities",
  "Namae": "Dev Utilities",
  "tickgit.com": "Code Quality",
  "Appho.st": "Mobile Development",
  "Diawi": "Mobile Development",
  "GetUpdraft": "Mobile Development",
  "InstallOnAir": "Mobile Development",
  "Loadly": "Mobile Development",
  "bitnami.com": "Server Management",
  "Esper": "Mobile Development",
  "jamf.com": "Mobile Development",
  "Miradore": "Mobile Development",
  "moss.sh": "Server Management",
  "ploi.io": "Server Management",
  "runcloud.io": "Server Management",
  "serveravatar.com": "Server Management",
  "xcloud.host": "Server Management",
  "AutoLocalise.com": "Localization",
  "Free PO editor": "Localization",
  "Lingo.dev": "Localization",
  "lingohub.com": "Localization",
  "localazy.com": "Localization",
  "Localit": "Localization",
  "localizely.com": "Localization",
  "Loco": "Localization",
  "POEditor": "Localization",
  "SimpleLocalize": "Localization",
  "Texterify": "Localization",
  "Tolgee": "Localization",
  "transifex.com": "Localization",
  "BudiBase": "Low-Code Platforms",
  "Clappia": "Low-Code Platforms",
  "lil'bots": "Low-Code Platforms",
  "manubes": "Low-Code Platforms",
  "Mendix": "Low-Code Platforms",
  "outsystems.com": "Low-Code Platforms",
  "ToolJet": "Low-Code Platforms",
  "UI Bakery": "Low-Code Platforms",
  "btunnel": "Tunneling & Networking",
  "cname.dev": "Tunneling & Networking",
  "conveyor.cloud": "Tunneling & Networking",
  "Expose": "Tunneling & Networking",
  "Hamachi": "Tunneling & Networking",
  "localhost.run": "Tunneling & Networking",
  "localtunnel": "Tunneling & Networking",
  "LocalXpose": "Tunneling & Networking",
  "Pinggy": "Tunneling & Networking",
  "Radmin VPN": "Tunneling & Networking",
  "serveo": "Tunneling & Networking",
  "webhookrelay.com": "Tunneling & Networking",
  "Xirsys": "Tunneling & Networking",
  "ZeroTier": "Tunneling & Networking",
  "acunote.com": "Project Management",
  "asana.com": "Project Management",
  "Backlog": "Project Management",
  "Basecamp": "Project Management",
  "bitrix24.com": "Project Management",
  "cacoo.com": "Diagramming",
  "clickup.com": "Project Management",
  "Clockify": "Project Management",
  "Cloudcraft": "Diagramming",
  "Crosswork": "Project Management",
  "diagrams.net": "Diagramming",
  "easyretro.io": "Project Management",
  "freedcamp.com": "Project Management",
  "GForge": "Project Management",
  "gleek.io": "Diagramming",
  "Helploom": "Communication",
  "Hygger": "Project Management",
  "Ilograph": "Diagramming",
  "kan.bn": "Project Management",
  "kanbanflow.com": "Project Management",
  "kanbantool.com": "Project Management",
  "Kitemaker.co": "Project Management",
  "Kiter.app": "Dev Utilities",
  "Kumu.io": "Diagramming",
  "leiga.com": "Project Management",
  "Lucidchart": "Diagramming",
  "MeisterTask": "Project Management",
  "MeuScrum": "Project Management",
  "nTask": "Project Management",
  "Plane": "Project Management",
  "planitpoker.com": "Project Management",
  "point.poker": "Project Management",
  "Pulse.red": "Project Management",
  "ScrumFast": "Project Management",
  "Sflow": "Project Management",
  "Shake": "Mobile Development",
  "Shortcut": "Project Management",
  "taiga.io": "Project Management",
  "taskade.com": "Project Management",
  "Teaminal": "Project Management",
  "teamwork.com": "Project Management",
  "teleretro.com": "Project Management",
  "Tenzu": "Project Management",
  "titanapps.io": "Project Management",
  "todoist.com": "Project Management",
  "Toggl": "Project Management",
  "trello.com": "Project Management",
  "Tweek": "Project Management",
  "Wikifactory": "Project Management",
  "Yodiz": "Project Management",
  "zenhub.com": "Project Management",
  "zenkit.com": "Project Management",
  "Zube": "Project Management",
  "build.opensuse.org": "CI/CD",
  "copr.fedorainfracloud.org": "CI/CD",
  "Android Studio": "IDE & Code Editors",
  "AndroidIDE": "IDE & Code Editors",
  "Apache Netbeans": "IDE & Code Editors",
  "apiary.io": "API Development",
  "BBEdit": "IDE & Code Editors",
  "Binder": "Notebooks & Data Science",
  "BlueJ": "IDE & Code Editors",
  "Bootify.io": "API Development",
  "Brackets": "IDE & Code Editors",
  "cacher.io": "IDE & Code Editors",
  "cocalc.com": "Notebooks & Data Science",
  "code.cs50.io": "IDE & Code Editors",
  "Code::Blocks": "IDE & Code Editors",
  "codepen.io": "IDE & Code Editors",
  "codesandbox.io": "IDE & Code Editors",
  "codiga.io": "IDE & Code Editors",
  "Components.studio": "IDE & Code Editors",
  "Eclipse Che": "IDE & Code Editors",
  "ForgeCode": "IDE & Code Editors",
  "GetVM": "IDE & Code Editors",
  "JDoodle": "IDE & Code Editors",
  "MarsCode": "IDE & Code Editors",
  "mockaroo": "API Development",
  "Mocklets": "API Development",
  "OneCompiler": "IDE & Code Editors",
  "Paiza": "IDE & Code Editors",
  "PHPSandbox": "IDE & Code Editors",
  "Replit": "IDE & Code Editors",
  "SoloLearn": "IDE & Code Editors",
  "stackblitz.com": "IDE & Code Editors",
  "Sublime Text": "IDE & Code Editors",
  "Visual Studio Code": "IDE & Code Editors",
  "Visual Studio Community": "IDE & Code Editors",
  "VSCodium": "IDE & Code Editors",
  "wakatime.com": "IDE & Code Editors",
  "Wave Terminal": "IDE & Code Editors",
  "WebComponents.dev": "IDE & Code Editors",
  "Zed": "IDE & Code Editors",
  "numverify": "Dev Utilities",
  "veriphone": "Dev Utilities",
  "GraphComment": "Communication",
  "IntenseDebate": "Communication",
  "Remarkbox": "Communication",
  "Utterances": "Communication",
  "FlutLab": "IDE & Code Editors",
  "BackgroundStyler.com": "Dev Utilities",
  "Base64 decoder/encoder": "Dev Utilities",
  "BinShare.net": "Dev Utilities",
  "Blynk": "Dev Utilities",
  "Bricks Note Calculator": "Dev Utilities",
  "Carbon.now.sh": "Dev Utilities",
  "Code Time": "IDE & Code Editors",
  "Codepng": "Dev Utilities",
  "CodeToImage": "Dev Utilities",
  "Cronhooks": "Dev Utilities",
  "datelist.io": "Dev Utilities",
  "Domain Forward": "Dev Utilities",
  "Exif Editor": "Dev Utilities",
  "Format Express": "Dev Utilities",
  "Hook Relay": "Dev Utilities",
  "Hosting Checker": "Dev Utilities",
  "newreleases.io": "Dev Utilities",
  "OnlineExifViewer": "Dev Utilities",
  "PDFMonkey": "Dev Utilities",
  "Pika Code Screenshots": "Dev Utilities",
  "QuickType.io": "API Development",
  "RandomKeygen": "Security",
  "ray.so": "Dev Utilities",
  "redirect.pizza": "Dev Utilities",
  "redirection.io": "Dev Utilities",
  "Renamer.ai": "Dev Utilities",
  "ReqBin": "API Development",
  "Smartcar API": "Dev Utilities",
  "snappify": "Dev Utilities",
  "Sunrise and Sunset": "Dev Utilities",
  "superfeedr.com": "Dev Utilities",
  "SurveyMonkey.com": "Dev Utilities",
  "UUID Generator": "Dev Utilities",
  "Versionfeeds": "Dev Utilities",
  "videoinu": "Dev Utilities",
  "Volume Shader BM": "Dev Utilities",
  "Webacus": "Dev Utilities",
  "XKit": "Dev Utilities",
  "Dadroit V Web": "Dev Utilities",
  "Snap Accelerate": "Startup Perks",
  "Freshworks for Startups": "Startup Perks",
  "Autodesk Fusion 360 for Startups": "Startup Perks",
  "HubSpot for Startups": "Startup Perks",
  "Esri Startup Program": "Startup Perks",
  "MATLAB and Simulink for Startups": "Startup Perks",
  "FeedBear": "Startup Perks",
  "Remote for Startups": "Startup Perks",
  "Pareto Security": "Security",
  "Achieved": "Startup Perks",
  "ActiveCampaign": "Startup Perks",
  "Adjust": "Startup Perks",
  "Aircall": "Startup Perks",
  "Apptimize": "Startup Perks",
  "Attribution App": "Startup Perks",
  "Axonaut": "Startup Perks",
  "Azameo": "Startup Perks",
  "Batch": "Startup Perks",
  "Bench": "Startup Perks",
  "Botnation": "Startup Perks",
  "Branch": "Startup Perks",
  "CaptainData": "Startup Perks",
  "Carta": "Startup Perks",
  "Caviar": "Startup Perks",
  "Chartio": "Startup Perks",
  "Clear Channel Outdoor": "Startup Perks",
  "ClearBrain": "Startup Perks",
  "Commando.io": "Startup Perks",
  "CrazyEgg": "Startup Perks",
  "Crowdfire": "Startup Perks",
  "Cyfe": "Startup Perks",
  "Dareboost": "Startup Perks",
  "Datananas": "Startup Perks",
  "Docsend": "Startup Perks",
  "Drift": "Startup Perks",
  "E-Goi": "Startup Perks",
  "findmassleads": "Startup Perks",
  "Forter": "Startup Perks",
  "Freebe": "Startup Perks",
  "Freshchat": "Startup Perks",
  "Freshdesk": "Startup Perks",
  "Freshmarketer": "Startup Perks",
  "Freshsales": "Startup Perks",
  "Front": "Startup Perks",
  "FullContact": "Startup Perks",
  "Gem": "Startup Perks",
  "GoCardLess": "Startup Perks",
  "GOOGLE ADS": "Startup Perks",
  "Google BigQuery": "Startup Perks",
  "Gorgias": "Startup Perks",
  "Guideline": "Startup Perks",
  "Guideline 401(k)": "Startup Perks",
  "Gusto": "Startup Perks",
  "Harvestr": "Startup Perks",
  "Instacart": "Startup Perks",
  "Intercom": "Startup Perks",
  "InVision": "Startup Perks",
  "Kasa": "Startup Perks",
  "Kaspr": "Startup Perks",
  "Klaviyo": "Startup Perks",
  "Knotel": "Startup Perks",
  "La Fabrique Juridique": "Startup Perks",
  "Legalstart": "Startup Perks",
  "Lester Lee": "Startup Perks",
  "Libeo": "Startup Perks",
  "Looker": "Startup Perks",
  "Mention": "Startup Perks",
  "Mode": "Startup Perks",
  "MoEngage": "Startup Perks",
  "Monday": "Startup Perks",
  "PayFit": "Startup Perks",
  "PersistIQ": "Startup Perks",
  "Phantom Buster": "Startup Perks",
  "Pilot": "Startup Perks",
  "Pipedrive": "Startup Perks",
  "PixelMe": "Startup Perks",
  "Polymail": "Startup Perks",
  "Postscript": "Startup Perks",
  "ProofHub": "Startup Perks",
  "ProspectIn": "Startup Perks",
  "Publist": "Startup Perks",
  "Qonto": "Startup Perks",
  "Quartzy": "Startup Perks",
  "Quickbooks": "Startup Perks",
  "Salesforce": "Startup Perks",
  "Scalingo": "Startup Perks",
  "Science Exchange": "Startup Perks",
  "Scrapingbee": "Startup Perks",
  "Seekwell": "Startup Perks",
  "Seeqle": "Startup Perks",
  "Sellsy": "Startup Perks",
  "Sendinblue": "Startup Perks",
  "Shine": "Startup Perks",
  "Shippo": "Startup Perks",
  "Shopify": "Startup Perks",
  "Slite": "Startup Perks",
  "Snap": "Startup Perks",
  "Snapcall": "Startup Perks",
  "Social Champ": "Startup Perks",
  "SocialBee": "Startup Perks",
  "Solium": "Startup Perks",
  "Split": "Startup Perks",
  "Squarespace": "Startup Perks",
  "Stitch Labs": "Startup Perks",
  "Twilio": "Startup Perks",
  "Userguiding": "Startup Perks",
  "Userlike": "Startup Perks",
  "Vero": "Startup Perks",
  "Vervoe": "Startup Perks",
  "Weglot": "Startup Perks",
  "WEWORK": "Startup Perks",
  "Wisepops": "Startup Perks",
  "Wizishop": "Startup Perks",
  "WP Engine": "Startup Perks",
  "Yousign": "Startup Perks",
  "Zentail": "Startup Perks",
  "Zoom": "Startup Perks",
  "Spotify": "Streaming & Media",
  "YouTube": "Streaming & Media",
  "Tubi": "Streaming & Media",
  "Pluto TV": "Streaming & Media",
  "Peacock": "Streaming & Media",
  "Crunchyroll": "Streaming & Media",
  "Pandora": "Streaming & Media",
  "iHeartRadio": "Streaming & Media",
  "SoundCloud": "Streaming & Media",
  "Plex": "Streaming & Media",
  "Apple Podcasts": "Streaming & Media",
  "Pocket Casts": "Streaming & Media",
  "Todoist": "Productivity & Notes",
  "Trello": "Productivity & Notes",
  "Google Keep": "Productivity & Notes",
  "Obsidian": "Productivity & Notes",
  "Apple Notes": "Productivity & Notes",
  "Microsoft To Do": "Productivity & Notes",
  "TickTick": "Productivity & Notes",
  "Standard Notes": "Productivity & Notes",
  "Simplenote": "Productivity & Notes",
  "Evernote": "Productivity & Notes",
  "Coda": "Productivity & Notes",
  "Google Drive": "Cloud Storage",
  "Google Photos": "Cloud Storage",
  "iCloud": "Cloud Storage",
  "OneDrive": "Cloud Storage",
  "Flickr": "Cloud Storage",
  "Dropbox": "Cloud Storage",
  "MEGA": "Cloud Storage",
  "pCloud": "Cloud Storage",
  "Internxt": "Cloud Storage",
  "Bitwarden": "Password Managers",
  "Apple Passwords": "Password Managers",
  "KeePassXC": "Password Managers",
  "LastPass": "Password Managers",
  "NordPass": "Password Managers",
  "Slack": "Communication & Messaging",
  "Discord": "Communication & Messaging",
  "Google Meet": "Communication & Messaging",
  "Microsoft Teams": "Communication & Messaging",
  "Signal": "Communication & Messaging",
  "WhatsApp": "Communication & Messaging",
  "Canva": "Design & Creative",
  "Figma": "Design & Creative",
  "Photopea": "Design & Creative",
  "GIMP": "Design & Creative",
  "Inkscape": "Design & Creative",
  "DaVinci Resolve": "Design & Creative",
  "Krita": "Design & Creative",
  "Blender": "Design & Creative",
  "Nike Training Club": "Fitness & Health",
  "Samsung Health": "Fitness & Health",
  "MyFitnessPal": "Fitness & Health",
  "Strava": "Fitness & Health",
  "Apple Health": "Fitness & Health",
  "Proton VPN": "VPN & Privacy",
  "Windscribe": "VPN & Privacy",
  "Cloudflare WARP": "VPN & Privacy",
  "Khan Academy": "Education",
  "Duolingo": "Education",
  "MIT OpenCourseWare": "Education",
  "Coursera": "Education",
  "edX": "Education",
  "Gmail": "Consumer Email",
  "Outlook.com": "Consumer Email",
  "Proton Mail": "Consumer Email",
  "Tutanota": "Consumer Email",
  "Pocket": "News & Reading",
  "Instapaper": "News & Reading",
  "Feedly": "News & Reading",
  "Apple News": "News & Reading",
  "Robinhood": "Banking & Finance",
  "Mint (Credit Karma)": "Banking & Finance",
  "Wise": "Banking & Finance",
  "Insight Timer": "Meditation & Wellness",
  "Waking Up": "Meditation & Wellness"
}


path: data/deal_changes.json
content: {
  "changes": [
    {
      "vendor": "Brave Search API",
      "change_type": "free_tier_removed",
      "date": "2026-02-12",
      "summary": "Free plan (5,000 queries/month) replaced with metered billing at $5/1,000 requests. $5 monthly credit provided as offset (~1,000 queries). Credit cards now actively charged with no spending cap",
      "previous_state": "Free plan: 5,000 queries/month, 3 QPS, no credit card required",
      "current_state": "Metered billing at $5/1,000 requests with $5 monthly credit (~1,000 free queries). No free plan. Credit card required, no spending cap",
      "impact": "high",
      "source_url": "https://www.implicator.ai/brave-drops-free-search-api-tier-puts-all-developers-on-metered-billing/",
      "category": "Search",
      "alternatives": [
        "SerpAPI",
        "Google Custom Search"
      ]
    },
    {
      "vendor": "X API (Twitter)",
      "change_type": "free_tier_removed",
      "date": "2026-02-09",
      "summary": "Free tier replaced with pay-per-use + $10 one-time credit",
      "previous_state": "Free basic endpoints (Like, Follow, List, etc.)",
      "current_state": "Pay-per-use pricing, $10 one-time credit for existing users, free plan expired Feb 13",
      "impact": "high",
      "source_url": "https://developer.x.com/en",
      "category": "APIs",
      "alternatives": [
        "Bluesky API",
        "Mastodon API"
      ]
    },
    {
      "vendor": "Spotify API",
      "change_type": "limits_reduced",
      "date": "2026-02-11",
      "summary": "Premium subscription now required for development mode, test users cut from 25 to 5",
      "previous_state": "Free developer access to Web API, 25 test users",
      "current_state": "Spotify Premium subscription required for dev mode, 5 test users max, multiple endpoints deprecated",
      "impact": "high",
      "source_url": "https://developer.spotify.com/",
      "category": "APIs",
      "alternatives": [
        "Last.fm API",
        "MusicBrainz API"
      ]
    },
    {
      "vendor": "Amazon SP-API",
      "change_type": "pricing_restructured",
      "date": "2026-01-31",
      "summary": "Free access ended after 10+ years, now $1,400/year + per-call fees starting April 30",
      "previous_state": "Free API access for 10+ years",
      "current_state": "$1,400/year subscription + per-call fees (effective April 30, 2026)",
      "impact": "high",
      "source_url": "https://developer-docs.amazon.com/sp-api/",
      "category": "APIs",
      "alternatives": []
    },
    {
      "vendor": "PythonAnywhere",
      "change_type": "pricing_restructured",
      "date": "2026-01-15",
      "summary": "$5 Hacker tier eliminated, features merged into free and higher tiers",
      "previous_state": "$5/mo Hacker tier with custom domains and more storage",
      "current_state": "Free tier and $10/mo+ paid tiers only, Hacker tier removed",
      "impact": "medium",
      "source_url": "https://www.pythonanywhere.com/pricing/",
      "category": "Hosting",
      "alternatives": [
        "Render",
        "Railway",
        "Replit"
      ]
    },
    {
      "vendor": "Google Gemini",
      "change_type": "limits_reduced",
      "date": "2025-12-15",
      "summary": "Google quietly slashed Gemini API free tier rate limits by 50-80% in late 2025. Gemini 2.5 Flash went from ~250 requests/day to 20-50. Pro model free tier removed entirely. A Google PM admitted generous limits were only supposed to be available for a single weekend",
      "previous_state": "Pro model available on free tier, Flash ~250 RPD, Flash-Lite generous limits",
      "current_state": "Pro free tier removed. Flash reduced to 10 RPM (~20-50 RPD). Flash-Lite 15 RPM. 50-80% reduction across the board. Gemini 2.0 Flash retiring March 2026",
      "impact": "high",
      "source_url": "https://ai.google.dev/gemini-api/docs/pricing",
      "category": "AI / ML",
      "alternatives": [
        "OpenRouter",
        "Groq",
        "Together AI"
      ]
    },
    {
      "vendor": "Netlify",
      "change_type": "pricing_restructured",
      "date": "2025-09-04",
      "summary": "Restructured to credit-based pricing; sites pause on credit exhaustion",
      "previous_state": "Traditional tiered pricing with fixed monthly allowances",
      "current_state": "300 credits/month free, sites pause on exhaustion",
      "impact": "medium",
      "source_url": "https://www.netlify.com/pricing/",
      "category": "Hosting",
      "alternatives": [
        "Vercel",
        "Cloudflare Pages",
        "GitHub Pages"
      ]
    },
    {
      "vendor": "Netlify",
      "change_type": "restriction",
      "date": "2026-03-01",
      "summary": "Every repo committer is now charged as a full Pro seat ($19/mo) when using Netlify CMS or Identity with a private repo. Previously only users who logged into the Netlify dashboard counted as seats",
      "previous_state": "Only Netlify dashboard users counted as billable seats",
      "current_state": "All repo committers to connected private repos count as Pro seats ($19/mo each) when using CMS or Identity features",
      "impact": "high",
      "source_url": "https://www.netlify.com/pricing/",
      "category": "Hosting",
      "alternatives": [
        "Vercel",
        "Cloudflare Pages",
        "GitHub Pages"
      ]
    },
    {
      "vendor": "Supabase",
      "change_type": "limits_reduced",
      "date": "2026-02-01",
      "summary": "Project pause policy tightened — inactive projects now pause after 1 week",
      "previous_state": "Longer inactivity window before project pause",
      "current_state": "Projects pause after 1 week of inactivity on free tier",
      "impact": "medium",
      "source_url": "https://supabase.com/pricing",
      "category": "Databases",
      "alternatives": [
        "Neon",
        "PlanetScale",
        "CockroachDB"
      ]
    },
    {
      "vendor": "Cloudflare",
      "change_type": "new_free_tier",
      "date": "2026-02-04",
      "summary": "Queues added to Workers free plan — message queuing now free",
      "previous_state": "Queues was paid-only",
      "current_state": "Queues included in free Workers plan",
      "impact": "medium",
      "source_url": "https://developers.cloudflare.com/queues/",
      "category": "Cloud IaaS",
      "alternatives": []
    },
    {
      "vendor": "GitHub Actions",
      "change_type": "pricing_postponed",
      "date": "2026-01-01",
      "summary": "Self-hosted runner per-minute fee ($0.002/min) announced for March 2026 was postponed indefinitely after community backlash. Self-hosted runners remain free. Separately, hosted runner prices were reduced up to 39% (Jan 2026)",
      "previous_state": "Self-hosted runners free. Previous per-minute pricing for hosted runners",
      "current_state": "Self-hosted runners remain free — no orchestration fees. Hosted runners 39% cheaper. GitHub re-evaluating self-hosted pricing approach",
      "impact": "low",
      "source_url": "https://github.com/orgs/community/discussions/182089",
      "category": "CI/CD",
      "alternatives": [
        "GitLab CI",
        "CircleCI",
        "Buildkite"
      ]
    },
    {
      "vendor": "Bright Data MCP",
      "change_type": "new_free_tier",
      "date": "2025-08-14",
      "summary": "Launched free tier for MCP Web Server — 5,000 requests/month for agent developers",
      "previous_state": "Paid-only web scraping API",
      "current_state": "5,000 free monthly requests on Rapid mode, includes search_engine and scrape_as_markdown tools, handles JS rendering and CAPTCHAs",
      "impact": "medium",
      "source_url": "https://brightdata.com/pricing/mcp-server",
      "category": "Web Scraping",
      "alternatives": [
        "Firecrawl",
        "Apify"
      ]
    },
    {
      "vendor": "Stripe",
      "change_type": "pricing_restructured",
      "date": "2026-02-01",
      "summary": "Managed Payments (Merchant of Record) service entering general availability — Stripe handles tax, billing, fraud, and disputes",
      "previous_state": "Payment processing only — developers handled tax compliance, fraud, and disputes separately",
      "current_state": "Managed Payments acts as Merchant of Record in 75+ countries, handling sales tax/VAT/GST, fraud prevention, dispute resolution, and transaction-level support",
      "impact": "high",
      "source_url": "https://docs.stripe.com/payments/managed-payments",
      "category": "Payments",
      "alternatives": [
        "Paddle",
        "Lemon Squeezy"
      ]
    },
    {
      "vendor": "Postman",
      "change_type": "restriction",
      "date": "2026-03-01",
      "summary": "Free plan now single-user only. Team collaboration (shared workspaces, collection sharing) requires paid plan at $19/user/month",
      "previous_state": "Free plan supported up to 3 users in shared workspaces with collection sharing and 25 monitoring calls/month",
      "current_state": "Free plan = single user only with unlimited collections, environments, mock servers, basic monitoring. Teams require Team plan ($19/user/mo) or higher",
      "impact": "high",
      "source_url": "https://www.postman.com/pricing/",
      "category": "Testing",
      "alternatives": [
        "Bruno",
        "Insomnia",
        "Hoppscotch",
        "Apidog",
        "Thunder Client"
      ]
    },
    {
      "vendor": "Atlassian Forge",
      "change_type": "pricing_restructured",
      "date": "2026-01-01",
      "summary": "Forge platform moved from free to consumption-based billing for compute and storage",
      "previous_state": "Entirely free — no charges for compute, storage, or KV operations",
      "current_state": "Free monthly allowances per app (100K GB-sec compute, 0.1 GB KVS, 1 GB logs, 1 hr SQL), usage above thresholds billed",
      "impact": "medium",
      "source_url": "https://www.atlassian.com/blog/developer/updates-to-forge-pricing-effective-january-2026",
      "category": "Developer Tools",
      "alternatives": [
        "AWS Lambda",
        "Self-hosted Connect apps"
      ]
    },
    {
      "vendor": "Fly.io",
      "change_type": "pricing_restructured",
      "date": "2026-01-01",
      "summary": "Volume snapshots now billed — previously free automatic snapshots now cost $0.08/GB-month",
      "previous_state": "Automatic volume snapshots included at no cost, enabled by default",
      "current_state": "$0.08/GB-month for snapshot storage, first 10 GB free. Snapshots can be disabled to avoid charges",
      "impact": "medium",
      "source_url": "https://fly.io/docs/about/pricing/",
      "category": "Cloud Hosting",
      "alternatives": [
        "Render",
        "Railway",
        "Koyeb"
      ]
    },
    {
      "vendor": "Cloudflare Durable Objects",
      "change_type": "pricing_restructured",
      "date": "2026-01-07",
      "summary": "SQLite storage billing began for Durable Objects — previously free during beta",
      "previous_state": "SQLite storage in Durable Objects was free during public beta (compute already billed)",
      "current_state": "Storage charged per GB-hour on Workers Paid plan, first 10 GB included. Free plan users unaffected",
      "impact": "low",
      "source_url": "https://developers.cloudflare.com/durable-objects/platform/pricing/",
      "category": "Cloud IaaS",
      "alternatives": [
        "Upstash Redis",
        "Cloudflare KV",
        "Neon"
      ]
    },
    {
      "vendor": "Sentry",
      "change_type": "pricing_restructured",
      "date": "2025-08-15",
      "summary": "Switched from performance units to spans-based metrics for error and performance monitoring",
      "previous_state": "Performance units pricing model",
      "current_state": "Spans-based metrics: Developer plan includes 50K errors + 10M spans/month free. Reserved volume pricing replaces on-demand",
      "impact": "medium",
      "source_url": "https://sentry.io/pricing/",
      "category": "Monitoring",
      "alternatives": [
        "Datadog",
        "New Relic",
        "BetterStack"
      ]
    },
    {
      "vendor": "Neon",
      "change_type": "pricing_restructured",
      "date": "2026-01-15",
      "summary": "Moved to fully usage-based pricing model post-Databricks acquisition — free tier storage now per-project, projects increased 10→100, branches capped at 10 per project, Neon Auth added",
      "previous_state": "Free tier: 0.5 GB total storage, 191.9 compute hours, 10 projects, unlimited branches",
      "current_state": "Free tier: 0.5 GB per project (up to 5 GB across 100 projects), 100 CU-hours/month per project, 10 branches/project, Neon Auth 60K MAU, auto-scaling up to 2 CU, scale-to-zero",
      "impact": "medium",
      "source_url": "https://neon.com/pricing",
      "category": "Databases",
      "alternatives": [
        "Supabase",
        "CockroachDB",
        "Turso"
      ]
    },
    {
      "vendor": "Vercel",
      "change_type": "pricing_restructured",
      "date": "2026-01-01",
      "summary": "Pro plan moved to credit-based model ($20/mo credit pool), Hobby plan metrics renamed and restructured — bandwidth→Fast Data Transfer, serverless split into Active CPU + Provisioned Memory, Blob Storage and Image Transformations added",
      "previous_state": "Hobby: 100 GB bandwidth, 100 GB-hours serverless. Pro: fixed per-resource allocations",
      "current_state": "Hobby: 100 GB/mo Fast Data Transfer, 4 hrs Active CPU, 360 GB-hrs Provisioned Memory, 1M edge requests, 1 GB Blob Storage, 5K image transformations. Pro: $20/mo credit pool across all resources, SAML SSO and HIPAA included",
      "impact": "medium",
      "source_url": "https://vercel.com/pricing",
      "category": "Cloud Hosting",
      "alternatives": [
        "Netlify",
        "Cloudflare Pages",
        "Render"
      ]
    },
    {
      "vendor": "Atlassian",
      "change_type": "pricing_restructured",
      "date": "2026-02-17",
      "summary": "Data Center pricing increased 15-40% across Jira, Confluence, and JSM. New DC licenses end March 30 — cloud mandatory for new customers",
      "previous_state": "Standard Data Center pricing with Advantaged/legacy discounts",
      "current_state": "15% standard list increase, 18-40% for legacy Advantaged pricing tiers. No new DC licenses after March 30, 2026. Full DC end-of-life March 2029",
      "impact": "high",
      "source_url": "https://www.atlassian.com/licensing/future-pricing",
      "category": "Developer Tools",
      "alternatives": [
        "Linear",
        "Jira Cloud",
        "Shortcut"
      ]
    },
    {
      "vendor": "Docker Hub",
      "change_type": "pricing_restructured",
      "date": "2024-12-10",
      "summary": "Docker Pro +80% ($5→$9/mo), Team +67% ($9→$15/mo). Free tier: Build Cloud minutes removed, Scout repos cut 3→1, private repos limited to 1 with 2GB storage",
      "previous_state": "Pro $5/user/month, Team $9/user/month. Free tier included Build Cloud minutes, 3 Scout repos",
      "current_state": "Pro $9/user/month, Team $15/user/month. Free: 1 private repo (2GB), 1 Scout repo, no Build Cloud minutes, 40 pulls/hour",
      "impact": "high",
      "source_url": "https://www.docker.com/blog/november-2024-updated-plans-announcement/",
      "category": "Container Registry",
      "alternatives": [
        "GitHub Container Registry",
        "Quay.io",
        "Podman"
      ]
    },
    {
      "vendor": "DigitalOcean",
      "change_type": "limits_increased",
      "date": "2025-01-22",
      "summary": "Hatch startup program expanded to up to $100K compute credits plus up to 3 months free H100 GPU access for AI/ML startups",
      "previous_state": "Standard Hatch startup credits program",
      "current_state": "Up to $100K compute credits (12 months, excludes GPU). Separate GPU benefit: discounted H100 at ~$1.90/hr (vs $3.39 standard) plus up to 3 months free GPU access",
      "impact": "medium",
      "source_url": "https://investors.digitalocean.com/news/news-details/2025/DigitalOcean-Announces-New-Benefits-for-Hatch-Startup-Program/default.aspx",
      "category": "Cloud IaaS",
      "alternatives": [
        "AWS Activate",
        "Google Cloud for Startups",
        "Azure for Startups"
      ]
    },
    {
      "vendor": "Google Cloud",
      "change_type": "limits_increased",
      "date": "2026-01-01",
      "summary": "Google for Startups Cloud Program now offers up to $350K GCP credits for AI-first startups, plus $10K Anthropic (Claude) partner credits via Model Garden",
      "previous_state": "Up to $200K GCP credits for standard startups",
      "current_state": "Scale AI Tier: up to $350K GCP credits ($250K year 1, $100K year 2). Standard: $200K. Partner credits: $10K Anthropic/Claude via Model Garden, Fireworks AI, MongoDB Atlas, CockroachDB, 1yr free GitLab Ultimate",
      "impact": "high",
      "source_url": "https://cloud.google.com/startup/ai",
      "category": "Cloud IaaS",
      "alternatives": [
        "AWS Activate",
        "Azure for Startups",
        "DigitalOcean Hatch"
      ]
    },
    {
      "vendor": "AWS",
      "change_type": "pricing_restructured",
      "date": "2026-01-04",
      "summary": "EC2 Capacity Blocks for ML GPU prices increased ~15% across regions. p5e.48xlarge went from $34.61 to $39.80/hr (US East) and $43.26 to $49.75/hr (US West N. California)",
      "previous_state": "p5e.48xlarge at $34.61/hr (US East), $43.26/hr (US West N. California) for Capacity Blocks",
      "current_state": "p5e.48xlarge at $39.80/hr (US East), $49.75/hr (US West N. California). ~15% increase across most GPU Capacity Block instances",
      "impact": "medium",
      "source_url": "https://www.theregister.com/2026/01/05/aws_price_increase/",
      "category": "Cloud IaaS",
      "alternatives": [
        "Google Cloud GPUs",
        "Azure GPU VMs",
        "Lambda Cloud"
      ]
    },
    {
      "vendor": "Google",
      "change_type": "pricing_restructured",
      "date": "2026-01-27",
      "summary": "Google Developer Program Premium merged into Google One AI Pro ($19.99/mo, includes $10 Cloud credits) and AI Ultra ($100 Cloud credits). Developer benefits now bundled into consumer AI subscriptions",
      "previous_state": "Separate Developer Program Premium subscription ($299/year) with dev tools and perks",
      "current_state": "Developer benefits folded into AI Pro ($19.99/mo, $10 Cloud credits for Vertex AI/Cloud Run/Gemini API) and AI Ultra ($100 Cloud credits). Developer Program Premium being phased out for @gmail.com accounts",
      "impact": "medium",
      "source_url": "https://blog.google/innovation-and-ai/technology/developers-tools/gdp-premium-ai-pro-ultra/",
      "category": "Developer Tools",
      "alternatives": [
        "GitHub Copilot",
        "JetBrains AI"
      ]
    },
    {
      "vendor": "OpenAI",
      "change_type": "free_tier_removed",
      "date": "2026-08-26",
      "summary": "Assistants API deprecated, full shutdown August 26, 2026. Developers must migrate to Responses API + Conversations API",
      "previous_state": "Assistants API available with code interpreter, file search, function calling, and Threads for conversation management",
      "current_state": "Deprecated. Full shutdown Aug 26, 2026. Migration required: Assistants → Prompts + Responses API, Threads → Conversations API. No automated migration tool provided",
      "impact": "high",
      "source_url": "https://community.openai.com/t/assistants-api-beta-deprecation-august-26-2026-sunset/1354666",
      "category": "AI/ML",
      "alternatives": [
        "Anthropic Claude API",
        "Google Gemini API",
        "Cohere API"
      ]
    },
    {
      "vendor": "Hetzner",
      "change_type": "pricing_restructured",
      "date": "2026-04-01",
      "summary": "Cloud & dedicated server prices increasing 30-50%. Entry-level CX23: €2.99→€3.99/mo (+33%). Applies to new and existing customers. Driven by DRAM (+171% YoY) and NVMe SSD cost surges from AI infrastructure demand",
      "previous_state": "Lower cloud server pricing across Germany, Finland, US, and Singapore data centers (e.g., CX23 at €2.99/mo)",
      "current_state": "30-37% increase on cloud servers, up to 50% on select dedicated/storage products. All regions and all customers (new and existing) affected. Announced Feb 23, 2026",
      "impact": "high",
      "source_url": "https://www.hetzner.com/pressroom/statement-price-adjustment/",
      "category": "Cloud Hosting",
      "alternatives": [
        "DigitalOcean",
        "Vultr",
        "OVHcloud"
      ]
    },
    {
      "vendor": "Anthropic",
      "change_type": "limits_increased",
      "date": "2026-02-05",
      "summary": "Claude Opus 4.6 API pricing at $5/$25 per MTok (input/output) — 67% below previous Opus 4/4.1 pricing of $15/$75. Frontier AI model at mid-tier prices",
      "previous_state": "Opus 4/4.1 priced at $15/$75 per million tokens (input/output)",
      "current_state": "Opus 4.6 at $5/$25 per MTok. Extended context (>200K tokens): $10/$37.50. Batch API: $2.50/$12.50. Price reduction began with Opus 4.5 generation",
      "impact": "high",
      "source_url": "https://docs.anthropic.com/en/docs/about-claude/models",
      "category": "AI/ML APIs",
      "alternatives": [
        "OpenAI GPT-4o",
        "Google Gemini",
        "Mistral"
      ]
    },
    {
      "vendor": "OpenAI",
      "change_type": "limits_reduced",
      "date": "2026-02-09",
      "summary": "Ads launched in ChatGPT Free and Go ($8/mo) tiers. Sponsored units from major brands appear below responses on first prompt. $60 CPM, $200K minimum ad commitment",
      "previous_state": "Ad-free experience across all ChatGPT tiers including Free",
      "current_state": "Free and Go tiers show contextual ads (labeled 'sponsored') below responses. Plus/Pro/Business/Enterprise/Education tiers remain ad-free. Users can opt out at cost of reduced daily message limits",
      "impact": "high",
      "source_url": "https://openai.com/index/testing-ads-in-chatgpt/",
      "category": "AI/ML",
      "alternatives": [
        "Anthropic Claude",
        "Google Gemini",
        "Perplexity"
      ]
    },
    {
      "vendor": "Google Programmable Search Engine",
      "change_type": "limits_reduced",
      "date": "2026-01-22",
      "summary": "Free full-web search being discontinued. Free tier restricted to max 50 domains per search engine. Migration deadline January 1, 2027",
      "previous_state": "Free search across the entire web with 10,000 queries/day limit via Custom Search JSON API",
      "current_state": "Free tier restricted to searching up to 50 specified domains only. Full-web search requires paid Google Cloud Search or Vertex AI Search. Takes effect January 1, 2027",
      "impact": "high",
      "source_url": "https://developers.google.com/custom-search",
      "category": "Search",
      "alternatives": [
        "Brave Search API",
        "SerpAPI",
        "Bing Web Search API",
        "Meilisearch"
      ]
    },
    {
      "vendor": "odrive",
      "change_type": "free_tier_removed",
      "date": "2026-03-31",
      "summary": "odrive free tier discontinued effective March 31, 2026. Previously offered free unified cloud storage sync across multiple providers (Google Drive, Dropbox, S3, etc.)",
      "previous_state": "Free tier with basic sync across multiple cloud storage providers, file manager interface",
      "current_state": "Free tier removed. Business plans at $20/user/month (min 5 users) or $200/year org subscription. March 31, 2026 deadline",
      "impact": "medium",
      "source_url": "https://www.odrive.com/pricing",
      "category": "Storage",
      "alternatives": [
        "rclone",
        "Cyberduck",
        "MultCloud"
      ]
    },
    {
      "vendor": "MinIO",
      "change_type": "open_source_killed",
      "date": "2026-02-12",
      "summary": "MinIO archived its open-source GitHub repository. All development moved to proprietary MinIO AIStor. No new Docker images, PRs, or contributions accepted. Only case-by-case critical security fixes",
      "previous_state": "Open-source S3-compatible object storage under AGPL-3.0. Active GitHub repo with community contributions, regular Docker image releases",
      "current_state": "GitHub repository archived. Development moved to proprietary AIStor product. No new open-source releases. Critical security fixes only on case-by-case basis",
      "impact": "high",
      "source_url": "https://linuxiac.com/minio-ends-active-development/",
      "category": "Storage",
      "alternatives": [
        "Ceph",
        "SeaweedFS",
        "GarageHQ",
        "Apache Ozone"
      ]
    },
    {
      "vendor": "Microsoft Partner Developer Benefits",
      "change_type": "pricing_restructured",
      "date": "2026-02-13",
      "summary": "Per-user monthly Azure credits eliminated, replaced with organization-level bulk Azure credits. Visual Studio license in all benefit tiers now exclusively VS Enterprise IDE",
      "previous_state": "Per-user monthly Azure credits allocated individually. Mix of Visual Studio Professional and Enterprise licenses across tiers",
      "current_state": "Organization-level bulk Azure credits (shared pool). All tiers include VS Enterprise IDE only. Per-user monthly credits discontinued",
      "impact": "medium",
      "source_url": "https://techcommunity.microsoft.com/discussions/partnerbenefitsforum/new-updates-to-developer-benefits-february-2026/4495536",
      "category": "Cloud IaaS",
      "alternatives": [
        "AWS Partner Network",
        "Google Cloud Partner Advantage"
      ]
    },
    {
      "vendor": "Cloudflare Startup Program",
      "change_type": "startup_program_expanded",
      "date": "2026-02-01",
      "summary": "Startup program revamped to 4 tiers with up to $250,000 in credits (previously lower). Tiers: Bootstrapped $5K, Up-and-Coming $25K, Seed-Funded $100K, High Growth $250K. Credits expire within 1 year",
      "previous_state": "Smaller startup program with fewer tiers and lower credit amounts",
      "current_state": "4 tiers: $5K, $25K, $100K, $250K credits. Covers Workers, R2, Workers AI, Stream. Caps: $10K for R2/Cache Reserve, $50K for Workers AI. Credits expire within 1 year",
      "impact": "high",
      "source_url": "https://blog.cloudflare.com/startup-program-250k-credits/",
      "category": "Cloud IaaS",
      "alternatives": [
        "AWS Activate",
        "Google for Startups Cloud Program",
        "Azure for Startups"
      ]
    },
    {
      "vendor": "Google Gemini 2.0 Flash",
      "change_type": "product_deprecated",
      "date": "2026-03-03",
      "summary": "Gemini 2.0 Flash and 2.0 Flash-Lite models deprecated, scheduled for retirement. Free tier continues with Gemini 2.5 models. Developers on 2.0 must migrate",
      "previous_state": "Gemini 2.0 Flash and Flash-Lite available as free-tier models with standard rate limits",
      "current_state": "2.0 Flash and Flash-Lite deprecated. Developers must migrate to Gemini 2.5 Flash or 2.5 Pro. Free tier preserved for 2.5 series",
      "impact": "medium",
      "source_url": "https://www.isumsoft.com/internet/gemini-2-flash-deprecation-migration-guide.html",
      "category": "AI/ML",
      "alternatives": [
        "Google Gemini 2.5 Flash",
        "Google Gemini 2.5 Pro",
        "OpenRouter",
        "Groq"
      ]
    },
    {
      "vendor": "Logz.io",
      "change_type": "free_tier_removed",
      "date": "2026-03-02",
      "summary": "Free Community plan (1 GB/day, 1-day retention, 10 alerts) removed. Only 14-day free trial remains. Consumption-based pricing starts at $0.92/ingested GB/day",
      "previous_state": "Free Community plan: 1 GB/day ingestion, 1-day retention, 10 alerts, ML-powered anomaly detection",
      "current_state": "14-day free trial only. Pay-as-go: $0.92/GB/day logs, $0.40/1K metrics/day, $0.16/1M spans/day. No permanent free tier",
      "impact": "medium",
      "source_url": "https://logz.io/pricing/",
      "category": "Logging",
      "alternatives": [
        "Grafana Cloud",
        "BetterStack",
        "Axiom",
        "New Relic"
      ]
    },
    {
      "vendor": "X (Twitter)",
      "change_type": "pricing_model_change",
      "date": "2026-02-06",
      "summary": "Pay-per-use pricing model introduced alongside existing fixed tiers. Legacy free tier users migrated to pay-per-use with $10 voucher. Free tier effectively reduced to proof-of-concept only (~500 posts/month read, no search)",
      "previous_state": "Fixed tiers: Free (limited), Basic $100/mo, Pro $5,000/mo, Enterprise $42,000/mo",
      "current_state": "Pay-per-use model added: per-operation billing (read, search, write priced separately). Fixed tiers remain: Free (very limited), Basic $200/mo, Pro $5,000/mo, Enterprise $42,000/mo",
      "impact": "high",
      "source_url": "https://devcommunity.x.com/t/announcing-the-launch-of-x-api-pay-per-use-pricing/256476",
      "category": "API Gateway",
      "alternatives": [
        "Bluesky API",
        "Mastodon API",
        "Reddit API"
      ]
    },
    {
      "vendor": "OpenAI",
      "change_type": "limits_reduced",
      "date": "2025-06-01",
      "summary": "Free trial credits ($5-$18 for new accounts) completely discontinued. Free tier now limited to GPT-3.5 Turbo at 3 RPM with no credits. All advanced models (GPT-4, DALL-E, Whisper) require paid access",
      "previous_state": "$5-$18 in free trial credits for new accounts, 3-month expiry, access to all models",
      "current_state": "No free credits. Free tier: GPT-3.5 Turbo only, 3 requests/minute. Must add payment method for any other model access",
      "impact": "high",
      "source_url": "https://openai.com/api/pricing/",
      "category": "AI / ML",
      "alternatives": [
        "Google Gemini API",
        "Anthropic Claude API",
        "Mistral API",
        "Groq"
      ]
    },
    {
      "vendor": "Xero Developer API",
      "change_type": "pricing_model_change",
      "date": "2026-03-02",
      "summary": "Xero retiring revenue-share model, moving to 5-tier connection+egress pricing. No more free API access for developers. Costs jumping from ~$0 to $17K+/year for some. Also banning API data use for AI/ML model training",
      "previous_state": "Revenue-share model with effective free API access for developers building Xero integrations",
      "current_state": "5-tier usage-based pricing: connection fees + data egress charges. No free tier. Explicit prohibition on using API data for AI/ML training",
      "impact": "high",
      "source_url": "https://developer.xero.com/pricing",
      "category": "API Gateway",
      "alternatives": [
        "QuickBooks API",
        "FreshBooks API",
        "Wave API"
      ]
    },
    {
      "vendor": "SendGrid",
      "change_type": "free_tier_removed",
      "date": "2025-05-27",
      "summary": "Perpetual free tier (100 emails/day) eliminated. Replaced with 60-day trial only. Paid plans start at $19.95/month",
      "previous_state": "Free plan: 100 emails/day forever, no credit card required",
      "current_state": "60-day trial only (100 emails/day, 100 contacts). Paid plans from $19.95/month after trial",
      "impact": "high",
      "source_url": "https://sendgrid.com/en-us/pricing",
      "category": "Email",
      "alternatives": [
        "Resend",
        "Postmark",
        "Mailgun",
        "Amazon SES"
      ]
    },
    {
      "vendor": "Firebase",
      "change_type": "limits_reduced",
      "date": "2026-02-03",
      "summary": "Cloud Storage removed from Spark (free) plan. Projects on Spark with default appspot.com buckets lose console access and API calls return 402/403 errors. Must upgrade to Blaze (pay-as-you-go) to use Cloud Storage",
      "previous_state": "Spark plan included Cloud Storage: 5 GB storage, 1 GB/day download, 20K uploads/day, 50K downloads/day",
      "current_state": "Cloud Storage requires Blaze plan. Blaze provides no-cost quota (1 GiB storage, 10 GB download/month) before charges begin, but credit card required. Other Spark limits unchanged (Firestore, Auth, Functions, Hosting)",
      "impact": "high",
      "source_url": "https://firebase.google.com/docs/storage/faqs-storage-changes-announced-sept-2024",
      "category": "Cloud Storage",
      "alternatives": [
        "Supabase Storage",
        "Cloudflare R2",
        "AWS S3",
        "Backblaze B2"
      ]
    },
    {
      "vendor": "PlanetScale",
      "change_type": "free_tier_removed",
      "date": "2024-04-08",
      "summary": "PlanetScale removed free Hobby plan entirely. All databases on free tier deleted after 30-day grace period. Minimum plan now Scaler at $39/month",
      "previous_state": "Free Hobby plan: 5 GB storage, 1B row reads/month, 10M row writes/month, 1 production branch, 1 development branch",
      "current_state": "No free tier. Scaler plan starts at $39/month (25 GB storage). Enterprise custom pricing. Company pivoted to enterprise focus",
      "impact": "high",
      "source_url": "https://planetscale.com/blog/planetscale-forever",
      "category": "Databases",
      "alternatives": [
        "Neon",
        "Turso",
        "Supabase",
        "CockroachDB"
      ]
    },
    {
      "vendor": "Fauna",
      "change_type": "product_deprecated",
      "date": "2025-05-30",
      "summary": "Fauna shut down entirely on May 30, 2025. The serverless document database ceased all operations. Over 80,000 development teams affected",
      "previous_state": "Free tier: 100K read ops/day, 50K write ops/day, 500K compute ops/day, 5 GB storage. Consumption-based pricing above free limits",
      "current_state": "Service discontinued. No longer available in any form. All customer data deleted",
      "impact": "high",
      "source_url": "https://fauna.com",
      "category": "Databases",
      "alternatives": [
        "Supabase",
        "MongoDB Atlas",
        "CockroachDB",
        "DynamoDB"
      ]
    },
    {
      "vendor": "LocalStack",
      "change_type": "free_tier_removed",
      "date": "2026-03-23",
      "summary": "LocalStack drops open-source Community Edition. Single unified image requires auth token. Free tier replaced with non-commercial-only tier requiring registration. Commercial use requires paid plan starting at $39/month",
      "previous_state": "Open-source Community Edition: 30+ emulated AWS services (S3, Lambda, DynamoDB, etc.), no registration required, Docker image freely available",
      "current_state": "Non-commercial free tier requires registration and auth token. Commercial use starts at $39/month. CI credits system introduced. OSS projects, students, and nonprofits get free access",
      "impact": "high",
      "source_url": "https://blog.localstack.cloud/the-road-ahead-for-localstack/",
      "category": "Testing",
      "alternatives": [
        "Moto (open-source AWS mock)",
        "AWS Free Tier",
        "Testcontainers"
      ]
    },
    {
      "vendor": "Google",
      "change_type": "pricing_restructured",
      "date": "2026-03-30",
      "summary": "Google Developer Program annual subscription ending March 30. Replaced by Google AI Pro ($10/mo) and AI Ultra ($100/mo) tiers with bundled cloud credits and AI model access.",
      "previous_state": "Google Developer Program: annual subscription with standalone cloud credits, developer tools access, and learning resources",
      "current_state": "Google AI Pro ($10/mo) and AI Ultra ($100/mo) tiers bundle cloud credits with AI model access (Gemini, etc). Developer benefits folded into AI-centric subscriptions",
      "impact": "high",
      "source_url": "https://developers.google.com/profile/help/benefits",
      "category": "Cloud IaaS",
      "alternatives": [
        "AWS Activate",
        "Microsoft for Startups",
        "Google for Startups Cloud"
      ],
      "verified_date": "2026-03-04"
    },
    {
      "vendor": "Terragrunt Scale",
      "change_type": "new_free_tier",
      "date": "2025-12-15",
      "summary": "Gruntwork launches free tier for Terragrunt Scale IaC orchestration platform, positioned as HCP Terraform free tier alternative ahead of HashiCorp March 31, 2026 EOL. Includes GitOps, drift detection, and module update automation",
      "previous_state": "No free tier — Team plan at $500/month",
      "current_state": "Free tier with limits matching or exceeding HCP Terraform (500+ managed resources, GitOps, drift detection, Patcher)",
      "impact": "medium",
      "source_url": "https://www.gruntwork.io/blog/announcing-the-terragrunt-scale-free-tier",
      "category": "Infrastructure",
      "alternatives": [
        "Spacelift",
        "env0",
        "Scalr"
      ]
    },
    {
      "vendor": "HCP Terraform",
      "change_type": "pricing_restructured",
      "date": "2026-03-31",
      "summary": "HashiCorp legacy free plan for HCP Terraform ends March 31, 2026. All legacy free plan users auto-migrated to enhanced free tier with different limits: 500 managed resources (previously unlimited for small teams), unlimited users, 1 concurrent run, SSO, and policy as code (Sentinel + OPA)",
      "previous_state": "Legacy free plan with basic Terraform Cloud features",
      "current_state": "Enhanced free tier: 500 managed resources, unlimited users, 1 concurrent run, SSO, policy as code (Sentinel + OPA). Auto-migration from legacy plan",
      "impact": "high",
      "source_url": "https://www.hashicorp.com/products/terraform/pricing",
      "category": "Infrastructure",
      "alternatives": [
        "Spacelift",
        "env0",
        "Scalr",
        "Terragrunt Scale"
      ]
    },
    {
      "vendor": "X (Twitter)",
      "change_type": "free_tier_removed",
      "date": "2026-02-01",
      "summary": "X/Twitter API eliminated free tier in February 2026, replacing it with a pay-per-use credit model. Active free-tier users receive a $0 voucher. 'For-good' utility apps remain free. Basic fixed tier remains at $200/month",
      "previous_state": "Free tier: ~500 posts/month read, 1,500 tweets/month write, no search, ~1 request per 15 minutes",
      "current_state": "No free tier. Pay-per-use credit model replaces free access. $0 voucher for existing free-tier users. For-good utility apps exempted",
      "impact": "high",
      "source_url": "https://developer.x.com/en/portal/products",
      "category": "API Gateway",
      "alternatives": [
        "Bluesky AT Protocol",
        "Mastodon API"
      ]
    },
    {
      "vendor": "Cognition Devin",
      "change_type": "pricing_restructured",
      "date": "2026-01-15",
      "summary": "Launched $20/month Core plan with pay-as-you-go ACU pricing, down from previous $500/month-only Team plan",
      "previous_state": "Team plan only at $500/month (250 credits)",
      "current_state": "Core plan $20/month (pay-as-you-go at $2.25/ACU), Team plan $500/month remains",
      "impact": "high",
      "source_url": "https://devin.ai/pricing",
      "category": "AI Coding",
      "alternatives": [
        "GitHub Copilot",
        "Cline",
        "Aider"
      ]
    },
    {
      "vendor": "Cursor",
      "change_type": "pricing_restructured",
      "date": "2025-06-01",
      "summary": "Moved from flat monthly subscription to credit-based pricing model with new $200/month Ultra tier",
      "previous_state": "Pro plan $20/month with 500 fast requests",
      "current_state": "Pro plan $20/month (credit-based), Business $40/seat/month, Ultra $200/month (higher credit limits)",
      "impact": "medium",
      "source_url": "https://www.cursor.com/pricing",
      "category": "AI Coding",
      "alternatives": [
        "Windsurf",
        "GitHub Copilot"
      ]
    },
    {
      "vendor": "Augment Code",
      "change_type": "pricing_restructured",
      "date": "2025-10-01",
      "summary": "Shifted from per-seat subscription to credit-based consumption model",
      "previous_state": "Per-seat monthly subscription",
      "current_state": "Credit-based consumption model with usage tiers",
      "impact": "medium",
      "source_url": "https://www.augmentcode.com/pricing",
      "category": "AI Coding",
      "alternatives": [
        "GitHub Copilot",
        "Cursor"
      ]
    },
    {
      "vendor": "Google Tenor API",
      "change_type": "product_deprecated",
      "date": "2026-06-30",
      "summary": "Google Tenor public API shutting down June 30, 2026. New API key signups halted January 13, 2026. Only the public GIF search API is affected — Tenor website and app continue operating. Developers must migrate to alternative GIF APIs",
      "previous_state": "Free public API for GIF search, trending GIFs, and autocomplete. No rate limits for reasonable use. Used by messaging apps and social platforms",
      "current_state": "API shutdown June 30, 2026. No new API keys issued since Jan 13, 2026. Existing keys work until shutdown date. No replacement API from Google",
      "impact": "high",
      "source_url": "https://developers.google.com/tenor/guides/api-shutdown",
      "category": "Media",
      "alternatives": [
        "Giphy API",
        "Imgur API"
      ]
    },
    {
      "vendor": "Testcontainers Cloud",
      "change_type": "pricing_restructured",
      "date": "2024-06-01",
      "summary": "Docker acquired AtomicJar (creators of Testcontainers Cloud). Standalone Testcontainers Cloud free tier retired — now bundled into Docker subscriptions. Docker Pro gets 100 min/month, Team 500 min/month, Business 1,500 min/month",
      "previous_state": "Standalone Testcontainers Cloud with independent free tier (100 min/month)",
      "current_state": "Bundled with Docker subscriptions. No standalone free tier. Open-source Testcontainers libraries remain MIT-licensed and fully free",
      "impact": "medium",
      "source_url": "https://testcontainers.com/cloud/",
      "category": "Testing",
      "alternatives": [
        "Testcontainers (open-source)",
        "Docker Desktop",
        "Podman"
      ]
    },
    {
      "vendor": "Docker Desktop",
      "change_type": "pricing_restructured",
      "date": "2026-01-01",
      "summary": "Docker Desktop subscription prices increased 67-80% across all tiers. Personal $7→$11, Pro $9→$14, Team $15→$24, Business $24→$35 per user/month. Separate from Docker Hub registry pricing changes",
      "previous_state": "Personal $7/user/month, Pro $9/user/month, Team $15/user/month, Business $24/user/month",
      "current_state": "Personal $11/user/month (+57%), Pro $14/user/month (+56%), Team $24/user/month (+60%), Business $35/user/month (+46%). All tiers affected",
      "impact": "high",
      "source_url": "https://www.docker.com/pricing/",
      "category": "Container Registry",
      "alternatives": [
        "Podman Desktop",
        "Rancher Desktop",
        "OrbStack"
      ]
    },
    {
      "vendor": "Unity DevOps",
      "change_type": "limits_increased",
      "date": "2026-03-01",
      "summary": "Free tier significantly expanded: storage 5x increase to 25 GB, 100 GB egress added (new), per-seat charges removed. Unity DevOps (formerly Plastic SCM) now offers generous free version control for game developers",
      "previous_state": "5 GB storage, no egress allowance, per-seat pricing for teams",
      "current_state": "25 GB storage (5x increase), 100 GB egress/month (new), per-seat charges removed. Free for individuals and small teams",
      "impact": "medium",
      "source_url": "https://unity.com/products/devops",
      "category": "Version Control",
      "alternatives": [
        "Git LFS",
        "Perforce",
        "GitHub"
      ]
    },
    {
      "vendor": "Anthropic Claude",
      "change_type": "limits_increased",
      "date": "2026-03-13",
      "summary": "Temporary usage promotion: double five-hour usage limits during off-peak hours, March 13-27, 2026. Applies to Claude Pro and Team subscribers",
      "previous_state": "Standard five-hour usage limits for Pro and Team subscribers",
      "current_state": "2x usage limits during off-peak hours (March 13-27, 2026 only). Temporary promotion to showcase increased capacity",
      "impact": "low",
      "source_url": "https://www.anthropic.com/news",
      "category": "AI/ML",
      "alternatives": [
        "OpenAI ChatGPT Plus",
        "Google Gemini Advanced"
      ]
    },
    {
      "vendor": "Heroku",
      "change_type": "free_tier_removed",
      "date": "2022-11-28",
      "summary": "Heroku removed all free dynos, free Heroku Postgres, and free Heroku Data for Redis. The iconic PaaS that defined 'free tier' for a generation eliminated free offerings entirely after Salesforce acquisition",
      "previous_state": "Free dyno: 550 hours/month (1,000 with credit card), free Postgres: 10K rows, free Redis: 25 MB. Dynos slept after 30 min inactivity",
      "current_state": "No free tier. Mini plan starts at $5/month (Eco dynos $5/month for 1,000 shared hours). Postgres starts at $5/month",
      "impact": "high",
      "source_url": "https://blog.heroku.com/next-chapter",
      "category": "Cloud Hosting",
      "alternatives": [
        "Render",
        "Railway",
        "Fly.io",
        "Koyeb"
      ]
    },
    {
      "vendor": "GitHub Copilot",
      "change_type": "new_free_tier",
      "date": "2025-12-18",
      "summary": "GitHub launched Copilot Free tier — AI code completion available to all GitHub users at no cost. 2,000 completions and 50 chat messages per month included",
      "previous_state": "Paid only: Individual $10/month, Business $19/user/month. Free for verified students, teachers, and OSS maintainers only",
      "current_state": "Free tier: 2,000 code completions/month, 50 chat messages/month. Individual $10/month, Business $19/month, Enterprise $39/month remain for higher limits",
      "impact": "high",
      "source_url": "https://github.blog/news-insights/product-news/github-copilot-in-vscode-free/",
      "category": "AI Coding",
      "alternatives": [
        "Cline",
        "Cody",
        "Cursor"
      ]
    },
    {
      "vendor": "Auth0",
      "change_type": "limits_increased",
      "date": "2025-11-01",
      "summary": "Auth0 increased free tier from 7,500 to 25,000 monthly active users. Largest free tier expansion since Okta acquisition. Makes Auth0 competitive with Clerk and Firebase Auth on free tier",
      "previous_state": "Free tier: 7,500 MAU, unlimited social connections, 2 organizations",
      "current_state": "Free tier: 25,000 MAU (+233%), unlimited social connections, 5 organizations. Actions, Branding, MFA included",
      "impact": "high",
      "source_url": "https://auth0.com/pricing",
      "category": "Authentication",
      "alternatives": [
        "Clerk",
        "Firebase Auth",
        "Supabase Auth"
      ]
    },
    {
      "vendor": "Railway",
      "change_type": "limits_increased",
      "date": "2025-10-01",
      "summary": "Railway expanded free tier following $100M Series B. Trial plan now includes $5 in free credits/month, sufficient for small hobby projects. Removed credit card requirement for trial",
      "previous_state": "$5 free trial credit one-time, credit card required for Hobby plan",
      "current_state": "Trial: $5/month in free credits, no credit card needed. Hobby: $5/month + usage. Pro: $20/month per seat",
      "impact": "medium",
      "source_url": "https://railway.com/pricing",
      "category": "Cloud Hosting",
      "alternatives": [
        "Render",
        "Fly.io",
        "Koyeb"
      ]
    },
    {
      "vendor": "Render",
      "change_type": "limits_reduced",
      "date": "2025-09-01",
      "summary": "Free web services now spin down after 15 minutes of inactivity (previously 30 minutes). Cold starts take 30-60 seconds. Free tier remains but with faster sleep times",
      "previous_state": "Free web services slept after 30 minutes of inactivity",
      "current_state": "Free web services sleep after 15 minutes of inactivity. 750 hours/month free compute unchanged. Cold start 30-60 seconds",
      "impact": "low",
      "source_url": "https://render.com/pricing",
      "category": "Cloud Hosting",
      "alternatives": [
        "Railway",
        "Fly.io",
        "Koyeb"
      ]
    },
    {
      "vendor": "Redis",
      "change_type": "pricing_restructured",
      "date": "2024-03-20",
      "summary": "Redis switched from BSD license to dual SSPL/RSALv2 license, effectively ending open-source status. Triggered creation of Valkey fork by Linux Foundation. Redis Cloud pricing unchanged but community trust impacted",
      "previous_state": "BSD 3-Clause open-source license. Community-driven development. Multiple hosted providers (AWS ElastiCache, etc.)",
      "current_state": "Dual SSPL + RSALv2 license. Cloud hosting by third parties restricted. Valkey fork (BSD) created by Linux Foundation with AWS, Google, Oracle backing",
      "impact": "high",
      "source_url": "https://redis.io/blog/redis-adopts-dual-source-available-licensing/",
      "category": "Databases",
      "alternatives": [
        "Valkey",
        "KeyDB",
        "DragonflyDB",
        "Upstash"
      ]
    },
    {
      "vendor": "HashiCorp",
      "change_type": "pricing_restructured",
      "date": "2023-08-10",
      "summary": "HashiCorp switched Terraform, Vault, Consul, Nomad, and other products from MPL 2.0 to BSL 1.1 license. Triggered OpenTofu fork under Linux Foundation. IBM acquired HashiCorp for $6.4B in 2024",
      "previous_state": "Mozilla Public License 2.0 (open-source). Community-driven development with corporate stewardship",
      "current_state": "Business Source License 1.1. Non-production use free, competitive commercial use restricted. OpenTofu fork (MPL 2.0) maintained by Linux Foundation",
      "impact": "high",
      "source_url": "https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license",
      "category": "Infrastructure",
      "alternatives": [
        "OpenTofu",
        "Pulumi",
        "Crossplane"
      ]
    },
    {
      "vendor": "DigitalOcean",
      "change_type": "limits_increased",
      "date": "2026-01-01",
      "summary": "20% price cut on Basic Droplets (cheapest now $4/mo from $5/mo). Per-second billing introduced (minimum 60 seconds or $0.01). Functions free tier increased to 25,000 GiB-seconds/mo",
      "previous_state": "Basic Droplets from $5/mo, hourly billing, Functions 90K GiB-seconds/mo free",
      "current_state": "Basic Droplets from $4/mo (1 vCPU, 512 MB, 10 GB SSD). Per-second billing (min 60s/$0.01). Functions 25,000 GiB-seconds/mo free",
      "impact": "medium",
      "source_url": "https://www.digitalocean.com/pricing/droplets",
      "category": "Cloud IaaS",
      "alternatives": [
        "Hetzner",
        "Vultr",
        "Linode"
      ]
    },
    {
      "vendor": "DigitalOcean",
      "change_type": "restriction",
      "date": "2026-03-21",
      "summary": "Managed databases are not part of the free tier. Pricing starts at $15/month. Previous listing incorrectly included '1 basic cluster' as a free tier benefit",
      "previous_state": "Listed as: Free static sites (up to 3), functions (90K GiB-seconds/mo), and managed databases (1 basic cluster)",
      "current_state": "Free static sites (up to 3) and functions (90K GiB-seconds/mo). Managed databases start at $15/mo, not free",
      "impact": "medium",
      "source_url": "https://www.digitalocean.com/pricing",
      "category": "Cloud IaaS",
      "alternatives": [
        "Neon",
        "PlanetScale",
        "Supabase"
      ]
    },
    {
      "vendor": "Freshping",
      "change_type": "free_tier_removed",
      "date": "2026-03-06",
      "summary": "Freshping shut down entirely. Free accounts disabled March 6, 2026. Paid plans expire at end of term. All data deleted 90 days later (~June 4, 2026). No replacement product offered by Freshworks",
      "previous_state": "Free plan: 50 monitors, 1-minute check intervals, public status pages, multi-location checks, email/Slack alerts",
      "current_state": "Service discontinued. No free or paid plan available",
      "impact": "high",
      "source_url": "https://www.freshworks.com/freshping/",
      "category": "Monitoring",
      "alternatives": [
        "UptimeRobot",
        "BetterStack",
        "Checkly",
        "StatusCake",
        "Pulsetic",
        "Cronitor",
        "Upptime"
      ]
    },
    {
      "vendor": "Firebase",
      "change_type": "product_deprecated",
      "date": "2026-03-19",
      "summary": "Firebase Studio (formerly Project IDX) shut down March 19, 2026. Cloud IDE and AI code generation features discontinued. Existing projects accessible until March 2027 for migration",
      "previous_state": "Firebase Studio: cloud IDE with AI code generation, Gemini integration, multi-platform preview",
      "current_state": "Discontinued. Projects accessible until March 2027 for data export only. Google recommends migrating to other IDEs",
      "impact": "medium",
      "source_url": "https://firebase.google.com/docs/studio",
      "category": "Databases",
      "alternatives": [
        "Supabase",
        "Appwrite Cloud",
        "PocketBase",
        "Nhost",
        "Convex"
      ]
    },
    {
      "vendor": "Firebase",
      "change_type": "restriction",
      "date": "2026-02-01",
      "summary": "Spark plan projects using legacy *.appspot.com Cloud Storage buckets forced to upgrade to Blaze (pay-as-you-go) plan or lose Cloud Storage access. No billing caps on Blaze plan — developers risk unexpected charges",
      "previous_state": "Spark plan: free Cloud Storage with *.appspot.com buckets, no credit card required",
      "current_state": "Legacy bucket projects must upgrade to Blaze (pay-as-you-go). No hard billing caps. New projects use Firebase Storage buckets on Spark plan",
      "impact": "high",
      "source_url": "https://firebase.google.com/docs/storage/web/start",
      "category": "Databases",
      "alternatives": [
        "Supabase",
        "Appwrite Cloud",
        "PocketBase",
        "Nhost",
        "Convex"
      ]
    },
    {
      "vendor": "Dub.co",
      "change_type": "limits_reduced",
      "date": "2026-03-22",
      "summary": "Free tier link limit reduced 40x from 1,000 links to 25 new links/month. Added 30-day analytics retention limit and 1-user cap",
      "previous_state": "1,000 links, 1,000 tracked clicks/month, up to 3 custom domains",
      "current_state": "25 new links/month, 1,000 tracked events/month, 3 custom domains, 1 user, 30-day analytics retention",
      "impact": "high",
      "source_url": "https://dub.co/pricing",
      "category": "Dev Utilities",
      "alternatives": [
        "Bitly",
        "Short.io"
      ]
    },
    {
      "vendor": "Uploadthing",
      "change_type": "free_tier_removed",
      "date": "2025-01-15",
      "summary": "Free tier eliminated. Uploadthing moved to usage-based pricing with minimum $25/month plan (250 GB storage). No free tier available",
      "previous_state": "Free tier: 2 GB storage, 2 GB bandwidth/month",
      "current_state": "Usage-based pricing only. Minimum $25/month plan with 250 GB storage. No free tier",
      "impact": "high",
      "source_url": "https://uploadthing.com/pricing",
      "category": "Storage",
      "alternatives": [
        "Cloudinary",
        "Vercel Blob",
        "AWS S3"
      ]
    },
    {
      "vendor": "Neo4j AuraDB",
      "change_type": "restriction",
      "date": "2026-03-22",
      "summary": "Data correction — previous entry incorrectly reduced limits to 50K nodes/175K relationships. Actual free tier is 200,000 nodes and 400,000 relationships per Neo4j AuraDB FAQ",
      "previous_state": "50,000 nodes, 175,000 relationships (incorrectly listed)",
      "current_state": "200,000 nodes, 400,000 relationships (restored to correct values)",
      "impact": "low",
      "source_url": "https://neo4j.com/cloud/platform/aura-graph-database/faq/",
      "category": "Databases",
      "alternatives": [
        "Dgraph Cloud",
        "Amazon Neptune"
      ]
    },
    {
      "vendor": "xAI (Grok)",
      "change_type": "free_tier_removed",
      "date": "2026-03-19",
      "summary": "Grok Imagine free image/video generation locked behind SuperGrok subscription ($30/mo). Over 1B generations in first month overwhelmed capacity",
      "previous_state": "Free image and video generation for all Grok users",
      "current_state": "Image/video generation requires SuperGrok subscription ($30/month). Text-only Grok remains free with limits",
      "impact": "medium",
      "source_url": "https://x.com/xai",
      "category": "AI / ML",
      "alternatives": [
        "DALL-E (credits)",
        "Stable Diffusion (self-hosted)",
        "Pollinations.AI"
      ]
    },
    {
      "vendor": "Google Gemini API",
      "change_type": "restriction",
      "date": "2026-04-01",
      "summary": "Billing-account-level spend caps enforced starting April 1, 2026. When a tier's spend cap is reached, API requests pause until the next billing month. Affects pay-as-you-go developers who may see unexpected request pausing.",
      "previous_state": "No hard spend caps — usage billed without automatic pausing",
      "current_state": "Tier-level spend caps enforced at billing account level. Requests pause when cap is hit until next month",
      "impact": "medium",
      "source_url": "https://ai.google.dev/gemini-api/docs/billing",
      "category": "AI / ML",
      "alternatives": [
        "OpenRouter",
        "Anthropic Claude API",
        "OpenAI API"
      ]
    },
    {
      "vendor": "Windsurf",
      "change_type": "pricing_restructured",
      "date": "2026-03-19",
      "summary": "Windsurf replaced credits-based pricing with daily/weekly quotas (Mar 19, 2026). New tiers: Pro $20/mo (was $15), Ultimate $40/mo, Team $200/mo/seat. Free tier changed from flexible credits to hard quotas. Existing Pro subscribers grandfathered at $15/mo",
      "previous_state": "Credits-based system allowing flexible allocation between completions and chat. Pro plan $15/month",
      "current_state": "Daily/weekly quotas replace credits. Pro $20/mo (existing subs grandfathered at $15), Ultimate $40/mo, Team $200/mo/seat. Free tier gets limited daily completions and chat messages",
      "impact": "medium",
      "source_url": "https://windsurf.com/pricing",
      "category": "AI Coding",
      "alternatives": [
        "Cursor",
        "GitHub Copilot",
        "Cline"
      ]
    },
    {
      "vendor": "Gemini Code Assist",
      "change_type": "new_free_tier",
      "date": "2025-12-18",
      "summary": "Google launched Gemini Code Assist free tier for individual developers. Includes code completions, chat, and multi-file editing powered by Gemini 2.5 Pro. Available in VS Code, JetBrains, and Cloud Shell",
      "previous_state": "No free tier — enterprise-only at $19/user/month",
      "current_state": "Free individual tier with code completions, chat, and multi-file editing. Enterprise tier remains at $19/user/month",
      "impact": "high",
      "source_url": "https://cloud.google.com/gemini/docs/codeassist/overview",
      "category": "AI Coding",
      "alternatives": [
        "GitHub Copilot Free",
        "Cursor",
        "Cline"
      ]
    },
    {
      "vendor": "Amazon Aurora PostgreSQL",
      "change_type": "new_free_tier",
      "date": "2026-03-25",
      "summary": "Aurora PostgreSQL Serverless added to AWS Free Tier — managed PostgreSQL-compatible database now available at no cost with up to 4 ACUs per cluster and 1 GB storage. Part of AWS Free Plan (6 months + credits)",
      "previous_state": "No free tier — Aurora was paid-only, starting at ~$0.08/ACU-hour",
      "current_state": "Free plan with up to 4 ACUs per cluster, 1 GB storage, $100–200 in credits for new accounts",
      "impact": "high",
      "source_url": "https://aws.amazon.com/rds/aurora/pricing/",
      "category": "Databases",
      "alternatives": [
        "Neon",
        "Supabase",
        "CockroachDB",
        "PlanetScale"
      ]
    }
  ]
}


path: data/watchlist.json
content: {
  "description": "Vendors monitored for pricing page changes. interval: daily (high-churn) or weekly (stable).",
  "vendors": [
    { "vendor": "Vercel", "url": "https://vercel.com/pricing", "interval": "daily" },
    { "vendor": "Render", "url": "https://render.com/pricing", "interval": "daily" },
    { "vendor": "Supabase", "url": "https://supabase.com/pricing", "interval": "daily" },
    { "vendor": "Neon", "url": "https://neon.com/pricing", "interval": "daily" },
    { "vendor": "Railway", "url": "https://railway.com/pricing", "interval": "daily" },
    { "vendor": "Netlify", "url": "https://www.netlify.com/pricing/", "interval": "daily" },
    { "vendor": "Cloudflare Workers", "url": "https://developers.cloudflare.com/workers/platform/pricing/", "interval": "daily" },
    { "vendor": "DigitalOcean", "url": "https://www.digitalocean.com/pricing", "interval": "daily" },
    { "vendor": "MongoDB Atlas", "url": "https://www.mongodb.com/pricing", "interval": "daily" },
    { "vendor": "Firebase", "url": "https://firebase.google.com/pricing", "interval": "daily" },
    { "vendor": "GitHub Actions", "url": "https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions", "interval": "weekly" },
    { "vendor": "Auth0", "url": "https://auth0.com/pricing", "interval": "daily" },
    { "vendor": "Postman", "url": "https://www.postman.com/pricing/", "interval": "weekly" },
    { "vendor": "AWS", "url": "https://aws.amazon.com/free/", "interval": "weekly" },
    { "vendor": "HCP Terraform", "url": "https://www.hashicorp.com/products/terraform/pricing", "interval": "weekly" },
    { "vendor": "Sentry", "url": "https://sentry.io/pricing/", "interval": "daily" },
    { "vendor": "Heroku", "url": "https://www.heroku.com/pricing", "interval": "weekly" },
    { "vendor": "Cloudflare R2", "url": "https://developers.cloudflare.com/r2/pricing/", "interval": "weekly" },
    { "vendor": "Twilio SendGrid", "url": "https://sendgrid.com/en-us/pricing", "interval": "weekly" },
    { "vendor": "Algolia", "url": "https://www.algolia.com/pricing/", "interval": "weekly" }
  ]
}


path: glama.json
content: {
  "$schema": "https://glama.ai/mcp/schemas/server.json",
  "name": "agentdeals",
  "description": "Search and compare free tiers, credits, and pricing changes across 1,500+ developer tools. 4 intent-based MCP tools for infrastructure decisions, cost estimation, and vendor comparison.",
  "repository": "https://github.com/robhunter/agentdeals",
  "license": "MIT",
  "tools": 4,
  "prompts": 6,
  "transport": ["http", "stdio"],
  "runtime": "node",
  "maintainers": ["robhunter"]
}


path: manifest.json
content: {
  "manifest_version": "0.3",
  "name": "agentdeals",
  "display_name": "AgentDeals",
  "version": "0.2.0",
  "description": "Search 1,500+ developer tool deals, compare vendors, check pricing stability, and estimate costs. Free tiers, startup credits, and promotional offers across 54 categories.",
  "long_description": "AgentDeals indexes real, verified pricing data from 1,500+ developer infrastructure vendors across 54 categories. Search deals by keyword, category, or eligibility. Compare vendors side-by-side, check pricing stability and risk, estimate infrastructure costs, get full stack recommendations, and monitor pricing changes — all from within Claude Desktop.\n\nData is fetched from a hosted API (no local data needed). Tools cover the full lifecycle: discovery, comparison, risk assessment, cost estimation, and ongoing monitoring.",
  "author": {
    "name": "Rob Hunter",
    "url": "https://github.com/robhunter"
  },
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/robhunter/agentdeals"
  },
  "homepage": "https://agentdeals.dev",
  "icon": "assets/logo-400.png",
  "keywords": [
    "mcp",
    "developer-tools",
    "deals",
    "free-tier",
    "startup-programs",
    "pricing",
    "infrastructure",
    "cost-estimation"
  ],
  "server": {
    "type": "node",
    "entry_point": "dist/index.js",
    "mcp_config": {
      "command": "node",
      "args": ["${__dirname}/dist/index.js"]
    }
  },
  "compatibility": {
    "platforms": ["darwin", "win32", "linux"],
    "runtimes": {
      "node": ">=20.0.0"
    }
  },
  "tools": [
    {
      "name": "search_deals",
      "description": "Find free tiers, credits, and discounts for 1,500+ developer tools. Search by keyword, browse categories, or get full vendor details with alternatives."
    },
    {
      "name": "plan_stack",
      "description": "Get stack recommendations, cost estimates, or a full infrastructure audit for your project."
    },
    {
      "name": "compare_vendors",
      "description": "Compare 2 vendors side-by-side or check a single vendor's pricing risk. Returns free tier limits, risk levels, and alternatives."
    },
    {
      "name": "track_changes",
      "description": "Track developer tool pricing changes, upcoming expirations, and new deals. Weekly digest with no params."
    }
  ],
  "prompts_generated": true
}


path: package-lock.json
content: {
  "name": "agentdeals",
  "version": "0.3.1",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "agentdeals",
      "version": "0.3.1",
      "license": "MIT",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.29.0",
        "swagger-ui-dist": "^5.32.0",
        "zod": "^3.25.0"
      },
      "bin": {
        "agentdeals": "dist/index.js"
      },
      "devDependencies": {
        "@anthropic-ai/mcpb": "^2.1.2",
        "@types/node": "^22.0.0",
        "ts-node": "^10.9.0",
        "typescript": "^5.7.0"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@anthropic-ai/mcpb": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/@anthropic-ai/mcpb/-/mcpb-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/prompts": "^6.0.1",
        "commander": "^13.1.0",
        "fflate": "^0.8.2",
        "galactus": "^1.0.0",
        "ignore": "^7.0.5",
        "node-forge": "^1.3.2",
        "pretty-bytes": "^5.6.0",
        "zod": "^3.25.67",
        "zod-to-json-schema": "^3.24.6"
      },
      "bin": {
        "mcpb": "dist/cli/cli.js"
      }
    },
    "node_modules/@cspotcode/source-map-support": {
      "version": "0.8.1",
      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/trace-mapping": "0.3.9"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@hono/node-server": {
      "version": "1.19.11",
      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.14.1"
      },
      "peerDependencies": {
        "hono": "^4"
      }
    },
    "node_modules/@inquirer/checkbox": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/figures": "^1.0.6",
        "@inquirer/type": "^2.0.0",
        "ansi-escapes": "^4.3.2",
        "yoctocolors-cjs": "^2.1.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/confirm": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/core": {
      "version": "9.2.1",
      "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/figures": "^1.0.6",
        "@inquirer/type": "^2.0.0",
        "@types/mute-stream": "^0.0.4",
        "@types/node": "^22.5.5",
        "@types/wrap-ansi": "^3.0.0",
        "ansi-escapes": "^4.3.2",
        "cli-width": "^4.1.0",
        "mute-stream": "^1.0.0",
        "signal-exit": "^4.1.0",
        "strip-ansi": "^6.0.1",
        "wrap-ansi": "^6.2.0",
        "yoctocolors-cjs": "^2.1.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/editor": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0",
        "external-editor": "^3.1.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/expand": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0",
        "yoctocolors-cjs": "^2.1.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/figures": {
      "version": "1.0.15",
      "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/input": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/number": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/password": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0",
        "ansi-escapes": "^4.3.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/prompts": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/checkbox": "^3.0.1",
        "@inquirer/confirm": "^4.0.1",
        "@inquirer/editor": "^3.0.1",
        "@inquirer/expand": "^3.0.1",
        "@inquirer/input": "^3.0.1",
        "@inquirer/number": "^2.0.1",
        "@inquirer/password": "^3.0.1",
        "@inquirer/rawlist": "^3.0.1",
        "@inquirer/search": "^2.0.1",
        "@inquirer/select": "^3.0.1"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/rawlist": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/type": "^2.0.0",
        "yoctocolors-cjs": "^2.1.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/search": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/figures": "^1.0.6",
        "@inquirer/type": "^2.0.0",
        "yoctocolors-cjs": "^2.1.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/select": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^9.2.1",
        "@inquirer/figures": "^1.0.6",
        "@inquirer/type": "^2.0.0",
        "ansi-escapes": "^4.3.2",
        "yoctocolors-cjs": "^2.1.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/type": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "mute-stream": "^1.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@jridgewell/resolve-uri": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@jridgewell/trace-mapping": {
      "version": "0.3.9",
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/resolve-uri": "^3.0.3",
        "@jridgewell/sourcemap-codec": "^1.4.10"
      }
    },
    "node_modules/@modelcontextprotocol/sdk": {
      "version": "1.29.0",
      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@hono/node-server": "^1.19.9",
        "ajv": "^8.17.1",
        "ajv-formats": "^3.0.1",
        "content-type": "^1.0.5",
        "cors": "^2.8.5",
        "cross-spawn": "^7.0.5",
        "eventsource": "^3.0.2",
        "eventsource-parser": "^3.0.0",
        "express": "^5.2.1",
        "express-rate-limit": "^8.2.1",
        "hono": "^4.11.4",
        "jose": "^6.1.3",
        "json-schema-typed": "^8.0.2",
        "pkce-challenge": "^5.0.0",
        "raw-body": "^3.0.0",
        "zod": "^3.25 || ^4.0",
        "zod-to-json-schema": "^3.25.1"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@cfworker/json-schema": "^4.1.1",
        "zod": "^3.25 || ^4.0"
      },
      "peerDependenciesMeta": {
        "@cfworker/json-schema": {
          "optional": true
        },
        "zod": {
          "optional": false
        }
      }
    },
    "node_modules/@scarf/scarf": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "Apache-2.0"
    },
    "node_modules/@tsconfig/node10": {
      "version": "1.0.12",
      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@tsconfig/node12": {
      "version": "1.0.11",
      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@tsconfig/node14": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@tsconfig/node16": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/mute-stream": {
      "version": "0.0.4",
      "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/node": {
      "version": "22.19.13",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/@types/wrap-ansi": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/accepts": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "^3.0.0",
        "negotiator": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/acorn": {
      "version": "8.15.0",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/acorn-walk": {
      "version": "8.3.4",
      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "acorn": "^8.11.0"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/ajv": {
      "version": "8.18.0",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.3",
        "fast-uri": "^3.0.1",
        "json-schema-traverse": "^1.0.0",
        "require-from-string": "^2.0.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ajv-formats": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependenciesMeta": {
        "ajv": {
          "optional": true
        }
      }
    },
    "node_modules/ansi-escapes": {
      "version": "4.3.2",
      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "type-fest": "^0.21.3"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/arg": {
      "version": "4.1.3",
      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/body-parser": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "^3.1.2",
        "content-type": "^1.0.5",
        "debug": "^4.4.3",
        "http-errors": "^2.0.0",
        "iconv-lite": "^0.7.0",
        "on-finished": "^2.4.1",
        "qs": "^6.14.1",
        "raw-body": "^3.0.1",
        "type-is": "^2.0.1"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/chardet": {
      "version": "0.7.0",
      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cli-width": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/commander": {
      "version": "13.1.0",
      "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/content-disposition": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6.6.0"
      }
    },
    "node_modules/cors": {
      "version": "2.8.6",
      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "object-assign": "^4",
        "vary": "^1"
      },
      "engines": {
        "node": ">= 0.10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/create-require": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/diff": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.3.1"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/eventsource": {
      "version": "3.0.7",
      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "eventsource-parser": "^3.0.1"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/eventsource-parser": {
      "version": "3.0.6",
      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/express": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "^2.0.0",
        "body-parser": "^2.2.1",
        "content-disposition": "^1.0.0",
        "content-type": "^1.0.5",
        "cookie": "^0.7.1",
        "cookie-signature": "^1.2.1",
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "finalhandler": "^2.1.0",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.0",
        "merge-descriptors": "^2.0.0",
        "mime-types": "^3.0.0",
        "on-finished": "^2.4.1",
        "once": "^1.4.0",
        "parseurl": "^1.3.3",
        "proxy-addr": "^2.0.7",
        "qs": "^6.14.0",
        "range-parser": "^1.2.1",
        "router": "^2.2.0",
        "send": "^1.1.0",
        "serve-static": "^2.2.0",
        "statuses": "^2.0.1",
        "type-is": "^2.0.1",
        "vary": "^1.1.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/express-rate-limit": {
      "version": "8.3.1",
      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ip-address": "10.1.0"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/express-rate-limit"
      },
      "peerDependencies": {
        "express": ">= 4.11"
      }
    },
    "node_modules/external-editor": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "chardet": "^0.7.0",
        "iconv-lite": "^0.4.24",
        "tmp": "^0.0.33"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/external-editor/node_modules/iconv-lite": {
      "version": "0.4.24",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fast-uri": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fastify"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fastify"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/fflate": {
      "version": "0.8.2",
      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/finalhandler": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "on-finished": "^2.4.1",
        "parseurl": "^1.3.3",
        "statuses": "^2.0.1"
      },
      "engines": {
        "node": ">= 18.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/flora-colossus": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "debug": "^4.3.4",
        "fs-extra": "^10.1.0"
      },
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/fs-extra": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "graceful-fs": "^4.2.0",
        "jsonfile": "^6.0.1",
        "universalify": "^2.0.0"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/galactus": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "debug": "^4.3.4",
        "flora-colossus": "^2.0.0",
        "fs-extra": "^10.1.0"
      },
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/graceful-fs": {
      "version": "4.2.11",
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/hono": {
      "version": "4.12.7",
      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.9.0"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/ignore": {
      "version": "7.0.5",
      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 4"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ip-address": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-fullwidth-code-point": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/is-promise": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/jose": {
      "version": "6.1.3",
      "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/panva"
      }
    },
    "node_modules/json-schema-traverse": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/json-schema-typed": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/jsonfile": {
      "version": "6.2.0",
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "universalify": "^2.0.0"
      },
      "optionalDependencies": {
        "graceful-fs": "^4.1.6"
      }
    },
    "node_modules/make-error": {
      "version": "1.3.6",
      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mime-db": {
      "version": "1.54.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "^1.54.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/mute-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
      }
    },
    "node_modules/negotiator": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/node-forge": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "(BSD-3-Clause OR GPL-2.0)",
      "engines": {
        "node": ">= 6.13.0"
      }
    },
    "node_modules/object-assign": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/os-tmpdir": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "8.4.0",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/pkce-challenge": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.20.0"
      }
    },
    "node_modules/pretty-bytes": {
      "version": "5.6.0",
      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.15.0",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.7.0",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/require-from-string": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/router": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "is-promise": "^4.0.0",
        "parseurl": "^1.3.3",
        "path-to-regexp": "^8.0.0"
      },
      "engines": {
        "node": ">= 18"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.3",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.1",
        "mime-types": "^3.0.2",
        "ms": "^2.1.3",
        "on-finished": "^2.4.1",
        "range-parser": "^1.2.1",
        "statuses": "^2.0.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/serve-static": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "parseurl": "^1.3.3",
        "send": "^1.2.0"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/signal-exit": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/swagger-ui-dist": {
      "version": "5.32.1",
      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@scarf/scarf": "=1.4.0"
      }
    },
    "node_modules/tmp": {
      "version": "0.0.33",
      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "os-tmpdir": "~1.0.2"
      },
      "engines": {
        "node": ">=0.6.0"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/ts-node": {
      "version": "10.9.2",
      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@cspotcode/source-map-support": "^0.8.0",
        "@tsconfig/node10": "^1.0.7",
        "@tsconfig/node12": "^1.0.7",
        "@tsconfig/node14": "^1.0.0",
        "@tsconfig/node16": "^1.0.2",
        "acorn": "^8.4.1",
        "acorn-walk": "^8.1.1",
        "arg": "^4.1.0",
        "create-require": "^1.1.0",
        "diff": "^4.0.1",
        "make-error": "^1.1.1",
        "v8-compile-cache-lib": "^3.0.1",
        "yn": "3.1.1"
      },
      "bin": {
        "ts-node": "dist/bin.js",
        "ts-node-cwd": "dist/bin-cwd.js",
        "ts-node-esm": "dist/bin-esm.js",
        "ts-node-script": "dist/bin-script.js",
        "ts-node-transpile-only": "dist/bin-transpile.js",
        "ts-script": "dist/bin-script-deprecated.js"
      },
      "peerDependencies": {
        "@swc/core": ">=1.2.50",
        "@swc/wasm": ">=1.2.50",
        "@types/node": "*",
        "typescript": ">=2.7"
      },
      "peerDependenciesMeta": {
        "@swc/core": {
          "optional": true
        },
        "@swc/wasm": {
          "optional": true
        }
      }
    },
    "node_modules/type-fest": {
      "version": "0.21.3",
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "(MIT OR CC0-1.0)",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/type-is": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "content-type": "^1.0.5",
        "media-typer": "^1.1.0",
        "mime-types": "^3.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/universalify": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 10.0.0"
      }
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/v8-compile-cache-lib": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrap-ansi": {
      "version": "6.2.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/yn": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/yoctocolors-cjs": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/zod": {
      "version": "3.25.76",
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    },
    "node_modules/zod-to-json-schema": {
      "version": "3.25.1",
      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "peerDependencies": {
        "zod": "^3.25 || ^4"
      }
    }
  }
}


path: package.json
content: {
  "name": "agentdeals",
  "version": "0.3.1",
  "description": "MCP server aggregating developer tool deals, free tiers, and startup programs",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "agentdeals": "dist/index.js"
  },
  "keywords": [
    "mcp",
    "developer-tools",
    "deals",
    "free-tier",
    "startup-programs",
    "model-context-protocol"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/robhunter/agentdeals"
  },
  "license": "MIT",
  "author": "Rob Hunter",
  "mcpName": "io.github.robhunter/agentdeals",
  "files": [
    "dist"
  ],
  "scripts": {
    "prepublishOnly": "npm run build && npm test",
    "build": "tsc",
    "start": "node dist/index.js",
    "serve": "node dist/serve.js",
    "dev": "ts-node src/index.ts",
    "test": "node --test --test-concurrency 1 test/**/*.test.ts",
    "lint": "tsc --noEmit",
    "check:staleness": "node scripts/check-staleness.js",
    "check:pricing": "node scripts/check-pricing-changes.js",
    "monitor:pricing": "node scripts/monitor-pricing.js",
    "ingest:free-for-dev": "node scripts/parse-free-for-dev.ts",
    "ingest:startup-deals": "node scripts/ingest-startup-deals.ts",
    "recategorize": "node scripts/recategorize.ts",
    "reverify": "node scripts/reverify.js",
    "validate": "node scripts/validate-data.ts",
    "build:mcpb": "npm run build && npx @anthropic-ai/mcpb pack ."
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "swagger-ui-dist": "^5.32.0",
    "zod": "^3.25.0"
  },
  "devDependencies": {
    "@anthropic-ai/mcpb": "^2.1.2",
    "@types/node": "^22.0.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.7.0"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}


path: scripts/check-pricing-changes.js
content: #!/usr/bin/env node

import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const HASHES_PATH = resolve(__dirname, "..", "data", "pricing-hashes.json");
const INDEX_PATH = resolve(__dirname, "..", "data", "index.json");
const FETCH_TIMEOUT_MS = 15000;

function extractTextContent(html) {
  // Extract visible text content only — far more stable than hashing full HTML.
  // Dynamic elements (nonces, hydration data, build hashes, session tokens)
  // live in tags/attributes, not visible text.
  let cleaned = html;
  // Remove entire head section (meta tags, scripts, styles, link tags)
  cleaned = cleaned.replace(/<head[\s\S]*?<\/head>/gi, "");
  // Remove script and style tags
  cleaned = cleaned.replace(/<script[\s\S]*?<\/script>/gi, "");
  cleaned = cleaned.replace(/<style[\s\S]*?<\/style>/gi, "");
  // Remove SVG content (icon hashes change)
  cleaned = cleaned.replace(/<svg[\s\S]*?<\/svg>/gi, "");
  // Remove all HTML tags, keeping text content
  cleaned = cleaned.replace(/<[^>]+>/g, " ");
  // Decode common HTML entities
  cleaned = cleaned.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ");
  // Remove remaining HTML entities
  cleaned = cleaned.replace(/&#?\w+;/g, " ");
  // Remove hex strings (build hashes, trace IDs) — 8+ hex chars
  cleaned = cleaned.replace(/\b[0-9a-f]{8,}\b/gi, "");
  // Remove UUIDs
  cleaned = cleaned.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "");
  // Remove ISO timestamps and date-time strings
  cleaned = cleaned.replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}[^\s]*/g, "");
  // Remove Unix timestamps (10-13 digits)
  cleaned = cleaned.replace(/\b\d{10,13}\b/g, "");
  // Normalize whitespace
  cleaned = cleaned.replace(/\s+/g, " ").trim();
  return cleaned;
}

function hashContent(content) {
  return createHash("sha256").update(content).digest("hex");
}

async function fetchPage(url) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
  try {
    const res = await fetch(url, {
      signal: controller.signal,
      headers: {
        "User-Agent": "AgentDeals-PricingMonitor/1.0",
        Accept: "text/html",
      },
      redirect: "follow",
    });
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }
    return await res.text();
  } finally {
    clearTimeout(timeout);
  }
}

async function main() {
  let data;
  try {
    data = JSON.parse(readFileSync(INDEX_PATH, "utf-8"));
  } catch (err) {
    console.error(`Failed to read index: ${err.message}`);
    process.exit(2);
  }

  const offers = data.offers || [];
  const missingUrl = offers.filter((o) => !o.url);
  if (missingUrl.length > 0) {
    console.error(`Warning: ${missingUrl.length} entries missing url field:`);
    for (const o of missingUrl) {
      console.error(`  - ${o.vendor}`);
    }
  }

  // Load existing hashes
  let previousHashes = {};
  const isBaseline = !existsSync(HASHES_PATH);
  if (!isBaseline) {
    try {
      previousHashes = JSON.parse(readFileSync(HASHES_PATH, "utf-8"));
    } catch {
      console.error("Warning: Could not parse existing hashes file, treating as baseline run.");
    }
  }

  const currentHashes = {};
  const changed = [];
  const errors = [];

  console.log(`Checking ${offers.length} vendor pricing pages...\n`);

  for (const offer of offers) {
    if (!offer.url) continue;

    process.stdout.write(`  ${offer.vendor}... `);
    try {
      const html = await fetchPage(offer.url);
      const cleaned = extractTextContent(html);
      const hash = hashContent(cleaned);
      currentHashes[offer.vendor] = { url: offer.url, hash, checkedAt: new Date().toISOString() };

      if (!isBaseline && previousHashes[offer.vendor] && previousHashes[offer.vendor].hash !== hash) {
        changed.push({ vendor: offer.vendor, url: offer.url });
        console.log("CHANGED");
      } else {
        console.log("ok");
      }
    } catch (err) {
      const reason = err.name === "AbortError" ? "timeout" : err.message;
      errors.push({ vendor: offer.vendor, url: offer.url, error: reason });
      currentHashes[offer.vendor] = previousHashes[offer.vendor] || { url: offer.url, hash: null, error: reason };
      console.log(`ERROR (${reason})`);
    }
  }

  // Write updated hashes
  writeFileSync(HASHES_PATH, JSON.stringify(currentHashes, null, 2) + "\n");

  console.log("");
  if (isBaseline) {
    console.log(`Baseline created: ${Object.keys(currentHashes).length} vendors hashed.`);
    console.log(`Hashes saved to ${HASHES_PATH}`);
  } else if (changed.length === 0) {
    console.log("No pricing page changes detected.");
  } else {
    console.log(`${changed.length} pricing page(s) changed:\n`);
    for (const c of changed) {
      console.log(`  ${c.vendor} — ${c.url}`);
    }
  }

  if (errors.length > 0) {
    console.log(`\n${errors.length} error(s):\n`);
    for (const e of errors) {
      console.log(`  ${e.vendor} — ${e.error} (${e.url})`);
    }
  }

  process.exit(changed.length > 0 ? 1 : 0);
}

main();


path: scripts/check-staleness.js
content: #!/usr/bin/env node

import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_THRESHOLD_DAYS = 30;

export function findStaleEntries(offers, thresholdDays, now = new Date()) {
  const stale = [];
  for (const offer of offers) {
    if (!offer.verifiedDate) {
      stale.push({ vendor: offer.vendor, category: offer.category, daysSince: Infinity });
      continue;
    }
    const verified = new Date(offer.verifiedDate);
    const diffMs = now.getTime() - verified.getTime();
    const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
    if (diffDays > thresholdDays) {
      stale.push({ vendor: offer.vendor, category: offer.category, daysSince: diffDays });
    }
  }
  return stale.sort((a, b) => b.daysSince - a.daysSince);
}

function main() {
  const thresholdArg = process.argv[2];
  const thresholdDays = thresholdArg ? parseInt(thresholdArg, 10) : DEFAULT_THRESHOLD_DAYS;

  if (isNaN(thresholdDays) || thresholdDays < 0) {
    console.error(`Invalid threshold: ${thresholdArg}. Must be a non-negative integer.`);
    process.exit(2);
  }

  const indexPath = resolve(__dirname, "..", "data", "index.json");
  let data;
  try {
    data = JSON.parse(readFileSync(indexPath, "utf-8"));
  } catch (err) {
    console.error(`Failed to read index: ${err.message}`);
    process.exit(2);
  }

  const offers = data.offers || [];
  const stale = findStaleEntries(offers, thresholdDays);

  if (stale.length === 0) {
    console.log(`All ${offers.length} entries verified within ${thresholdDays} days.`);
    process.exit(0);
  }

  console.log(`Found ${stale.length} stale entries (threshold: ${thresholdDays} days):\n`);
  for (const entry of stale) {
    const days = entry.daysSince === Infinity ? "never verified" : `${entry.daysSince} days ago`;
    console.log(`  ${entry.vendor} (${entry.category}) — ${days}`);
  }

  process.exit(1);
}

// Only run main when executed directly (not imported)
const isMainModule = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
if (isMainModule) {
  main();
}


path: scripts/ingest-startup-deals.ts
content: #!/usr/bin/env node

import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const INDEX_PATH = resolve(ROOT, "data/index.json");
const CACHE_DIR = resolve(ROOT, ".cache");
const AWESOME_DIR = resolve(CACHE_DIR, "awesome-startup-credits");
const STARTUPDEALS_DIR = resolve(CACHE_DIR, "startupdeals");
const OUTPUT_PATH = resolve(ROOT, "data/startup-deals-new.json");

// Map awesome-startup-credits categories to our categories
const AWESOME_CATEGORY_MAP: Record<string, string> = {
  Advertising: "Developer Tools",
  Analytics: "Analytics",
  "Bug Tracking": "Error Tracking",
  "Business Suites": "Developer Tools",
  "Cloud Computing": "Cloud IaaS",
  "Cloud Database": "Databases",
  "Cloud Telephony": "Communication",
  "Customer Engagement": "Communication",
  "Edge Computing": "Cloud Hosting",
  "Email Delivery": "Email",
  Hardware: "Developer Tools",
  "Incident Management": "Monitoring",
  Payments: "Payments",
  "Marketing and Sales": "Developer Tools",
  Video: "Video",
  Miscellaneous: "Developer Tools",
};

// Tags for each mapped category
const CATEGORY_TAGS: Record<string, string[]> = {
  "Developer Tools": ["developer-tools"],
  Analytics: ["analytics", "statistics"],
  "Error Tracking": ["error-tracking", "bug-tracking"],
  "Cloud IaaS": ["cloud", "iaas"],
  Databases: ["database", "data"],
  Communication: ["communication"],
  "Cloud Hosting": ["hosting", "paas"],
  Email: ["email"],
  Monitoring: ["monitoring", "incident-management"],
  Payments: ["payments", "billing"],
  Video: ["video"],
};

interface ParsedEntry {
  vendor: string;
  url: string;
  description: string;
  source: string;
  mappedCategory: string;
  eligibilityType: string;
  conditions: string[];
  program: string;
}

function cloneOrUpdate(repoUrl: string, targetDir: string): void {
  if (existsSync(targetDir)) {
    console.log(`  Updating ${targetDir}...`);
    execSync("git pull", { cwd: targetDir, stdio: "pipe" });
  } else {
    console.log(`  Cloning ${repoUrl}...`);
    execSync(`git clone --depth 1 ${repoUrl} ${targetDir}`, { stdio: "pipe" });
  }
}

function parseAwesomeStartupCredits(): ParsedEntry[] {
  // Try README.md (uppercase)
  let readmePath = resolve(AWESOME_DIR, "README.md");
  if (!existsSync(readmePath)) {
    readmePath = resolve(AWESOME_DIR, "readme.md");
  }
  const content = readFileSync(readmePath, "utf8");
  const lines = content.split("\n");
  const entries: ParsedEntry[] = [];
  let currentCategory = "";
  let inDiscontinued = false;

  // Pattern: - [Name](url) - Description
  const entryPattern = /^\s*-\s+\[([^\]]+)\]\(([^)]+)\)\s*[\u2014\u2013\-]+\s*(.+)/;

  for (const line of lines) {
    // Detect category headers (h3)
    const h3Match = line.match(/^###\s+(.+)/);
    if (h3Match) {
      currentCategory = h3Match[1].trim();
      continue;
    }

    // Detect discontinued section
    const h2Match = line.match(/^##\s+(.+)/);
    if (h2Match) {
      if (h2Match[1].trim().toLowerCase().includes("discontinued")) {
        inDiscontinued = true;
      }
      continue;
    }

    // Skip discontinued entries
    if (inDiscontinued) continue;

    // Skip unmapped categories
    if (!AWESOME_CATEGORY_MAP[currentCategory]) continue;

    const entryMatch = line.match(entryPattern);
    if (entryMatch) {
      const [, vendor, url, description] = entryMatch;

      // Extract eligibility from description
      const conditions: string[] = [];
      const descLower = description.toLowerCase();
      let eligType = "startup";

      if (
        descLower.includes("y combinator") ||
        descLower.includes("accelerator") ||
        descLower.includes("incubator")
      ) {
        eligType = "accelerator";
        conditions.push("Accelerator/incubator membership required");
      }
      if (descLower.includes("raised less than") || descLower.includes("funding")) {
        const fundingMatch = description.match(/raised?\s+(?:less than\s+)?\$[\d,.]+[MKB]?/i);
        if (fundingMatch) conditions.push(fundingMatch[0]);
      }
      if (descLower.includes("employee") || descLower.includes("team size")) {
        const empMatch = description.match(/(?:under|less than|up to)\s+\d+\s+employees?/i);
        if (empMatch) conditions.push(empMatch[0]);
      }
      if (descLower.includes("incorporated") || descLower.includes("launched")) {
        const ageMatch = description.match(
          /(?:incorporated|launched|founded)\s+(?:less than\s+)?\d+\s+years?\s+ago/i
        );
        if (ageMatch) conditions.push(ageMatch[0]);
      }

      if (conditions.length === 0) {
        conditions.push("Startup program — check vendor for eligibility details");
      }

      entries.push({
        vendor: vendor.trim(),
        url: url.trim(),
        description: description.trim().replace(/\s+/g, " "),
        source: "awesome-startup-credits",
        mappedCategory: AWESOME_CATEGORY_MAP[currentCategory],
        eligibilityType: eligType,
        conditions,
        program: `${vendor.trim()} Startup Program`,
      });
    }
  }

  return entries;
}

function parseStartupDeals(): ParsedEntry[] {
  // Try readme.md (lowercase)
  let readmePath = resolve(STARTUPDEALS_DIR, "readme.md");
  if (!existsSync(readmePath)) {
    readmePath = resolve(STARTUPDEALS_DIR, "README.md");
  }
  const content = readFileSync(readmePath, "utf8");
  const lines = content.split("\n");
  const entries: ParsedEntry[] = [];

  // Pattern: | [Company](url) | Deal description | Conditions |
  const tableRowPattern = /^\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/;

  for (const line of lines) {
    // Skip header and separator rows
    if (line.includes("Company") && line.includes("Deal") && line.includes("Conditions")) continue;
    if (/^\|[\s\-:]+\|/.test(line)) continue;

    const match = line.match(tableRowPattern);
    if (match) {
      const [, vendor, sourceUrl, deal, conditions] = match;

      // Skip entries that are paid-only (no free component)
      const condLower = conditions.toLowerCase();
      if (condLower.includes("$") && !condLower.includes("free")) {
        // Has a cost but no free mention — skip
        continue;
      }

      // The URL in startupdeals points to the deal source, not the vendor
      // Try to extract vendor URL from the vendor name
      const vendorClean = vendor.trim();
      const description = `${deal.trim()}. Access via: ${conditions.trim()}`;

      entries.push({
        vendor: vendorClean,
        url: sourceUrl.trim(),
        description: description.replace(/\s+/g, " "),
        source: "startupdeals",
        mappedCategory: guessCategory(deal, vendorClean),
        eligibilityType: "startup",
        conditions: [conditions.trim()],
        program: `${vendorClean} Startup Deal`,
      });
    }
  }

  return entries;
}

function normalizeVendorName(name: string): string {
  return name
    .toLowerCase()
    .replace(/\s+for\s+startups?$/i, "")
    .replace(/\s+startup\s+program$/i, "")
    .replace(/\s+(?:free|credits?|deal)$/i, "")
    .replace(/\.(com|io|dev|sh|ai|co|org|net|app|cloud|so)$/i, "")
    .replace(/[^a-z0-9]/g, "")
    .trim();
}

function extractDomain(url: string): string {
  try {
    const hostname = new URL(url).hostname;
    return hostname.replace(/^www\./, "").toLowerCase();
  } catch {
    return "";
  }
}

function guessCategory(description: string, vendor: string): string {
  const text = `${vendor} ${description}`.toLowerCase();

  if (text.includes("cloud") || text.includes("aws") || text.includes("azure") || text.includes("gcp"))
    return "Cloud IaaS";
  if (text.includes("database") || text.includes("db") || text.includes("sql")) return "Databases";
  if (text.includes("email") || text.includes("sendgrid") || text.includes("mailgun")) return "Email";
  if (text.includes("analytics") || text.includes("mixpanel") || text.includes("amplitude"))
    return "Analytics";
  if (text.includes("hosting") || text.includes("heroku") || text.includes("deploy"))
    return "Cloud Hosting";
  if (text.includes("monitoring") || text.includes("apm") || text.includes("observ")) return "Monitoring";
  if (text.includes("payment") || text.includes("stripe") || text.includes("billing")) return "Payments";
  if (text.includes("ci/cd") || text.includes("pipeline") || text.includes("build")) return "CI/CD";
  if (text.includes("auth") || text.includes("identity") || text.includes("sso")) return "Auth";
  if (text.includes("security") || text.includes("vuln")) return "Security";
  if (text.includes("crm") || text.includes("customer") || text.includes("engagement"))
    return "Communication";
  if (text.includes("design") || text.includes("figma") || text.includes("sketch")) return "Design";
  if (text.includes("search") || text.includes("algolia")) return "Search";
  if (text.includes("storage") || text.includes("cdn") || text.includes("s3")) return "Storage";
  if (text.includes("log") || text.includes("logging")) return "Logging";

  return "Developer Tools";
}

function deduplicate(
  parsed: ParsedEntry[],
  existingOffers: any[]
): { newEntries: ParsedEntry[]; duplicates: string[] } {
  const existingNames = new Set(existingOffers.map((o: any) => normalizeVendorName(o.vendor)));
  const existingDomains = new Set(
    existingOffers.map((o: any) => extractDomain(o.url)).filter(Boolean)
  );

  const seenNames = new Set<string>();
  const newEntries: ParsedEntry[] = [];
  const duplicates: string[] = [];

  for (const entry of parsed) {
    const normalized = normalizeVendorName(entry.vendor);

    if (existingNames.has(normalized) || seenNames.has(normalized)) {
      duplicates.push(entry.vendor);
      continue;
    }

    // For startupdeals, URLs point to deal sources not vendor sites — skip domain check
    if (entry.source !== "startupdeals") {
      const domain = extractDomain(entry.url);
      if (domain && existingDomains.has(domain)) {
        duplicates.push(entry.vendor);
        continue;
      }
    }

    seenNames.add(normalized);
    newEntries.push(entry);
  }

  return { newEntries, duplicates };
}

function toOfferFormat(entry: ParsedEntry, today: string) {
  const category = entry.mappedCategory;
  const baseTags = CATEGORY_TAGS[category] || ["developer-tools"];
  const tags = [...baseTags, "startup-credits", entry.source];

  return {
    vendor: entry.vendor,
    category,
    description: entry.description,
    tier: "Startup Program",
    url: entry.url,
    tags,
    verifiedDate: today,
    eligibility: {
      type: entry.eligibilityType,
      conditions: entry.conditions,
      program: entry.program,
    },
  };
}

async function checkUrls(entries: ParsedEntry[]): Promise<{ live: ParsedEntry[]; dead: string[] }> {
  const TIMEOUT_MS = 10000;
  const CONCURRENCY = 20;
  const live: ParsedEntry[] = [];
  const dead: string[] = [];

  for (let i = 0; i < entries.length; i += CONCURRENCY) {
    const batch = entries.slice(i, i + CONCURRENCY);
    const results = await Promise.allSettled(
      batch.map(async (entry) => {
        try {
          const controller = new AbortController();
          const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
          const resp = await fetch(entry.url, {
            method: "HEAD",
            signal: controller.signal,
            redirect: "follow",
            headers: { "User-Agent": "AgentDeals/1.0 (https://github.com/robhunter/agentdeals)" },
          });
          clearTimeout(timeout);
          return { entry, ok: resp.ok || resp.status === 403 || resp.status === 405 };
        } catch {
          try {
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
            const resp = await fetch(entry.url, {
              method: "GET",
              signal: controller.signal,
              redirect: "follow",
              headers: { "User-Agent": "AgentDeals/1.0 (https://github.com/robhunter/agentdeals)" },
            });
            clearTimeout(timeout);
            return { entry, ok: resp.ok || resp.status === 403 };
          } catch {
            return { entry, ok: false };
          }
        }
      })
    );

    for (const result of results) {
      if (result.status === "fulfilled" && result.value.ok) {
        live.push(result.value.entry);
      } else if (result.status === "fulfilled") {
        dead.push(result.value.entry.vendor + " (" + result.value.entry.url + ")");
      } else {
        dead.push("unknown");
      }
    }

    if (i + CONCURRENCY < entries.length) {
      await new Promise((r) => setTimeout(r, 500));
    }
  }

  return { live, dead };
}

async function main() {
  const skipUrlCheck = process.argv.includes("--skip-url-check");
  const dryRun = process.argv.includes("--dry-run");

  console.log("=== Startup Deals Ingestion Script ===\n");

  // Step 1: Clone/update repos
  console.log("Step 1: Fetching source repos...");
  cloneOrUpdate("https://github.com/dakshshah96/awesome-startup-credits.git", AWESOME_DIR);
  cloneOrUpdate("https://github.com/startupdeals/startupdeals.git", STARTUPDEALS_DIR);
  console.log();

  // Step 2: Parse both sources
  console.log("Step 2: Parsing entries...");
  const awesomeEntries = parseAwesomeStartupCredits();
  console.log(`  awesome-startup-credits: ${awesomeEntries.length} entries`);

  const startupDealsEntries = parseStartupDeals();
  console.log(`  startupdeals: ${startupDealsEntries.length} entries`);

  // Combine — awesome-startup-credits first (higher quality data)
  const allParsed = [...awesomeEntries, ...startupDealsEntries];
  console.log(`  Total parsed: ${allParsed.length} entries\n`);

  // Step 3: Load existing index
  const index = JSON.parse(readFileSync(INDEX_PATH, "utf8"));
  console.log(`Step 3: Existing index has ${index.offers.length} offers\n`);

  // Step 4: Deduplicate
  const { newEntries, duplicates } = deduplicate(allParsed, index.offers);
  console.log(`Step 4: After deduplication: ${newEntries.length} new, ${duplicates.length} duplicates\n`);

  // Step 5: URL health check
  let finalEntries: ParsedEntry[];
  if (skipUrlCheck) {
    console.log("Step 5: Skipping URL health checks (--skip-url-check)\n");
    finalEntries = newEntries;
  } else {
    console.log(`Step 5: Checking ${newEntries.length} URLs...`);
    const { live, dead } = await checkUrls(newEntries);
    console.log(`  ${live.length} live, ${dead.length} dead/unreachable\n`);
    if (dead.length > 0) {
      console.log("Dead URLs:");
      for (const d of dead.slice(0, 20)) console.log(`  - ${d}`);
      if (dead.length > 20) console.log(`  ... and ${dead.length - 20} more`);
      console.log();
    }
    finalEntries = live;
  }

  // Step 6: Convert to offer format
  const today = new Date().toISOString().split("T")[0];
  const offers = finalEntries.map((e) => toOfferFormat(e, today));

  // Category breakdown
  const categoryCounts: Record<string, number> = {};
  for (const o of offers) {
    categoryCounts[o.category] = (categoryCounts[o.category] || 0) + 1;
  }
  console.log("Step 6: Category breakdown of new entries:");
  for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
    console.log(`  ${cat}: ${count}`);
  }
  console.log();

  // Source breakdown
  const sourceCounts: Record<string, number> = {};
  for (const e of finalEntries) {
    sourceCounts[e.source] = (sourceCounts[e.source] || 0) + 1;
  }
  console.log("Source breakdown:");
  for (const [src, count] of Object.entries(sourceCounts)) {
    console.log(`  ${src}: ${count}`);
  }
  console.log();

  // Step 7: Output
  writeFileSync(OUTPUT_PATH, JSON.stringify({ offers }, null, 2));
  console.log(`Wrote ${offers.length} new entries to ${OUTPUT_PATH}`);

  if (!dryRun && offers.length > 0) {
    index.offers.push(...offers);
    writeFileSync(INDEX_PATH, JSON.stringify(index, null, 2) + "\n");
    console.log(`\nMerged into index. New total: ${index.offers.length} offers`);
  } else if (dryRun) {
    console.log(
      `\nDry run — not modifying index. Review ${OUTPUT_PATH} and re-run without --dry-run to merge.`
    );
  }

  // Summary
  console.log("\n=== Summary ===");
  console.log(`Source entries parsed: ${allParsed.length}`);
  console.log(`Duplicates skipped: ${duplicates.length}`);
  console.log(`Dead URLs filtered: ${newEntries.length - finalEntries.length}`);
  console.log(`Net new entries: ${offers.length}`);
  console.log(`Categories covered: ${Object.keys(categoryCounts).length}`);
}

main().catch((err) => {
  console.error("Fatal error:", err);
  process.exit(1);
});


path: scripts/monitor-pricing.js
content: #!/usr/bin/env node

/**
 * Automated pricing page monitor for high-churn vendors.
 *
 * Reads data/watchlist.json, fetches pricing pages in parallel,
 * compares content hashes against stored baselines, and logs
 * detected changes to data/pricing-changes.jsonl.
 *
 * Usage:
 *   npm run monitor:pricing              # check all vendors due today
 *   npm run monitor:pricing -- --init    # create baseline (first run)
 *   npm run monitor:pricing -- --all     # check all vendors regardless of interval
 *   npm run monitor:pricing -- --vendor "Vercel"  # check a specific vendor
 *
 * Add new vendors by editing data/watchlist.json.
 */

import { readFileSync, writeFileSync, appendFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const WATCHLIST_PATH = resolve(__dirname, "..", "data", "watchlist.json");
const HASHES_PATH = resolve(__dirname, "..", "data", "pricing-hashes.json");
const CHANGES_PATH = resolve(__dirname, "..", "data", "pricing-changes.jsonl");
const CONCURRENCY = 10;
const FETCH_TIMEOUT_MS = 15000;

export function extractTextContent(html) {
  let cleaned = html;
  cleaned = cleaned.replace(/<head[\s\S]*?<\/head>/gi, "");
  cleaned = cleaned.replace(/<script[\s\S]*?<\/script>/gi, "");
  cleaned = cleaned.replace(/<style[\s\S]*?<\/style>/gi, "");
  cleaned = cleaned.replace(/<svg[\s\S]*?<\/svg>/gi, "");
  cleaned = cleaned.replace(/<[^>]+>/g, " ");
  cleaned = cleaned.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ");
  cleaned = cleaned.replace(/&#?\w+;/g, " ");
  cleaned = cleaned.replace(/\b[0-9a-f]{8,}\b/gi, "");
  cleaned = cleaned.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "");
  cleaned = cleaned.replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}[^\s]*/g, "");
  cleaned = cleaned.replace(/\b\d{10,13}\b/g, "");
  cleaned = cleaned.replace(/\s+/g, " ").trim();
  return cleaned;
}

function hashContent(content) {
  return createHash("sha256").update(content).digest("hex");
}

async function fetchPage(url) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
  try {
    const res = await fetch(url, {
      signal: controller.signal,
      headers: {
        "User-Agent": "AgentDeals-PricingMonitor/1.0",
        Accept: "text/html",
      },
      redirect: "follow",
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.text();
  } finally {
    clearTimeout(timeout);
  }
}

export function isDueForCheck(entry, lastCheck, now = new Date()) {
  if (!lastCheck) return true;
  const lastDate = new Date(lastCheck);
  const diffMs = now.getTime() - lastDate.getTime();
  const diffHours = diffMs / (1000 * 60 * 60);
  return entry.interval === "daily" ? diffHours >= 24 : diffHours >= 168;
}

async function checkBatch(entries, hashes, isInit) {
  const results = { changed: [], unchanged: [], errors: [] };

  await Promise.all(entries.map(async (entry) => {
    try {
      const html = await fetchPage(entry.url);
      const text = extractTextContent(html);
      const hash = hashContent(text);
      const now = new Date().toISOString();
      const prev = hashes[entry.vendor];

      if (!isInit && prev && prev.hash && prev.hash !== hash) {
        results.changed.push({ vendor: entry.vendor, url: entry.url, oldHash: prev.hash, newHash: hash });
      } else {
        results.unchanged.push(entry.vendor);
      }

      hashes[entry.vendor] = { url: entry.url, hash, checkedAt: now };
    } catch (err) {
      const reason = err.name === "AbortError" ? "timeout" : err.message;
      results.errors.push({ vendor: entry.vendor, url: entry.url, error: reason });
    }
  }));

  return results;
}

function logChange(change) {
  const entry = {
    ts: new Date().toISOString(),
    vendor: change.vendor,
    url: change.url,
    type: "pricing_page_changed",
    oldHash: change.oldHash,
    newHash: change.newHash,
  };
  appendFileSync(CHANGES_PATH, JSON.stringify(entry) + "\n");
}

async function main() {
  const args = process.argv.slice(2);
  const isInit = args.includes("--init");
  const checkAll = args.includes("--all");
  const vendorIdx = args.indexOf("--vendor");
  const specificVendor = vendorIdx !== -1 ? args[vendorIdx + 1] : null;

  let watchlist;
  try {
    watchlist = JSON.parse(readFileSync(WATCHLIST_PATH, "utf-8"));
  } catch (err) {
    console.error(`Failed to read watchlist: ${err.message}`);
    process.exit(2);
  }

  let hashes = {};
  if (!isInit && existsSync(HASHES_PATH)) {
    try {
      hashes = JSON.parse(readFileSync(HASHES_PATH, "utf-8"));
    } catch {
      console.error("Warning: Could not parse hashes file, treating as init run.");
    }
  }

  let vendors = watchlist.vendors;
  if (specificVendor) {
    vendors = vendors.filter((v) => v.vendor === specificVendor);
    if (vendors.length === 0) {
      console.error(`Vendor "${specificVendor}" not found in watchlist.`);
      process.exit(2);
    }
  } else if (!checkAll && !isInit) {
    const now = new Date();
    vendors = vendors.filter((v) => isDueForCheck(v, hashes[v.vendor]?.checkedAt, now));
  }

  if (vendors.length === 0) {
    console.log("No vendors due for check. Use --all to force check all vendors.");
    process.exit(0);
  }

  console.log(`${isInit ? "Initializing baseline for" : "Checking"} ${vendors.length} vendor pricing pages...\n`);

  const allChanged = [];
  const allErrors = [];
  let unchangedCount = 0;

  // Process in batches for concurrency control
  for (let i = 0; i < vendors.length; i += CONCURRENCY) {
    const batch = vendors.slice(i, i + CONCURRENCY);
    const results = await checkBatch(batch, hashes, isInit);
    allChanged.push(...results.changed);
    allErrors.push(...results.errors);
    unchangedCount += results.unchanged.length;

    // Log each change immediately
    for (const c of results.changed) {
      logChange(c);
    }
  }

  // Save updated hashes
  writeFileSync(HASHES_PATH, JSON.stringify(hashes, null, 2) + "\n");

  // Summary
  if (isInit) {
    console.log(`Baseline created: ${Object.keys(hashes).length} vendors hashed.`);
  } else if (allChanged.length === 0) {
    console.log(`No pricing page changes detected (${unchangedCount} checked).`);
  } else {
    console.log(`${allChanged.length} pricing page(s) changed:\n`);
    for (const c of allChanged) {
      console.log(`  ${c.vendor} — ${c.url}`);
    }
    console.log(`\nChanges logged to ${CHANGES_PATH}`);
  }

  if (allErrors.length > 0) {
    console.log(`\n${allErrors.length} error(s):\n`);
    for (const e of allErrors) {
      console.log(`  ${e.vendor} — ${e.error} (${e.url})`);
    }
  }

  process.exit(allChanged.length > 0 ? 1 : 0);
}

const isMainModule = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
if (isMainModule) {
  main();
}


path: scripts/parse-free-for-dev.ts
content: #!/usr/bin/env node

import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const INDEX_PATH = resolve(ROOT, "data/index.json");
const FREE_FOR_DEV_DIR = resolve(ROOT, ".cache/free-for-dev");
const OUTPUT_PATH = resolve(ROOT, "data/free-for-dev-new.json");

// Categories to exclude (not dev infrastructure)
const EXCLUDED_CATEGORIES = new Set([
  "Education and Career Development",
  "Font",
  "Design Inspiration",
  "Dev Blogging Sites",
  "Browser-based hardware emulation written in Javascript",
  "Remote Desktop Tools",
  "Game Development",
  "Other Free Resources",
]);

// Map free-for-dev categories to our category names
const CATEGORY_MAP: Record<string, string> = {
  "Major Cloud Providers": "Cloud IaaS",
  "Cloud management solutions": "Infrastructure",
  "Source Code Repos": "Developer Tools",
  "APIs, Data, and ML": "Developer Tools",
  "Artifact Repos": "Developer Tools",
  "Tools for Teams and Collaboration": "Developer Tools",
  "CMS": "Headless CMS",
  "Code Generation": "Developer Tools",
  "Code Quality": "Developer Tools",
  "Code Search and Browsing": "Developer Tools",
  "CI and CD": "CI/CD",
  "Testing": "Testing",
  "Security and PKI": "Security",
  "Authentication, Authorization, and User Management": "Auth",
  "Mobile App Distribution and Feedback": "Developer Tools",
  "Management System": "Developer Tools",
  "Messaging and Streaming": "Messaging",
  "Log Management": "Logging",
  "Translation Management": "Developer Tools",
  "Monitoring": "Monitoring",
  "Crash and Exception Handling": "Error Tracking",
  "Search": "Search",
  "Email": "Email",
  "Feature Toggles Management Platforms": "Feature Flags",
  "Forms": "Forms",
  "Generative AI": "AI / ML",
  "CDN and Protection": "CDN",
  "PaaS": "Cloud Hosting",
  "BaaS": "Cloud Hosting",
  "Low-code Platform": "Developer Tools",
  "Web Hosting": "Cloud Hosting",
  "DNS": "DNS & Domain Management",
  "Domain": "DNS & Domain Management",
  "IaaS": "Cloud IaaS",
  "Managed Data Services": "Databases",
  "Tunneling, WebRTC, Web Socket Servers and Other Routers": "Developer Tools",
  "Issue Tracking and Project Management": "Developer Tools",
  "Storage and Media Processing": "Storage",
  "Design and UI": "Design",
  "Data Visualization on Maps": "Maps/Geolocation",
  "Package Build System": "Developer Tools",
  "IDE and Code Editing": "Developer Tools",
  "Analytics, Events and Statistics": "Analytics",
  "Visitor Session Recording": "Analytics",
  "International Mobile Number Verification API and SDK": "Developer Tools",
  "Payment and Billing Integration": "Payments",
  "Docker Related": "Container Registry",
  "Commenting Platforms": "Developer Tools",
  "Screenshot APIs": "Web Scraping",
  "Flutter Related and Building IOS Apps without Mac": "Developer Tools",
  "Privacy Management": "Security",
  "Miscellaneous": "Developer Tools",
};

// Generate tags based on mapped category
const CATEGORY_TAGS: Record<string, string[]> = {
  "Cloud IaaS": ["cloud", "iaas"],
  "Infrastructure": ["infrastructure", "cloud", "management"],
  "Developer Tools": ["developer-tools"],
  "Headless CMS": ["cms", "headless", "content"],
  "CI/CD": ["ci", "cd", "continuous-integration"],
  "Testing": ["testing", "qa"],
  "Security": ["security"],
  "Auth": ["auth", "authentication", "identity"],
  "Messaging": ["messaging", "streaming"],
  "Logging": ["logging", "log-management"],
  "Monitoring": ["monitoring", "observability"],
  "Error Tracking": ["error-tracking", "crash-reporting"],
  "Search": ["search"],
  "Email": ["email"],
  "Feature Flags": ["feature-flags", "feature-toggles"],
  "Forms": ["forms"],
  "AI / ML": ["ai", "ml", "generative-ai"],
  "CDN": ["cdn", "protection"],
  "Cloud Hosting": ["hosting", "paas"],
  "DNS & Domain Management": ["dns", "domain"],
  "Databases": ["database", "data"],
  "Storage": ["storage", "media"],
  "Design": ["design", "ui"],
  "Maps/Geolocation": ["maps", "geolocation"],
  "Analytics": ["analytics", "statistics"],
  "Payments": ["payments", "billing"],
  "Container Registry": ["docker", "containers"],
  "Web Scraping": ["scraping", "screenshots"],
};

interface ParsedEntry {
  vendor: string;
  url: string;
  description: string;
  sourceCategory: string;
  mappedCategory: string;
}

function cloneOrUpdate(): string {
  if (existsSync(FREE_FOR_DEV_DIR)) {
    console.log("Updating existing free-for-dev clone...");
    execSync("git pull", { cwd: FREE_FOR_DEV_DIR, stdio: "pipe" });
  } else {
    console.log("Cloning free-for-dev repo...");
    execSync(`git clone --depth 1 https://github.com/ripienaar/free-for-dev.git ${FREE_FOR_DEV_DIR}`, {
      stdio: "pipe",
    });
  }
  return resolve(FREE_FOR_DEV_DIR, "README.md");
}

function parseReadme(readmePath: string): ParsedEntry[] {
  const content = readFileSync(readmePath, "utf8");
  const lines = content.split("\n");
  const entries: ParsedEntry[] = [];
  let currentCategory = "";

  // Pattern: "  * [Name](url) — description" or "  * [Name](url) - description"
  const entryPattern = /^\s*\*\s+\[([^\]]+)\]\(([^)]+)\)\s*[\u2014\u2013\-:]+\s*(.+)/;

  for (const line of lines) {
    // Detect category headers
    const headerMatch = line.match(/^## (.+)/);
    if (headerMatch) {
      currentCategory = headerMatch[1].trim();
      continue;
    }

    // Skip excluded categories
    if (EXCLUDED_CATEGORIES.has(currentCategory)) continue;

    // Skip unmapped categories
    if (!CATEGORY_MAP[currentCategory]) continue;

    // Parse entry lines
    const entryMatch = line.match(entryPattern);
    if (entryMatch) {
      const [, vendor, url, description] = entryMatch;
      entries.push({
        vendor: vendor.trim(),
        url: url.trim(),
        description: description.trim().replace(/\s+/g, " "),
        sourceCategory: currentCategory,
        mappedCategory: CATEGORY_MAP[currentCategory],
      });
    }
  }

  return entries;
}

function normalizeVendorName(name: string): string {
  return name
    .toLowerCase()
    .replace(/\.(com|io|dev|sh|ai|co|org|net|app|cloud|so)$/i, "")
    .replace(/[^a-z0-9]/g, "")
    .trim();
}

function extractDomain(url: string): string {
  try {
    const hostname = new URL(url).hostname;
    return hostname.replace(/^www\./, "").toLowerCase();
  } catch {
    return "";
  }
}

function deduplicate(
  parsed: ParsedEntry[],
  existingOffers: any[]
): { newEntries: ParsedEntry[]; duplicates: string[] } {
  // Build lookup sets from existing offers
  const existingNames = new Set(existingOffers.map((o: any) => normalizeVendorName(o.vendor)));
  const existingDomains = new Set(
    existingOffers.map((o: any) => extractDomain(o.url)).filter(Boolean)
  );

  // Also track names we've already added (to avoid dupes within free-for-dev itself)
  const seenNames = new Set<string>();
  const newEntries: ParsedEntry[] = [];
  const duplicates: string[] = [];

  for (const entry of parsed) {
    const normalized = normalizeVendorName(entry.vendor);
    const domain = extractDomain(entry.url);

    if (existingNames.has(normalized) || (domain && existingDomains.has(domain)) || seenNames.has(normalized)) {
      duplicates.push(entry.vendor);
      continue;
    }

    seenNames.add(normalized);
    newEntries.push(entry);
  }

  return { newEntries, duplicates };
}

function toOfferFormat(entry: ParsedEntry, today: string) {
  const baseTags = CATEGORY_TAGS[entry.mappedCategory] || ["developer-tools"];
  // Add source tag
  const tags = [...baseTags, "free-for-dev"];

  return {
    vendor: entry.vendor,
    category: entry.mappedCategory,
    description: entry.description,
    tier: "Free",
    url: entry.url,
    tags,
    verifiedDate: today,
  };
}

async function checkUrls(entries: ParsedEntry[]): Promise<{ live: ParsedEntry[]; dead: string[] }> {
  const TIMEOUT_MS = 10000;
  const CONCURRENCY = 20;
  const live: ParsedEntry[] = [];
  const dead: string[] = [];

  // Process in batches
  for (let i = 0; i < entries.length; i += CONCURRENCY) {
    const batch = entries.slice(i, i + CONCURRENCY);
    const results = await Promise.allSettled(
      batch.map(async (entry) => {
        try {
          const controller = new AbortController();
          const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
          const resp = await fetch(entry.url, {
            method: "HEAD",
            signal: controller.signal,
            redirect: "follow",
            headers: { "User-Agent": "AgentDeals/1.0 (https://github.com/robhunter/agentdeals)" },
          });
          clearTimeout(timeout);
          return { entry, ok: resp.ok || resp.status === 403 || resp.status === 405 };
        } catch {
          // Retry with GET for servers that reject HEAD
          try {
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
            const resp = await fetch(entry.url, {
              method: "GET",
              signal: controller.signal,
              redirect: "follow",
              headers: { "User-Agent": "AgentDeals/1.0 (https://github.com/robhunter/agentdeals)" },
            });
            clearTimeout(timeout);
            return { entry, ok: resp.ok || resp.status === 403 };
          } catch {
            return { entry, ok: false };
          }
        }
      })
    );

    for (const result of results) {
      if (result.status === "fulfilled" && result.value.ok) {
        live.push(result.value.entry);
      } else if (result.status === "fulfilled") {
        dead.push(result.value.entry.vendor + " (" + result.value.entry.url + ")");
      } else {
        // Promise rejected — shouldn't happen with allSettled but just in case
        dead.push("unknown");
      }
    }

    if (i + CONCURRENCY < entries.length) {
      // Small delay between batches to avoid rate limiting
      await new Promise((r) => setTimeout(r, 500));
    }
  }

  return { live, dead };
}

async function main() {
  const skipUrlCheck = process.argv.includes("--skip-url-check");
  const dryRun = process.argv.includes("--dry-run");

  console.log("=== free-for-dev Ingestion Script ===\n");

  // Step 1: Clone/update
  const readmePath = cloneOrUpdate();
  console.log(`Parsing ${readmePath}...\n`);

  // Step 2: Parse
  const parsed = parseReadme(readmePath);
  console.log(`Parsed ${parsed.length} entries from free-for-dev\n`);

  // Step 3: Load existing index
  const index = JSON.parse(readFileSync(INDEX_PATH, "utf8"));
  console.log(`Existing index: ${index.offers.length} offers\n`);

  // Step 4: Deduplicate
  const { newEntries, duplicates } = deduplicate(parsed, index.offers);
  console.log(`After deduplication: ${newEntries.length} new, ${duplicates.length} duplicates\n`);

  // Step 5: URL health check
  let finalEntries: ParsedEntry[];
  if (skipUrlCheck) {
    console.log("Skipping URL health checks (--skip-url-check)\n");
    finalEntries = newEntries;
  } else {
    console.log(`Checking ${newEntries.length} URLs...\n`);
    const { live, dead } = await checkUrls(newEntries);
    console.log(`URL check: ${live.length} live, ${dead.length} dead/unreachable\n`);
    if (dead.length > 0) {
      console.log("Dead URLs:");
      for (const d of dead.slice(0, 20)) console.log(`  - ${d}`);
      if (dead.length > 20) console.log(`  ... and ${dead.length - 20} more`);
      console.log();
    }
    finalEntries = live;
  }

  // Step 6: Convert to offer format
  const today = new Date().toISOString().split("T")[0];
  const offers = finalEntries.map((e) => toOfferFormat(e, today));

  // Category breakdown
  const categoryCounts: Record<string, number> = {};
  for (const o of offers) {
    categoryCounts[o.category] = (categoryCounts[o.category] || 0) + 1;
  }
  console.log("Category breakdown of new entries:");
  for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
    console.log(`  ${cat}: ${count}`);
  }
  console.log();

  // Step 7: Output
  writeFileSync(OUTPUT_PATH, JSON.stringify({ offers }, null, 2));
  console.log(`Wrote ${offers.length} new entries to ${OUTPUT_PATH}`);

  if (!dryRun && offers.length > 0) {
    // Merge into index
    index.offers.push(...offers);
    writeFileSync(INDEX_PATH, JSON.stringify(index, null, 2) + "\n");
    console.log(`\nMerged into index. New total: ${index.offers.length} offers`);
  } else if (dryRun) {
    console.log(`\nDry run — not modifying index. Review ${OUTPUT_PATH} and re-run without --dry-run to merge.`);
  }

  // Summary
  console.log("\n=== Summary ===");
  console.log(`Source entries parsed: ${parsed.length}`);
  console.log(`Duplicates skipped: ${duplicates.length}`);
  console.log(`Dead URLs filtered: ${newEntries.length - finalEntries.length}`);
  console.log(`Net new entries: ${offers.length}`);
  console.log(`Categories covered: ${Object.keys(categoryCounts).length}`);
}

main().catch((err) => {
  console.error("Fatal error:", err);
  process.exit(1);
});


path: scripts/recategorize.ts
content: #!/usr/bin/env node

import { readFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const indexPath = resolve(__dirname, "../data/index.json");
const mappingPath = resolve(__dirname, "../data/category-mapping.json");

// Keyword-based categorization rules for Developer Tools entries
// that aren't in the explicit mapping. Applied in order — first match wins.
const keywordRules: Array<{ category: string; test: (v: string, d: string, tags: string[]) => boolean }> = [
  { category: "Startup Perks", test: (_v, d) => d.includes("Access via:") },
  { category: "Startup Perks", test: (v) => /startup|accelerat/i.test(v) },
];

interface Offer {
  vendor: string;
  category: string;
  description: string;
  tags?: string[];
  [key: string]: unknown;
}

interface IndexData {
  offers: Offer[];
  [key: string]: unknown;
}

function main() {
  const dryRun = process.argv.includes("--dry-run");
  const data: IndexData = JSON.parse(readFileSync(indexPath, "utf8"));
  const mapping: Record<string, string> = JSON.parse(readFileSync(mappingPath, "utf8"));

  const devTools = data.offers.filter(o => o.category === "Developer Tools");
  console.log(`Developer Tools entries: ${devTools.length}`);
  console.log(`Mapping entries: ${Object.keys(mapping).length}`);

  let moved = 0;
  let unmapped = 0;
  const unmappedVendors: string[] = [];

  for (const offer of data.offers) {
    if (offer.category !== "Developer Tools") continue;

    // Check explicit mapping first
    if (mapping[offer.vendor]) {
      offer.category = mapping[offer.vendor];
      moved++;
      continue;
    }

    // Fall back to keyword rules
    let matched = false;
    for (const rule of keywordRules) {
      if (rule.test(offer.vendor, offer.description || "", offer.tags || [])) {
        offer.category = rule.category;
        moved++;
        matched = true;
        break;
      }
    }

    if (!matched) {
      unmapped++;
      unmappedVendors.push(offer.vendor);
    }
  }

  // Count categories after recategorization
  const cats: Record<string, number> = {};
  for (const offer of data.offers) {
    cats[offer.category] = (cats[offer.category] || 0) + 1;
  }

  console.log(`\nMoved: ${moved}`);
  console.log(`Remaining in Developer Tools: ${unmapped}`);
  if (unmappedVendors.length > 0) {
    console.log(`Unmapped vendors: ${unmappedVendors.join(", ")}`);
  }

  console.log(`\nCategory distribution after recategorization:`);
  Object.entries(cats)
    .sort((a, b) => b[1] - a[1])
    .forEach(([cat, count]) => {
      console.log(`  ${count.toString().padStart(4)} ${cat}`);
    });

  // Verify acceptance criteria
  const maxCategory = Object.entries(cats).reduce((a, b) => a[1] > b[1] ? a : b);
  const devToolsRemaining = cats["Developer Tools"] || 0;
  const startupPerks = cats["Startup Perks"] || 0;

  console.log(`\n--- Acceptance Criteria ---`);
  console.log(`Max category: ${maxCategory[0]} (${maxCategory[1]}) — ${maxCategory[1] <= 120 ? "PASS" : "FAIL"} (limit: 120)`);
  console.log(`Developer Tools: ${devToolsRemaining} — ${devToolsRemaining < 30 ? "PASS" : "FAIL"} (limit: <30)`);
  console.log(`Startup Perks: ${startupPerks}`);
  console.log(`Total offers: ${data.offers.length}`);

  if (!dryRun) {
    writeFileSync(indexPath, JSON.stringify(data, null, 2) + "\n");
    console.log(`\nWrote updated index to ${indexPath}`);
  } else {
    console.log(`\n[DRY RUN] No changes written.`);
  }
}

main();


path: scripts/reverify.js
content: #!/usr/bin/env node

/**
 * Automated data re-verification pipeline.
 *
 * Finds stale entries (verifiedDate older than threshold), checks URL
 * reachability, and bumps verifiedDate for reachable vendors.
 *
 * Usage:
 *   npm run reverify                        # re-verify entries older than 25 days
 *   npm run reverify -- --threshold 14      # custom threshold
 *   npm run reverify -- --dry-run           # report only, don't modify data
 */

import { readFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const INDEX_PATH = resolve(__dirname, "..", "data", "index.json");
const DEFAULT_THRESHOLD_DAYS = 25;
const CONCURRENCY = 10;
const FETCH_TIMEOUT_MS = 15000;

export function findStaleOffers(offers, thresholdDays, now = new Date()) {
  const stale = [];
  const fresh = [];
  for (let i = 0; i < offers.length; i++) {
    const offer = offers[i];
    if (!offer.verifiedDate) {
      stale.push({ index: i, offer });
      continue;
    }
    const verified = new Date(offer.verifiedDate);
    const diffMs = now.getTime() - verified.getTime();
    const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
    if (diffDays >= thresholdDays) {
      stale.push({ index: i, offer });
    } else {
      fresh.push({ index: i, offer });
    }
  }
  return { stale, freshCount: fresh.length };
}

async function checkUrl(url) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
  try {
    // Try HEAD first (lighter)
    let res = await fetch(url, {
      method: "HEAD",
      signal: controller.signal,
      headers: {
        "User-Agent": "AgentDeals-Reverify/1.0",
        Accept: "text/html",
      },
      redirect: "follow",
    });
    // Some servers reject HEAD — fall back to GET
    if (res.status === 405 || res.status === 403) {
      clearTimeout(timeout);
      const controller2 = new AbortController();
      const timeout2 = setTimeout(() => controller2.abort(), FETCH_TIMEOUT_MS);
      try {
        res = await fetch(url, {
          method: "GET",
          signal: controller2.signal,
          headers: {
            "User-Agent": "AgentDeals-Reverify/1.0",
            Accept: "text/html",
          },
          redirect: "follow",
        });
      } finally {
        clearTimeout(timeout2);
      }
    }
    if (!res.ok) {
      return { reachable: false, error: `HTTP ${res.status}` };
    }
    return { reachable: true };
  } catch (err) {
    const reason = err.name === "AbortError" ? "timeout" : err.message;
    return { reachable: false, error: reason };
  } finally {
    clearTimeout(timeout);
  }
}

export async function reverifyBatch(entries, today) {
  const results = { verified: [], flagged: [] };

  await Promise.all(entries.map(async (entry) => {
    const result = await checkUrl(entry.offer.url);
    if (result.reachable) {
      results.verified.push({ index: entry.index, vendor: entry.offer.vendor, category: entry.offer.category });
    } else {
      results.flagged.push({
        index: entry.index,
        vendor: entry.offer.vendor,
        url: entry.offer.url,
        error: result.error,
      });
    }
  }));

  return results;
}

async function main() {
  const args = process.argv.slice(2);
  const dryRun = args.includes("--dry-run");

  const thresholdIdx = args.indexOf("--threshold");
  const thresholdDays = thresholdIdx !== -1
    ? parseInt(args[thresholdIdx + 1], 10)
    : DEFAULT_THRESHOLD_DAYS;

  if (isNaN(thresholdDays) || thresholdDays < 0) {
    console.error(`Invalid threshold: ${args[thresholdIdx + 1]}. Must be a non-negative integer.`);
    process.exit(2);
  }

  let data;
  try {
    data = JSON.parse(readFileSync(INDEX_PATH, "utf-8"));
  } catch (err) {
    console.error(`Failed to read index: ${err.message}`);
    process.exit(2);
  }

  const offers = data.offers || [];
  const { stale, freshCount } = findStaleOffers(offers, thresholdDays);

  if (stale.length === 0) {
    console.log(`All ${offers.length} entries verified within ${thresholdDays} days.`);
    console.log(`Re-verified: 0 | Flagged: 0 | Already fresh: ${freshCount} | Total: ${offers.length}`);
    process.exit(0);
  }

  console.log(`Found ${stale.length} stale entries (threshold: ${thresholdDays} days).`);
  if (dryRun) console.log("(dry-run mode — no changes will be written)\n");
  else console.log("");

  const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
  let totalVerified = 0;
  let totalFlagged = 0;

  // Process in batches for concurrency control
  for (let i = 0; i < stale.length; i += CONCURRENCY) {
    const batch = stale.slice(i, i + CONCURRENCY);
    const results = await reverifyBatch(batch, today);

    // Update verifiedDate for reachable entries
    if (!dryRun) {
      for (const v of results.verified) {
        data.offers[v.index].verifiedDate = today;
      }
    }

    // Log flagged entries
    for (const f of results.flagged) {
      console.log(`  ⚠ ${f.vendor} — ${f.error} (${f.url})`);
    }

    totalVerified += results.verified.length;
    totalFlagged += results.flagged.length;
  }

  // Write updated index
  if (!dryRun && totalVerified > 0) {
    writeFileSync(INDEX_PATH, JSON.stringify(data, null, 2) + "\n");
  }

  console.log(`\nRe-verified: ${totalVerified} | Flagged: ${totalFlagged} | Already fresh: ${freshCount} | Total: ${offers.length}`);

  process.exit(0);
}

const isMainModule = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
if (isMainModule) {
  main();
}


path: scripts/validate-data.ts
content: #!/usr/bin/env node

import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");

const VALID_CATEGORIES = [
  "AI / ML",
  "AI Coding",
  "API Development",
  "API Gateway",
  "Analytics",
  "Auth",
  "Background Jobs",
  "Browser Automation",
  "Banking & Finance",
  "CDN",
  "CI/CD",
  "Cloud Hosting",
  "Cloud IaaS",
  "Cloud Storage",
  "Code Quality",
  "Communication",
  "Communication & Messaging",
  "Consumer Email",
  "Container Registry",
  "DNS & Domain Management",
  "Databases",
  "Design",
  "Design & Creative",
  "Dev Utilities",
  "Education",
  "Diagramming",
  "Documentation",
  "Email",
  "Error Tracking",
  "Feature Flags",
  "Fitness & Health",
  "Forms",
  "Headless CMS",
  "IDE & Code Editors",
  "Infrastructure",
  "Localization",
  "Logging",
  "Low-Code Platforms",
  "Maps/Geolocation",
  "Media",
  "Meditation & Wellness",
  "Messaging",
  "Mobile Development",
  "Monitoring",
  "News & Reading",
  "Notebooks & Data Science",
  "Password Managers",
  "Payments",
  "Productivity & Notes",
  "Project Management",
  "Search",
  "Secrets Management",
  "Security",
  "Server Management",
  "Source Control",
  "Startup Perks",
  "Startup Programs",
  "Status Pages",
  "Storage",
  "Streaming & Media",
  "Team Collaboration",
  "Testing",
  "Tunneling & Networking",
  "VPN & Privacy",
  "Video",
  "Web Scraping",
  "Workflow Automation",
];

interface Offer {
  vendor: string;
  category: string;
  description: string;
  tier: string;
  url: string;
  tags: string[];
  verifiedDate: string;
  [key: string]: unknown;
}

interface DealChange {
  vendor: string;
  change_type: string;
  date: string;
  summary: string;
  previous_state: string;
  current_state: string;
  impact: string;
  source_url: string;
  category: string;
  alternatives: string[];
}

interface ValidationError {
  file: string;
  index: number;
  vendor: string;
  field: string;
  message: string;
}

const URL_REGEX = /^https?:\/\/.+/;
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;

const REQUIRED_OFFER_FIELDS = [
  "vendor",
  "category",
  "description",
  "tier",
  "url",
  "tags",
  "verifiedDate",
];

const REQUIRED_CHANGE_FIELDS = [
  "vendor",
  "change_type",
  "date",
  "summary",
  "previous_state",
  "current_state",
  "impact",
  "source_url",
  "category",
  "alternatives",
];

function validateOffers(offers: Offer[]): ValidationError[] {
  const errors: ValidationError[] = [];
  const seen = new Map<string, number>();

  for (let i = 0; i < offers.length; i++) {
    const offer = offers[i];
    const vendor = offer.vendor || `(index ${i})`;

    // Required fields
    for (const field of REQUIRED_OFFER_FIELDS) {
      if (offer[field] === undefined || offer[field] === null) {
        errors.push({
          file: "data/index.json",
          index: i,
          vendor,
          field,
          message: `Missing required field: ${field}`,
        });
      }
    }

    // URL format
    if (offer.url && !URL_REGEX.test(offer.url)) {
      errors.push({
        file: "data/index.json",
        index: i,
        vendor,
        field: "url",
        message: `Invalid URL format: ${offer.url}`,
      });
    }

    // Description length
    if (offer.description && offer.description.length < 30) {
      errors.push({
        file: "data/index.json",
        index: i,
        vendor,
        field: "description",
        message: `Description too short (${offer.description.length} chars, min 30): "${offer.description}"`,
      });
    }

    // verifiedDate format
    if (offer.verifiedDate && !ISO_DATE_REGEX.test(offer.verifiedDate)) {
      errors.push({
        file: "data/index.json",
        index: i,
        vendor,
        field: "verifiedDate",
        message: `Invalid date format (expected YYYY-MM-DD): ${offer.verifiedDate}`,
      });
    }

    // Valid verifiedDate
    if (offer.verifiedDate && ISO_DATE_REGEX.test(offer.verifiedDate)) {
      const d = new Date(offer.verifiedDate);
      if (isNaN(d.getTime())) {
        errors.push({
          file: "data/index.json",
          index: i,
          vendor,
          field: "verifiedDate",
          message: `Invalid date value: ${offer.verifiedDate}`,
        });
      }
    }

    // Category validation
    if (offer.category && !VALID_CATEGORIES.includes(offer.category)) {
      errors.push({
        file: "data/index.json",
        index: i,
        vendor,
        field: "category",
        message: `Unknown category: "${offer.category}"`,
      });
    }

    // Duplicate detection (same vendor + category)
    const key = `${offer.vendor}|||${offer.category}`;
    if (seen.has(key)) {
      errors.push({
        file: "data/index.json",
        index: i,
        vendor,
        field: "vendor+category",
        message: `Duplicate entry (same as index ${seen.get(key)}): ${offer.vendor} | ${offer.category}`,
      });
    } else {
      seen.set(key, i);
    }
  }

  return errors;
}

function validateDealChanges(changes: DealChange[]): ValidationError[] {
  const errors: ValidationError[] = [];

  for (let i = 0; i < changes.length; i++) {
    const change = changes[i];
    const vendor = change.vendor || `(index ${i})`;

    // Required fields
    for (const field of REQUIRED_CHANGE_FIELDS) {
      if (
        (change as Record<string, unknown>)[field] === undefined ||
        (change as Record<string, unknown>)[field] === null
      ) {
        errors.push({
          file: "data/deal_changes.json",
          index: i,
          vendor,
          field,
          message: `Missing required field: ${field}`,
        });
      }
    }

    // Date format
    if (change.date && !ISO_DATE_REGEX.test(change.date)) {
      errors.push({
        file: "data/deal_changes.json",
        index: i,
        vendor,
        field: "date",
        message: `Invalid date format (expected YYYY-MM-DD): ${change.date}`,
      });
    }

    // Source URL format
    if (change.source_url && !URL_REGEX.test(change.source_url)) {
      errors.push({
        file: "data/deal_changes.json",
        index: i,
        vendor,
        field: "source_url",
        message: `Invalid URL format: ${change.source_url}`,
      });
    }
  }

  return errors;
}

function main(): void {
  const indexPath = resolve(ROOT, "data/index.json");
  const changesPath = resolve(ROOT, "data/deal_changes.json");

  const indexData = JSON.parse(readFileSync(indexPath, "utf8"));
  const changesData = JSON.parse(readFileSync(changesPath, "utf8"));

  const offerErrors = validateOffers(indexData.offers);
  const changeErrors = validateDealChanges(changesData.changes);
  const allErrors = [...offerErrors, ...changeErrors];

  if (allErrors.length === 0) {
    console.log(
      `✓ All data valid. ${indexData.offers.length} offers, ${changesData.changes.length} deal changes.`
    );
    process.exit(0);
  }

  console.error(`Found ${allErrors.length} validation error(s):\n`);
  for (const err of allErrors) {
    console.error(
      `  [${err.file}] index ${err.index} (${err.vendor}): ${err.message}`
    );
  }
  console.error(
    `\n${indexData.offers.length} offers, ${changesData.changes.length} deal changes checked.`
  );
  process.exit(1);
}

// Export for testing
export {
  validateOffers,
  validateDealChanges,
  VALID_CATEGORIES,
  type Offer,
  type DealChange,
  type ValidationError,
};

main();


path: server.json
content: {
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "io.github.robhunter/agentdeals",
  "title": "AgentDeals",
  "description": "MCP server aggregating developer infrastructure deals, free tiers, and startup programs",
  "version": "0.3.1",
  "websiteUrl": "https://agentdeals.dev",
  "repository": {
    "url": "https://github.com/robhunter/agentdeals",
    "source": "github"
  },
  "icons": [
    {
      "src": "https://agentdeals.dev/favicon.png",
      "size": "400x400",
      "mediaType": "image/png"
    }
  ],
  "remotes": [
    {
      "type": "streamable-http",
      "url": "https://agentdeals.dev/mcp"
    }
  ]
}


path: smithery.yaml
content: startCommand:
  type: stdio
  configSchema:
    type: object
    properties: {}
  commandFunction:
    |-
    (config) => ({ command: 'node', args: ['dist/index.js'], env: {} })


path: src/api-client.ts
content: const DEFAULT_BASE_URL = "https://agentdeals.dev";
const TIMEOUT_MS = 10_000;

export function getBaseUrl(): string {
  return process.env.AGENTDEALS_API_URL || DEFAULT_BASE_URL;
}

async function apiFetch(path: string, params?: Record<string, string>): Promise<unknown> {
  const url = new URL(path, getBaseUrl());
  if (params) {
    for (const [k, v] of Object.entries(params)) {
      if (v !== undefined && v !== "") url.searchParams.set(k, v);
    }
  }

  let res: Response;
  try {
    res = await fetch(url.toString(), {
      signal: AbortSignal.timeout(TIMEOUT_MS),
    });
  } catch (err) {
    if (err instanceof DOMException && err.name === "TimeoutError") {
      throw new Error("AgentDeals API request timed out after 10 seconds. Try again later.");
    }
    throw new Error(
      `AgentDeals API is unreachable. Check your network connection or try again later. (${err instanceof Error ? err.message : String(err)})`
    );
  }

  if (!res.ok) {
    let body: string;
    try {
      body = await res.text();
    } catch {
      body = "";
    }
    throw new Error(`AgentDeals API error (${res.status}): ${body || res.statusText}`);
  }

  return res.json();
}

export async function fetchCategories(): Promise<unknown> {
  const data = await apiFetch("/api/categories") as { categories: unknown };
  return data.categories;
}

export async function fetchOffers(params: {
  q?: string;
  category?: string;
  eligibility_type?: string;
  sort?: string;
  stability?: string;
  limit?: number;
  offset?: number;
}): Promise<unknown> {
  const p: Record<string, string> = {};
  if (params.q) p.q = params.q;
  if (params.category) p.category = params.category;
  if (params.eligibility_type) p.eligibility_type = params.eligibility_type;
  if (params.sort) p.sort = params.sort;
  if (params.stability) p.stability = params.stability;
  if (params.limit !== undefined) p.limit = String(params.limit);
  if (params.offset !== undefined) p.offset = String(params.offset);
  return apiFetch("/api/offers", p);
}

export async function fetchOfferDetails(vendor: string, alternatives?: boolean): Promise<unknown> {
  const p: Record<string, string> = {};
  if (alternatives) p.alternatives = "true";
  return apiFetch(`/api/details/${encodeURIComponent(vendor)}`, p);
}

export async function fetchNewOffers(days?: number): Promise<unknown> {
  const p: Record<string, string> = {};
  if (days !== undefined) p.days = String(days);
  return apiFetch("/api/new", p);
}

export async function fetchDealChanges(params: {
  since?: string;
  type?: string;
  vendor?: string;
  vendors?: string;
}): Promise<unknown> {
  const p: Record<string, string> = {};
  if (params.since) p.since = params.since;
  if (params.type) p.type = params.type;
  if (params.vendor) p.vendor = params.vendor;
  if (params.vendors) p.vendors = params.vendors;
  return apiFetch("/api/changes", p);
}

export async function fetchStackRecommendation(useCase: string, requirements?: string[]): Promise<unknown> {
  const p: Record<string, string> = { use_case: useCase };
  if (requirements && requirements.length > 0) p.requirements = requirements.join(",");
  return apiFetch("/api/stack", p);
}

export async function fetchCosts(services: string[], scale?: string): Promise<unknown> {
  const p: Record<string, string> = { services: services.join(",") };
  if (scale) p.scale = scale;
  return apiFetch("/api/costs", p);
}

export async function fetchCompare(vendorA: string, vendorB: string): Promise<unknown> {
  return apiFetch("/api/compare", { a: vendorA, b: vendorB });
}

export async function fetchVendorRisk(vendor: string): Promise<unknown> {
  return apiFetch(`/api/vendor-risk/${encodeURIComponent(vendor)}`);
}

export async function fetchAuditStack(services: string[]): Promise<unknown> {
  return apiFetch("/api/audit-stack", { services: services.join(",") });
}

export async function fetchNewestDeals(params: {
  since?: string;
  limit?: number;
  category?: string;
}): Promise<unknown> {
  const p: Record<string, string> = {};
  if (params.since) p.since = params.since;
  if (params.limit !== undefined) p.limit = String(params.limit);
  if (params.category) p.category = params.category;
  return apiFetch("/api/newest", p);
}

export async function fetchExpiringDeals(withinDays?: number): Promise<unknown> {
  const p: Record<string, string> = {};
  if (withinDays !== undefined) p.within_days = String(withinDays);
  return apiFetch("/api/expiring", p);
}

export async function fetchWeeklyDigest(): Promise<unknown> {
  return apiFetch("/api/digest");
}


path: src/costs.ts
content: import { loadOffers, loadDealChanges, searchOffers } from "./data.js";
import type { Offer, DealChange } from "./types.js";

export interface ServiceCostEstimate {
  vendor: string;
  current_tier: string;
  free_tier_limits: string;
  estimated_monthly_cost: string;
  free_alternative?: { vendor: string; tier: string; description: string };
  recent_changes?: string[];
}

export interface CostEstimateResult {
  services: ServiceCostEstimate[];
  total_estimated_cost: string;
  savings_available: string;
  warnings: string[];
  scale: string;
}

type Scale = "hobby" | "startup" | "growth";

const SCALE_DESCRIPTIONS: Record<Scale, string> = {
  hobby: "Within free tier limits — side projects and prototyping",
  startup: "Likely exceeding some free tiers — early-stage product with real users",
  growth: "Exceeding most free tiers — established product with significant usage",
};

// Scale multipliers for rough cost estimation
// hobby = free, startup = $5-50/service, growth = $25-200/service
const SCALE_COST_RANGES: Record<Scale, { min: number; max: number }> = {
  hobby: { min: 0, max: 0 },
  startup: { min: 5, max: 50 },
  growth: { min: 25, max: 200 },
};

function extractFreeTierLimits(offer: Offer): string {
  // Extract the most useful part of the description (up to 200 chars)
  const desc = offer.description;
  if (desc.length <= 200) return desc;
  return desc.slice(0, 197) + "...";
}

function findFreeAlternative(offer: Offer): { vendor: string; tier: string; description: string } | undefined {
  const offers = searchOffers(undefined, offer.category);
  const alternative = offers.find(
    (o) =>
      o.vendor.toLowerCase() !== offer.vendor.toLowerCase() &&
      (o.tier === "Free" || o.tier === "Hobby" || o.tier === "Open Source")
  );
  if (!alternative) return undefined;
  return {
    vendor: alternative.vendor,
    tier: alternative.tier,
    description: alternative.description.length > 150
      ? alternative.description.slice(0, 147) + "..."
      : alternative.description,
  };
}

function getRecentChanges(vendorName: string): string[] {
  const changes = loadDealChanges();
  const sixMonthsAgo = new Date(Date.now() - 180 * 24 * 60 * 60 * 1000)
    .toISOString()
    .slice(0, 10);
  return changes
    .filter(
      (c) =>
        c.vendor.toLowerCase() === vendorName.toLowerCase() &&
        c.date >= sixMonthsAgo
    )
    .map((c) => `${c.date}: ${c.summary}`);
}

function generateWarnings(
  services: ServiceCostEstimate[],
  scale: Scale,
  offers: Map<string, Offer>
): string[] {
  const warnings: string[] = [];

  for (const svc of services) {
    const offer = offers.get(svc.vendor.toLowerCase());
    if (!offer) continue;

    const desc = offer.description.toLowerCase();

    // Warn about tight free tier limits at startup/growth scale
    if (scale !== "hobby") {
      if (desc.includes("1,000") || desc.includes("1000 ")) {
        warnings.push(
          `${svc.vendor} free tier has low request/usage limits — likely exceeded at ${scale} scale`
        );
      }
      if (desc.includes("10k mau") || desc.includes("10,000 mau") || desc.includes("10000 mau")) {
        warnings.push(
          `${svc.vendor} free tier limited to 10K MAU — you'll hit this at ${scale} scale`
        );
      }
      if (desc.includes("500 mb") || desc.includes("512 mb") || desc.includes("0.5 gi")) {
        warnings.push(
          `${svc.vendor} storage is under 1 GB on free tier — plan for upgrades at ${scale} scale`
        );
      }
    }

    // Warn about recent pricing changes
    if (svc.recent_changes && svc.recent_changes.length > 0) {
      warnings.push(
        `${svc.vendor} has had recent pricing changes — review current terms`
      );
    }
  }

  return warnings;
}

export function estimateCosts(
  vendorNames: string[],
  scale: Scale = "hobby"
): CostEstimateResult {
  const allOffers = loadOffers();
  const matchedOffers = new Map<string, Offer>();
  const services: ServiceCostEstimate[] = [];
  const unknownVendors: string[] = [];

  for (const name of vendorNames) {
    const lowerName = name.toLowerCase();
    const offer = allOffers.find((o) => o.vendor.toLowerCase() === lowerName);
    if (!offer) {
      unknownVendors.push(name);
      services.push({
        vendor: name,
        current_tier: "Unknown",
        free_tier_limits: "Vendor not found in our index",
        estimated_monthly_cost: "N/A",
      });
      continue;
    }

    matchedOffers.set(lowerName, offer);

    const recentChanges = getRecentChanges(offer.vendor);
    const costRange = SCALE_COST_RANGES[scale];
    let estimatedCost: string;
    if (scale === "hobby") {
      estimatedCost = "$0 (within free tier)";
    } else {
      estimatedCost = `$${costRange.min}-${costRange.max}/mo (estimated at ${scale} scale)`;
    }

    const svc: ServiceCostEstimate = {
      vendor: offer.vendor,
      current_tier: offer.tier,
      free_tier_limits: extractFreeTierLimits(offer),
      estimated_monthly_cost: estimatedCost,
    };

    // Only suggest alternatives for paid tiers or when scaling past free
    if (scale !== "hobby" || offer.tier !== "Free") {
      const alt = findFreeAlternative(offer);
      if (alt) svc.free_alternative = alt;
    }

    if (recentChanges.length > 0) {
      svc.recent_changes = recentChanges;
    }

    services.push(svc);
  }

  const warnings = generateWarnings(services, scale, matchedOffers);
  if (unknownVendors.length > 0) {
    warnings.unshift(
      `Unknown vendor(s): ${unknownVendors.join(", ")} — not in our index`
    );
  }

  // Calculate totals
  let totalEstimated: string;
  let savingsAvailable: string;
  const knownCount = services.length - unknownVendors.length;

  if (scale === "hobby") {
    totalEstimated = "$0/mo (all within free tiers)";
    savingsAvailable = "$0 — already on free tiers";
  } else {
    const range = SCALE_COST_RANGES[scale];
    const totalMin = range.min * knownCount;
    const totalMax = range.max * knownCount;
    totalEstimated = `$${totalMin}-${totalMax}/mo (estimated for ${knownCount} services at ${scale} scale)`;

    const alternativesCount = services.filter((s) => s.free_alternative).length;
    if (alternativesCount > 0) {
      savingsAvailable = `${alternativesCount} service(s) have free alternatives — switching could save $${range.min * alternativesCount}-${range.max * alternativesCount}/mo`;
    } else {
      savingsAvailable = "No free alternatives identified";
    }
  }

  return {
    services,
    total_estimated_cost: totalEstimated,
    savings_available: savingsAvailable,
    warnings,
    scale: `${scale} — ${SCALE_DESCRIPTIONS[scale]}`,
  };
}


path: src/index.ts
content: #!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server-remote.js";

async function main() {
  const server = createServer();
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("agentdeals MCP server running on stdio");
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});


path: src/types.ts
content: export interface Eligibility {
  type: "public" | "accelerator" | "oss" | "student" | "fintech" | "geographic" | "enterprise";
  conditions: string[];
  program?: string;
}

export interface Offer {
  vendor: string;
  category: string;
  description: string;
  tier: string;
  url: string;
  tags: string[];
  verifiedDate: string;
  eligibility?: Eligibility;
  expires_date?: string;
}

export type StabilityClass = "stable" | "watch" | "volatile" | "improving";

export interface EnrichedOffer extends Offer {
  recent_change: string | null;
  expires_soon: string | null;
  risk_level: "stable" | "caution" | "risky" | null;
  stability: StabilityClass;
  days_since_verified: number;
}

export interface OfferIndex {
  offers: Offer[];
}

export interface DealChange {
  vendor: string;
  change_type: "free_tier_removed" | "limits_reduced" | "restriction" | "limits_increased" | "new_free_tier" | "pricing_restructured" | "open_source_killed" | "pricing_model_change" | "startup_program_expanded" | "pricing_postponed" | "product_deprecated";
  date: string;
  summary: string;
  previous_state: string;
  current_state: string;
  impact: "high" | "medium" | "low";
  source_url: string;
  category: string;
  alternatives: string[];
}

export interface DealChangesIndex {
  changes: DealChange[];
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)