{"id":49187718,"url":"https://github.com/mukundakatta/fanout","last_synced_at":"2026-05-27T07:01:43.718Z","repository":{"id":351928840,"uuid":"1213060466","full_name":"MukundaKatta/fanout","owner":"MukundaKatta","description":"Agentic content studio: one product → 5 platform-tailored drafts → posted from your own browser session. 15 channels, no third-party API keys.","archived":false,"fork":false,"pushed_at":"2026-05-20T01:34:04.000Z","size":255,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-20T04:51:58.661Z","etag":null,"topics":["agentic-ai","bluesky","browser-automation","chrome-extension","content-generation","fastapi","groq","indie-hackers","linkedin","llama","marketing-automation","mastodon","nextjs","reddit","social-media"],"latest_commit_sha":null,"homepage":"https://github.com/MukundaKatta/fanout","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MukundaKatta.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-17T02:17:17.000Z","updated_at":"2026-05-20T01:33:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/MukundaKatta/fanout","commit_stats":null,"previous_names":["mukundakatta/fanout"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/MukundaKatta/fanout","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MukundaKatta%2Ffanout","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MukundaKatta%2Ffanout/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MukundaKatta%2Ffanout/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MukundaKatta%2Ffanout/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MukundaKatta","download_url":"https://codeload.github.com/MukundaKatta/fanout/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MukundaKatta%2Ffanout/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33554780,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-27T02:00:06.184Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agentic-ai","bluesky","browser-automation","chrome-extension","content-generation","fastapi","groq","indie-hackers","linkedin","llama","marketing-automation","mastodon","nextjs","reddit","social-media"],"created_at":"2026-04-23T05:02:59.175Z","updated_at":"2026-05-27T07:01:43.713Z","avatar_url":"https://github.com/MukundaKatta.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fanout\n\n[![CI](https://github.com/MukundaKatta/fanout/actions/workflows/ci.yml/badge.svg)](https://github.com/MukundaKatta/fanout/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FMukundaKatta%2Ffanout\u0026project-name=fanout\u0026root-directory=web\u0026env=NEXT_PUBLIC_API_URL%2CNEXT_PUBLIC_SUPABASE_URL%2CNEXT_PUBLIC_SUPABASE_ANON_KEY)\n\nAgentic content studio for indie shippers. One product description → 5 distinct, platform-tailored drafts → posted from your own browser.\n\n**15 channels:** LinkedIn · X · Threads · Bluesky · Mastodon · Instagram · Reddit · Hacker News · Product Hunt · Medium · Dev.to · Email · Telegram · Discord · Slack\n\n## Architecture\n\n```\n                       ┌────────────────────────────────────────────┐\n                       │           web (Next.js 15 + Tailwind)       │\n                       │   /  composer  ·  /research  ·  /operator   │\n                       └─────────────────┬──────────────────────────┘\n                                         │ REST\n                                         ▼\n   ┌──────────────────────────────────────────────────────────────┐\n   │            backend (FastAPI + Postgres + SQLAlchemy)          │\n   │                                                                │\n   │   /generate  /variations            ─►  Groq (free Llama 3.3)  │\n   │   /research /research/sources/lead.   ─►  HN · Dev.to · Reddit │\n   │   /operator/run /operator/tick/all          · RSS              │\n   │   /drafts /drafts/{id}/outcomes                                │\n   │   /queue /posted                                               │\n   └─────────────┬─────────────────────────────────────────────────┘\n                 │ polls every 30s          ▲\n                 ▼                          │ POSTs engagement\n   ┌─────────────────────────────────────────────────────────────┐\n   │            extension (Chrome MV3) ── your browser session    │\n   │  · posts drafts via X / LinkedIn / Threads / Bluesky / ...  │\n   │  · outcome puller scrapes engagement off posted URLs        │\n   └─────────────────────────────────────────────────────────────┘\n```\n\n- **No third-party API keys** to authorise — extension drives composers in your own browser session\n- **Auto-post** on LinkedIn, X, Threads, Bluesky, Mastodon\n- **Assist / copy-and-open** for Reddit, HN, Product Hunt, Medium, Dev.to, Telegram, Discord, Slack\n- **mailto:** for Email — opens your default mail client with subject + body filled\n- **Self-tuning research loop** — pulls live HN / Dev.to / Reddit / RSS signals, drafts cite which signals they used, posted drafts feed engagement back, and the operator biases the next cycle toward sources that produce hits\n- Atomic claim via `SELECT ... FOR UPDATE SKIP LOCKED` so concurrent polls can't double-post\n\n## The autonomy loop\n\nFanout's compounding piece — once configured, the whole loop runs on a cron\nwith no human in the middle:\n\n```\nresearch-tick   (:17 hourly) ─►  bank fresh signals from HN / Dev.to / Reddit / RSS\noperator-tick   (:37 hourly) ─►  pick experiment + draft for each subscription\nextension queue (every 30s)  ─►  post via your browser session\noutcome puller  (every 60m)  ─►  scrape likes/comments/reposts/views from post URLs\n                                 └─► leaderboard fills\n                                         └─► next operator-tick weights\n                                             toward sources that earned engagement\n```\n\nEach layer is independently usable — turn off the operator and use the\nresearch workbench manually, or skip outcome polling and ground drafts\non live signals alone. They compose, they don't depend on each other.\n\n| Stage | What runs | Where to wire it |\n| --- | --- | --- |\n| **Research** | `POST /research` (manual), `POST /research/tick/all` (cron) | `docs/RESEARCH_LOOP.md` |\n| **Outcome feedback** | `POST /drafts/{id}/outcomes`, `GET /research/sources/leaderboard` | `docs/RESEARCH_LOOP.md#outcome-feedback-closing-the-loop` |\n| **Operator** | `POST /operator/run` (manual), `POST /operator/tick/all` (cron) | `docs/OPERATOR.md` |\n| **Extension outcome puller** | Background tabs on a configurable cadence | `extension/src/outcomes.js`, popup toggle |\n\n## Run locally\n\nPrereqs: Node 22+, Python 3.11+, Postgres, a free [Groq API key](https://console.groq.com/keys).\n\n```bash\n# 1. backend\ncd backend\npython3 -m venv .venv\n.venv/bin/pip install -r requirements.txt\ncp .env.example .env   # paste your GROQ_API_KEY\ncreatedb fanout\n.venv/bin/uvicorn app.main:app --reload\n\n# 2. web (in a second terminal)\ncd web\nnpm install\ncp .env.local.example .env.local   # for Supabase auth, otherwise leave blank\nnpm run dev\n# open http://localhost:3000\n\n# 3. extension (one-time)\n# Chrome → chrome://extensions → Developer mode → Load unpacked → pick ./extension\n```\n\n## Auth (optional)\n\nRuns in **dev mode** by default (single user, no login). To enable Supabase magic-link auth:\n\n1. Create a free Supabase project\n2. Set `SUPABASE_JWT_SECRET` in `backend/.env`\n3. Set `NEXT_PUBLIC_SUPABASE_URL` + `NEXT_PUBLIC_SUPABASE_ANON_KEY` in `web/.env.local`\n4. Optionally point `DATABASE_URL` at the Supabase Postgres\n5. After signing in, paste your access token into the extension popup so it can authenticate when polling\n\n## Repo layout\n\n```\nbackend/      FastAPI + agentic pipeline + Postgres store\n  app/agent.py     plan → write → critique → refine + variations(N)\n  app/research.py  HN / Dev.to / Reddit / RSS signal fetchers (no API keys)\n  app/operator.py  autonomous experiment picker + OperatorRun audit trail\n  app/store.py     SQLAlchemy-backed CRUD scoped by user_id\n  app/main.py      REST endpoints\n  migrations/      ordered .sql files (research, subscriptions, citations,\n                     draft_outcomes, operator_runs, operator_subscriptions)\n  tests/           pytest, mocked HTTP for research, sqlite-backed for store\nextension/    Chrome MV3 extension that posts via your browser session\n  src/background.js     polls /queue + outcome-puller alarm\n  src/outcomes.js       engagement-metric extractor (aria-label + visible-text)\n  src/content-*.js      one per platform\n  tests/test_outcomes.mjs   pure-Node smoke test of the extractor\nweb/          Next.js 15 + Tailwind + Supabase\n  app/page.tsx          composer + draft picker + sticky action bar\n  app/research/         research workbench + research/operator subscriptions\n  app/operator/         OperatorRun history timeline\n  lib/api.ts            typed client for every endpoint above\n  components/           Logo, Marquee, Spotlight, Typewriter, CitationsPill\ndocs/         deep-dive guides — start with RESEARCH_LOOP.md and OPERATOR.md\n```\n\n## Research loop\n\n\u003e Full reference: [`docs/RESEARCH_LOOP.md`](docs/RESEARCH_LOOP.md). This\n\u003e section is the 60-second tour.\n\nGeneration gets sharper when the agent has fresh signal to ground against.\nRun a research pass before generating, then opt in with `use_research: true`:\n\n```bash\n# pull signals into your account (deduped per user_id)\ncurl -X POST http://localhost:8000/research \\\n  -H 'content-type: application/json' \\\n  -d '{\"queries\":[\"ai agents\",\"content automation\"],\"rss_feeds\":[\"https://news.ycombinator.com/rss\"]}'\n\n# generate drafts that reference the top unused snippets\ncurl -X POST http://localhost:8000/generate \\\n  -H 'content-type: application/json' \\\n  -d '{\"product\":\"...\",\"use_research\":true}'\n```\n\nSnippets are scored by upstream popularity × recency (48h half-life), deduped\nper `(user_id, url)`, and marked **used** the first time they feed into a\ndraft so the next research run pulls fresh material.\n\n### Autonomous loop (recurring subscriptions)\n\nSave a query/feed config once and let it refresh on its own:\n\n```bash\n# create a daily subscription\ncurl -X POST http://localhost:8000/research/subscriptions \\\n  -H 'content-type: application/json' \\\n  -d '{\"name\":\"AI agents\",\"queries\":[\"ai agents\",\"llm tooling\"],\"interval_hours\":24}'\n\n# tick the loop — runs every active subscription that's due\ncurl -X POST http://localhost:8000/research/tick\n```\n\nTwo tick endpoints depending on use case:\n\n- **`POST /research/tick`** — per-user, JWT auth. The \"Run due now\" button on\n  the workbench hits this.\n- **`POST /research/tick/all`** — service-auth (`X-Tick-Secret` header), fans\n  out across every user in one request. **Disabled by default** — set\n  `RESEARCH_TICK_SECRET` env var on the backend to enable.\n\nFor a real cron, use `/research/tick/all`. Wire it to **any** scheduler —\nVercel cron, Render scheduled job, or the bundled GitHub Action at\n`.github/workflows/research-tick.yml`.\n\nTo enable the GitHub Action:\n\n1. Set backend env var `RESEARCH_TICK_SECRET` to a strong random string\n2. Set repo **Variable** `RESEARCH_TICK_ENABLED = \"true\"`\n3. Set repo **Secret** `RESEARCH_TICK_URL` = `https://\u003cyour-backend\u003e/research/tick/all`\n4. Set repo **Secret** `RESEARCH_TICK_SECRET` to the same value as step 1\n\nThe action runs hourly. The tick endpoint is idempotent — subscriptions that\naren't due yet are skipped server-side, so over-polling is safe.\n\n## Eva operator\n\n\u003e Full reference: [`docs/OPERATOR.md`](docs/OPERATOR.md).\n\nThe operator wraps `/generate` with two things the research loop alone can't\ngive you:\n\n1. **An experiment picker** — biases snippet selection toward sources whose\n   past drafts have produced engagement (via the leaderboard from outcomes).\n   Cold-start safe: falls back to plain score-desc when no outcomes exist.\n2. **An audit row** — every cycle logs product, platforms, picked snippet\n   ids, draft ids, status and a human-readable notes string. Replayable\n   from `/operator/runs` or the `/operator` page.\n\n```bash\n# one cycle, all platforms, leaderboard-weighted\ncurl -X POST http://localhost:8000/operator/run \\\n  -H 'content-type: application/json' \\\n  -d '{\"product\":\"\u003cblurb\u003e\",\"platforms\":[\"x\",\"linkedin\"]}'\n\n# save it as a recurring cycle on the workbench\ncurl -X POST http://localhost:8000/operator/subscriptions \\\n  -d '{\"name\":\"daily-social\",\"product\":\"\u003cblurb\u003e\",\"platforms\":[\"x\",\"linkedin\"]}'\n```\n\nService-auth tick + bundled GitHub Action at\n`.github/workflows/operator-tick.yml` mirror the research-tick setup —\nenable with `OPERATOR_TICK_ENABLED=true` + `OPERATOR_TICK_URL` +\n`OPERATOR_TICK_SECRET`. Runs at minute :37 hourly, deliberately offset from\nresearch-tick at :17 so they don't fight for backend attention.\n\n## Extension outcome puller\n\n\u003e Wired into the popup; off by default until you opt in.\n\nThe Chrome extension already posts drafts via your browser session. The\noutcome puller closes the loop: it visits each posted-draft URL on a\nconfigurable cadence (default 60 min), scrapes engagement via a\nDOM-agnostic extractor, and POSTs the metrics to `/drafts/{id}/outcomes`\nso the leaderboard fills.\n\n- **Two-pass extractor:** aria-label scan first (X / Bluesky / Threads\n  carry exact counts there), then visible-text fallback\n  (LinkedIn / Mastodon). Max wins so an `aria-label=\"1,234 likes\"`\n  beats a visible `\"1.2K likes\"` rounded form.\n- **Normalizes** `1,234` / `1.2K` / `1.5M` / `1B` to an int. Open metric\n  vocabulary — recognizes likes / favorites, comments / replies, reposts\n  / retweets / boosts / reblogs / shares, views / impressions, clicks,\n  saves / bookmarks.\n- **Defaults off:** opening background tabs to social pages requires\n  explicit consent via the popup toggle.\n\n## Deploying the web app\n\n**Easy mode — Vercel native git integration (recommended):**\n\n1. Click [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FMukundaKatta%2Ffanout\u0026project-name=fanout\u0026root-directory=web\u0026env=NEXT_PUBLIC_API_URL%2CNEXT_PUBLIC_SUPABASE_URL%2CNEXT_PUBLIC_SUPABASE_ANON_KEY) above\n2. Set `Root Directory` to `web` when prompted\n3. Provide the three `NEXT_PUBLIC_*` env vars\n4. Vercel auto-deploys on every push to `main`\n\n**Optional — CI-driven deploy from GitHub Actions:**\n\nThe repo includes [`.github/workflows/deploy-web.yml`](.github/workflows/deploy-web.yml). It runs only when you set:\n\n- Secret `VERCEL_TOKEN`\n- Secret `VERCEL_ORG_ID`\n- Secret `VERCEL_PROJECT_ID`\n- Variable `VERCEL_DEPLOY_ENABLED=true`\n\nThis lets CI gate the deploy and post the live URL back to the commit.\n\n## Security\n\nSee [SECURITY.md](SECURITY.md). Short version: the backend never holds your social\ncredentials — the extension drives composers in your own logged-in browser session.\n\n## Contributing\n\nPRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). `main` is protected; CI must\npass and changes flow through PRs.\n\nCI includes a repository-layout contract check for the backend, web app, and\nChrome extension so future refactors do not accidentally remove a deployable\nsurface.\n\n## License\n\n[MIT](LICENSE) © Mukunda Katta\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmukundakatta%2Ffanout","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmukundakatta%2Ffanout","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmukundakatta%2Ffanout/lists"}