My Stream

A web app that pulls articles from RSS feeds, has an AI model score each one, and drafts only the ones that clear a quality threshold, in English and Arabic. Publishing is left to an editor.

Model calls cost money, and cheaper models score lower on the same rubric. So anything that can be rejected without a model call is rejected first, each remaining item gets one scoring call, and the threshold moves with the model, so one platform setting holds across ten providers.

Open My Stream

Plays the six stages below in order, about 45 seconds. Scroll or press Esc to stop.

Web app
Angular 15 and Ionic 6, server-rendered on AWS Amplify Hosting
Backend
Amplify Gen 2: AppSync GraphQL, 20 Lambda functions, DynamoDB
Sign-in
Amazon Cognito, email or phone, optional MFA
Billing
Stripe Checkout and the Stripe customer portal. Plans and prices
Models
10 providers. Anthropic works out of the box, the others with the workspace's own API key
Described here
The build live on feed.geohashim.com, deployed 6 October 2026
node offline-cycle.js --no-offset [Orchestrator] ── Article Scores (threshold: 60) ── [Orchestrator] ✗ FAIL | Overall: 56.2 | R:62 Q:58 T:20 | "City council approves new cycle lanes for the river district" [Orchestrator] No articles meet quality threshold for drafting Drafted: 0 articles

Offline runs of the deployed Lambda's own orchestrator and quota code, with the model and the database stubbed. The articles, model scores and plan counts are illustrative; lines starting with [harness] come from the test harness, and the italic lines mark trims.

1Start the run in the background

A run starts from the article queue's Run Full Cycle button as one GraphQL mutation. The Lambda records a job, hands the work to a second invocation of itself, and returns the job id straight away.

  • A request id seen in the last 90 seconds returns the existing job, so a double click or a retried request doesn't start a second run.
  • Before touching an article, the worker loads the workspace's AI profile and sends the chosen model a one-token probe. A retired model, an exhausted quota or a rejected key fails the job with a specific message; a transient error lets it continue.
  • The page polls the job record every 1.5 seconds and stops waiting after 15 minutes, the Lambda's own time limit. A cancel request is checked before scoring and again before drafting.

The other way in is the daily schedule. At 08:00 UTC, workspaces set to daily or weekdays get up to 10 articles written from their own topic list, as many as the workspace sets. That path skips scraping, scoring and the threshold. Its topic is still checked against the blocklist.

Four answers to the one-token probe Illustrative input

    Run Full Cycle, in the article queuemutation invokeArticleAutomation(action: full, orgId, requestId)Returns a job id at once. The worker probes the model, then starts on the articles.
    Messages, the error kind and the stop-or-continue decision come from the deployed validateModel and its error classifier, run offline against a stubbed provider. The four status codes are the inputs.

    2Scrape, then drop what's already known

    Every workspace gets the platform's shared RSS feeds, 13 in the code's defaults, plus any it adds. Before a model sees an item, two checks that cost nothing remove what shouldn't be scored.

    • Already drafted: the item's URL is normalised (lower-case host, tracking parameters and trailing slash removed) and compared with the source URLs of existing articles.
    • Blocked: admin patterns match the URL, the title or the body, as exact, prefix or contains.
    • Near-duplicates within the run are dropped by link, by title, or by more than 85% word overlap between titles.
    • Items with 1,000 characters of text or fewer get the full page fetched, three at a time.

    7 scraped, 5 queued for scoring Illustrative input

      Queued for scoring
      0
      Already drafted
      0
      Blocked
      0
      [Orchestrator] ⛔ Blocked: "Sponsored: the best mattress deals this week" matches title contains "sponsored"[Orchestrator] Pre-filter: skipped 1 already-drafted URL(s)[Orchestrator] Pre-filter: skipped 1 URL(s) matching admin blocklist[Queue] ✓ Added 5/5 articles (0 duplicates skipped)
      Outcomes come from the deployed normalizeUrl and matchBlockedPattern on these items; the existing article's source URL and the block pattern are illustrative. The log lines are from the offline run of the real orchestrator.

      3Score every item on one fixed rubric

      Up to a per-run cap, each queued item gets one model call with a fixed system prompt that asks for relevance, quality and trending, each out of 100. The workspace's voice and keywords don't enter scoring.

      • The overall score is 0.5 × relevance + 0.4 × quality + 0.1 × trending.
      • Before that, the code takes 10 quality points per warning sign, up to 30: clickbait phrasing, a title mostly in capitals, more than one exclamation mark, under 50 characters of text, or a speculative "will …?" title.
      • The cap is 5 items by default, and results are cached for 24 hours per Lambda instance.

      Three numbers become one Illustrative input

      Relevance, × 0.5Quality, × 0.4Trending, × 0.1
        The model's three scores are illustrative. The penalty, the weights and the arithmetic are the deployed code's, and the overall scores match the offline run's log: 56.2, 54.0, 42.0, 43.8 and 45.2.

        4Hold anything below the line

        The platform sets one quality threshold. The workspace's chosen model shifts it by a fixed offset, because cheaper models score lower on the same rubric, and the result is clamped between 20 and 95. Items below it are never drafted.

        effective = clamp(round(platform threshold + model offset), 20, 95)

        • Not every item that clears the line is drafted: only the top scorers, one per run by default, after a check that no article from the last 7 days has a near-identical title.
        • When nothing clears it, the article queue suggests a lower setting: five under the current line or five under the top quartile's score, whichever is lower, and never below 30.
        • Right before each draft, the workspace's count of AI generations this month is checked against its plan.

        This gate covers the article queue's runs. The scheduled daily run, described in stage 1, doesn't pass through it.

        Five scored items against the line Illustrative input

        Effective thresholdPlatform threshold, when the offset moves the lineOutside the clamp, 20 to 95
        Model offset
        −10
        Effective threshold
        50
        Cleared the line
        2 of 5
        Drafted this run
        1
        Model calls to draft
        3 to 6
        node offline-quota.js
        period 2026-10
        Pro, 499 used: allowed, counter now 500
        Pro, 500 used: QuotaExceededError: AI generation cap reached (500/500 this period). Upgrade the plan to continue.
        No plan, 10 used: QuotaExceededError: AI generation cap reached (10/10 this period). Upgrade the plan to continue.
        Offsets and the clamp are the deployed values, and every effective threshold on the slider was computed by the Lambda's own function; its copy in the web app agrees on all 819 model and threshold pairs checked. The slider has the product's own range, 0 to 100 in steps of 5. The five scores come from one illustrative Haiku run (stage 3); another model would score the items differently. The starting value of 60 is illustrative, and the plan counts in the quota run are fixtures.

        ClearedAbove this line a run has spent a one-token probe and one model call per scored item. Below it, each draft takes at least three more calls, and ends as a draft for an editor, not a published article.

        5Draft in the workspace's voice, in English and Arabic

        The workspace's AI profile goes in front of the drafting prompt as one line of guidelines: voice, reading level, target length, keywords and the citation rule. Its custom rules go at the end.

        • English body first, then an Arabic version with up to two attempts and a title-only translation if both fail, then three alternative headlines for an editor to choose from.
        • That is at least three model calls per draft and up to six, not counting retries, against one to score, which is why the line sits before drafting.
        • Each draft records the model, provider, tokens and cost. If every model call fell back, the model field is left empty rather than naming a model that wrote nothing.
        • The result is saved with the status drafted.

        What changes when you change the voice Illustrative input

        
                    
        1. English body1 call
        2. Arabic version1 or 2 calls
        3. Headline variants1 call
        4. Arabic title or category name0 to 2 calls
        The system prompt is the deployed buildSystem output for an illustrative profile: the default 1,200 words, two keywords, citations on and one custom rule. Switching voice changes one phrase. The six voices are the ones the product's AI Studio offers.

        6Leave publishing to an editor

        Nothing the pipeline writes is published by the pipeline. Drafts wait in the article queue for someone with the Editor role or above.

        • Approve, one at a time or in bulk, sets the status to published. Reject sets it to rejected and hides the article.
        • Public article lists, the sitemap and the public API leave drafts out.
        • Workspaces have Owner, Admin, Editor and Viewer roles, and each has its own AI profile, plan and Stripe subscription.

        One draft, two possible endings Illustrative input

        City council approves new cycle lanes for the river district

        Drafted from stage 4's run

        Statusdrafted

        Waiting for an editor
        • Public article listspublished only
        • Sitemappublished only
        • Public APIpublished only
        Status values and the filters come from the deployed article queue service, the public site's article queries, and the sitemap and public API Lambdas.

        What runs where

        Every piece below is in the deployed backend definition; nothing here is planned work. One Amplify app builds the backend and the server-rendered front end together, in one job per deploy of the main branch.

        Amplify Hosting

        The Angular and Ionic app, rendered on the server by a Node 20 compute function, with static files served alongside.

        Cognito

        Sign-in by email or phone, with optional SMS or authenticator-app MFA. After sign-up, onboarding creates the user's workspace and makes them its Owner.

        AppSync GraphQL

        32 data models, plus custom mutations backed by Lambda: start a run, manage members, check out, open the billing portal.

        Lambda

        33 functions. The pipeline runs in one of them, with a 900-second limit and 1 GB of memory. The others include membership, billing, email, webhooks, account deletion, the public API, the sitemap and the scheduled jobs.

        DynamoDB

        The tables behind the GraphQL models. The pipeline writes its job, queue and article rows directly.

        EventBridge

        Eight scheduled rules, among them scheduled generation at 08:00 UTC, trial reminders at 09:00 UTC and a publishing check every 5 minutes.

        Stripe

        Checkout for subscriptions and the customer portal. A webhook with a verified signature keeps each workspace's plan and status in step.

        S3 and SES

        Article images in S3, and transactional email through SES.

        Measured, and what wasn't

        The repository documents no benchmarks. These are the numbers that could be checked: one by re-running the code, one from the hosting service, and one that is only an estimate in the code.

        Two copies of one formula

        The effective threshold is computed in the web app, to preview it, and in the Lambda, to enforce it. Both were run on every catalog model at every step of the product's slider, 0 to 100 in steps of 5.

        39 models21 thresholds819 of 819 agree

        The offsets on this page are the Lambda's values for the twelve models the landing page lists.

        One push, one deploy

        Amplify builds the backend and the server-rendered front end in one job. The last three deploys of the main branch:

        Latest827 s
        Before756 s
        Earlier1,622 s

        Between about 13 and 27 minutes each, all successful.

        Scoring against drafting, estimated

        The model picker shows a cost per call from token counts fixed in the code: 3,500 in and 800 out to score, 6,000 in and 2,500 out per drafting call. These are estimates, not measurements.

        Sonnet 4.6, score$0.0225
        Sonnet 4.6, draft$0.0555
        Haiku 4.5, score$0.0075
        Haiku 4.5, draft$0.0185

        The draft estimate covers the English body alone. A draft also writes the Arabic version and makes smaller headline and translation calls.

        Not in this build

        Scoring on scheduled runs
        The daily run writes from a topic and never meets the threshold. Only runs from the article queue are scored.
        A threshold per workspace
        The base threshold is one platform setting. A workspace moves its line only by choosing a model.
        Live progress
        The page polls the job record every 1.5 seconds. Nothing is pushed to it.
        A way to set a dollar cap
        The code checks a monthly AI spend limit in dollars when a plan carries one, but no screen or setup script sets it. The limit plans are set up with is a count of AI generations a month.

        Try it

        My Stream runs at feed.geohashim.com. Sign up and create a workspace, choose a model and an editorial voice in AI Studio, add your own RSS feeds, then press Run Full Cycle in the article queue and watch the job.

        Open My Stream Pricing

        The source code is private.