<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alma Mahler</title>
    <description>The latest articles on Forem by Alma Mahler (@alma_mahler_b061ac3ffc408).</description>
    <link>https://forem.com/alma_mahler_b061ac3ffc408</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868926%2F550a156f-180e-4c23-ba68-21b879a36d22.gif</url>
      <title>Forem: Alma Mahler</title>
      <link>https://forem.com/alma_mahler_b061ac3ffc408</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alma_mahler_b061ac3ffc408"/>
    <language>en</language>
    <item>
      <title>I built an AI-powered SEO API in one afternoon and put it on RapidAPI</title>
      <dc:creator>Alma Mahler</dc:creator>
      <pubDate>Thu, 09 Apr 2026 04:26:59 +0000</pubDate>
      <link>https://forem.com/alma_mahler_b061ac3ffc408/i-built-an-ai-powered-seo-api-in-one-afternoon-and-put-it-on-rapidapi-2l7n</link>
      <guid>https://forem.com/alma_mahler_b061ac3ffc408/i-built-an-ai-powered-seo-api-in-one-afternoon-and-put-it-on-rapidapi-2l7n</guid>
      <description>&lt;p&gt;I've been wanting to ship a tiny, useful API on RapidAPI for a while, mostly as an excuse to play with Claude's structured output. I finally blocked off an afternoon, and the result is live: &lt;a href="https://rapidapi.com/almamahler/api/seo-metadata-analyzer" rel="noopener noreferrer"&gt;SEO Metadata Analyzer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Point it at any URL and it returns the usual SEO stuff (title, description, OG tags), a readability score, and — the part I actually wanted to build — AI-generated keyword candidates grouped by intent (primary / secondary / long-tail).&lt;/p&gt;

&lt;p&gt;Here's what the stack looks like, what it cost, and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Every SEO tool I've used either wants $99/mo for a dashboard I don't need, or it scrapes a page and hands me a word-frequency list and calls that "keyword research." I wanted something in between: a boring REST endpoint that gives me &lt;em&gt;actual&lt;/em&gt; keyword candidates I can pipe into whatever script I'm writing that day.&lt;/p&gt;

&lt;p&gt;Once I realized Claude Haiku could do the keyword extraction reliably for under a cent a call, the whole thing became a one-afternoon project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — async handlers, Pydantic v2 response models, Depends for auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Haiku 4.5&lt;/strong&gt; via the Anthropic SDK — &lt;code&gt;client.messages.parse()&lt;/code&gt; with a Pydantic schema so the keywords come back pre-validated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;httpx + BeautifulSoup4 + lxml&lt;/strong&gt; — fetch and parse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;textstat&lt;/strong&gt; — Flesch-Kincaid readability for English; a simple custom scorer for Japanese&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; (with an in-memory fallback for local dev) — 24h response cache keyed by URL hash&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Railway&lt;/strong&gt; — deploy target, Redis addon, env vars&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI&lt;/strong&gt; — monetization, rate limiting, auth via &lt;code&gt;X-RapidAPI-Proxy-Secret&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
                 ┌────────────┐&lt;br&gt;
 client ───────▶ │  RapidAPI  │ ──▶ proxy secret check&lt;br&gt;
                 └─────┬──────┘&lt;br&gt;
                       │&lt;br&gt;
                       ▼&lt;br&gt;
             ┌──────────────────┐&lt;br&gt;
             │  FastAPI /analyze │&lt;br&gt;
             └─────────┬────────┘&lt;br&gt;
                       │&lt;br&gt;
                       ▼&lt;br&gt;
              ┌─────────────────┐&lt;br&gt;
              │  Redis cache?   │──hit──▶ return cached JSON&lt;br&gt;
              └────────┬────────┘&lt;br&gt;
                       │ miss&lt;br&gt;
                       ▼&lt;br&gt;
            ┌───────────────────────┐&lt;br&gt;
            │ httpx fetch + parse    │&lt;br&gt;
            │ (BS4 + lxml)           │&lt;br&gt;
            └──────────┬────────────┘&lt;br&gt;
                       │&lt;br&gt;
                       ▼&lt;br&gt;
           ┌─────────────────────────┐&lt;br&gt;
           │ readability (textstat)   │&lt;br&gt;
           └──────────┬──────────────┘&lt;br&gt;
                      │&lt;br&gt;
                      ▼&lt;br&gt;
         ┌─────────────────────────────┐&lt;br&gt;
         │ Claude Haiku keyword extract │&lt;br&gt;
         │  (Pydantic schema)           │&lt;br&gt;
         └──────────────┬──────────────┘&lt;br&gt;
                        │&lt;br&gt;
                        ▼&lt;br&gt;
                store in cache, return&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The whole &lt;code&gt;/analyze&lt;/code&gt; handler is ~30 lines. The interesting bit is that Claude returns structured JSON I never have to parse myself — &lt;code&gt;messages.parse()&lt;/code&gt; with a &lt;code&gt;KeywordList&lt;/code&gt; Pydantic model is the entire contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a response looks like
&lt;/h2&gt;

&lt;p&gt;Here's a real response for &lt;code&gt;https://www.anthropic.com&lt;/code&gt; (trimmed for readability):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "url": "https://www.anthropic.com",&lt;br&gt;
  "title": "Home \\ Anthropic",&lt;br&gt;
  "description": "Anthropic is an AI safety and research company...",&lt;br&gt;
  "og_tags": {&lt;br&gt;
    "og:title": "Home",&lt;br&gt;
    "og:description": "Anthropic is an AI safety and research company...",&lt;br&gt;
    "og:image": "https://cdn.sanity.io/.../anthropic-social.jpg",&lt;br&gt;
    "og:type": "website"&lt;br&gt;
  },&lt;br&gt;
  "language": "en",&lt;br&gt;
  "readability": {&lt;br&gt;
    "score": 42.1,&lt;br&gt;
    "grade_level": "Grade 11.8",&lt;br&gt;
    "method": "flesch-kincaid"&lt;br&gt;
  },&lt;br&gt;
  "keywords": [&lt;br&gt;
    { "keyword": "Claude AI",          "relevance": 0.95, "type": "primary"   },&lt;br&gt;
    { "keyword": "Anthropic",          "relevance": 0.92, "type": "primary"   },&lt;br&gt;
    { "keyword": "AI safety",          "relevance": 0.88, "type": "primary"   },&lt;br&gt;
    { "keyword": "large language models","relevance": 0.82, "type": "secondary" },&lt;br&gt;
    { "keyword": "responsible AI research","relevance": 0.74,"type": "long-tail"},&lt;br&gt;
    { "keyword": "enterprise AI solutions","relevance": 0.70,"type": "long-tail"}&lt;br&gt;
  ],&lt;br&gt;
  "cached": false&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The keyword buckets are what make this useful for me — primaries are the things you'd put in &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, long-tails are the things you'd target with a blog post. That categorization is literally just a prompt instruction to Claude, and the Pydantic schema enforces it on the way out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost math (being honest about it)
&lt;/h2&gt;

&lt;p&gt;Every miss is one Claude Haiku call. I benchmarked a handful of pages and the average is roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~2K input tokens (system prompt + page text, after extraction)&lt;/li&gt;
&lt;li&gt;~400 output tokens (structured keywords)&lt;/li&gt;
&lt;li&gt;Haiku 4.5 pricing: $1.00 / 1M input, $5.00 / 1M output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per miss: ~$0.004&lt;/strong&gt; (plus a rounding pad for variance)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I set the internal cost estimate to &lt;strong&gt;$0.0075/request&lt;/strong&gt; to give myself margin on longer pages. Cache hits cost $0. At 24h TTL and typical repeat access patterns, real-world average cost is well under half a cent per request.&lt;/p&gt;

&lt;p&gt;That's what made the pricing on RapidAPI work: even the BASIC tier (50 req/mo, free) can't put me in the red, and the paid tiers have healthy margin without being predatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs I made
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No JavaScript rendering.&lt;/strong&gt; It's plain &lt;code&gt;httpx&lt;/code&gt; + BeautifulSoup. SPAs with empty &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; won't get useful keywords. For my use case (blog posts, marketing pages, docs) this is fine, and it keeps the response time under a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24h cache, not configurable.&lt;/strong&gt; One less query param, one less thing to explain in the RapidAPI docs. If you care about fresh data, the TTL is short enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English + Japanese only for readability.&lt;/strong&gt; The keyword extraction works in any language Claude speaks, but the readability scorer is language-specific and I only wrote two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No batch endpoint yet.&lt;/strong&gt; If people actually use this I'll add one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;The thing that ate the most time wasn't the code — it was RapidAPI's monetization UI. The form for setting up pricing plans is a React-heavy modal that fights you if you try to move quickly, and the "Proxy Secret" they auto-generate can't be overridden, so you have to sync &lt;em&gt;their&lt;/em&gt; value into &lt;em&gt;your&lt;/em&gt; backend env, not the other way around. Once I figured that out it was fine, but I wasted 20 minutes on it.&lt;/p&gt;

&lt;p&gt;If I were doing this again I'd write the Railway deploy and the RapidAPI setup into a checklist before I touched the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI listing:&lt;/strong&gt; &lt;a href="https://rapidapi.com/almamahler/api/seo-metadata-analyzer" rel="noopener noreferrer"&gt;https://rapidapi.com/almamahler/api/seo-metadata-analyzer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free tier: 50 requests/month, no credit card&lt;/li&gt;
&lt;li&gt;PRO: $14.99/mo for 1,000 req/mo&lt;/li&gt;
&lt;li&gt;ULTRA: $49.99/mo for 5,000 req/mo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it I'd love to hear about it — especially if you wire the keyword output into a content brief generator or a competitive analysis script. That's the kind of downstream use I had in mind when I picked the three-bucket output shape.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>python</category>
      <category>fastapi</category>
    </item>
  </channel>
</rss>
