← back_to_portfolio
CASE STUDY // CONTENT AUTOMATION

CRYPTOPRISM SOCIALS

7 AI-generated Instagram posts daily — fully automated from database to publish

7 Posts / Day
GPT-4o mini Captions
99.5% Uptime
Zero Manual Work

> the_problem

CryptoPrism had already solved the hard part: 1000+ coins tracked, sentiment signals generated, indicators computed, long/short calls surfaced. The data pipeline was working. The problem was distribution.

Manual social posting is unsustainable at any quality bar worth maintaining. The data existed — it needed a publishing pipeline, not a person copying numbers into Canva at 6am.

The specific constraints made this harder than a generic social automation project: post timing had to match market open windows, images had to be pixel-perfect (not approximate), caption quality needed to be consistent, and the system had to survive Instagram's session expiry logic and GitHub Actions' ephemeral CI environment.

> my_approach

The core design decision: separate content generation from publishing. Neither layer needs to know about the other's implementation.

  • Jinja2 as Design System Each of the 11 post formats has a Jinja2 HTML template. The template is the single source of truth for layout, typography, and brand consistency. Change the template — every future post changes. No code required for design updates.
  • Playwright for Image Generation Playwright renders each Jinja2 template to a screenshot at exact pixel dimensions (1080x1920 for stories, 1080x1080 for carousels). This is the same engine Chrome DevTools uses for screenshots — pixel-perfect, CSS-consistent, no font rendering surprises.
  • GPT-4o-mini for Captions 120-150 character captions + 3-5 hashtags per post. GPT-4o-mini is fast enough (sub-2s) and cheap enough that running it 7x daily costs cents. OpenRouter provides multi-LLM fallback — if the primary endpoint is slow, it routes to an equivalent model.
  • GitHub Actions as Scheduler 7 separate CRON workflows, each triggering at its scheduled UTC time. No infrastructure to maintain, no always-on server costs, no monitoring overhead. The workflow logs are the audit trail.
  • Google Sheets Content Calendar A Sheets-backed content calendar gives visibility into what's scheduled without requiring database access. Non-technical stakeholders can view — and if needed, flag — upcoming content.

> architecture

   CRON TRIGGERS (GitHub Actions)
   02:00 UTC ──── Carousels workflow
   02:30–05:30 UTC ── Stories workflows (4x)
           │
           ▼
┌──────────────────────────────────────────────────┐
│              DATA FETCHER                         │
│  PostgreSQL (SQLAlchemy) → crypto data           │
│  Google Sheets API (gspread) → content calendar  │
│  Fear & Greed Index → BTC intelligence story     │
└──────────┬───────────────────────────────────────┘
           │  structured data context
           ▼
┌──────────────────────────────────────────────────┐
│           JINJA2 TEMPLATE RENDERER                │
│  11 templates: stories (4) + carousel slides (7) │
│  Fixed dimensions, strict CSS layout             │
│  No fluid layouts — pixel precision required     │
└──────────┬───────────────────────────────────────┘
           │  rendered HTML
           ▼
┌──────────────────────────────────────────────────┐
│           PLAYWRIGHT (Chromium)                   │
│  Headless browser screenshot at exact px dims    │
│  JPEG 95% quality output                         │
│  Stories: 1080x1920 / Carousels: 1080x1080       │
└──────────┬───────────────────────────────────────┘
           │  image files
           ▼
┌──────────────────────────────────────────────────┐
│           GPT-4o-mini CAPTIONS                    │
│  OpenRouter API (multi-LLM fallback)             │
│  120-150 chars + 3-5 hashtags per post           │
│  Retry with exponential backoff                  │
└──────────┬───────────────────────────────────────┘
           │  images + captions
           ▼
┌──────────────────────────────────────────────────┐
│           INSTAGRAPI PUBLISHER                    │
│  30-day session management + refresh logic       │
│  Story upload / carousel upload                  │
│  Post metadata logged to PostgreSQL              │
└──────────┬───────────────────────────────────────┘
           │
           ▼
      INSTAGRAM PUBLISHED
        

> hard_challenges

01 // Instagram session expiry — 30-day limit instagrapi sessions expire after 30 days. An expired session causes a silent auth failure — the publish call returns no error, but the post never appears. The fix: session age is checked before every publish run. If it's within 5 days of expiry, the session is refreshed proactively. If a publish call fails with an auth error, the session is invalidated and re-authenticated immediately. The 30-day window is treated as a 25-day operational window.
02 // Playwright in GitHub Actions CI Playwright running headless Chromium in a GitHub Actions Ubuntu runner requires specific system dependencies that aren't installed by default: libglib2.0-0, libnss3, libnspr4, and a handful of others. The workflow installs these explicitly before running Playwright. Additionally, Chromium must run with --no-sandbox in the CI environment — standard practice but not the default config. The workflow YAML has been stable across 6 months of daily runs.
03 // Image consistency across 11 templates 11 templates must produce consistent brand output — same fonts, same colour values, same spacing rhythm — despite being independently rendered. The solution: a shared CSS file imported by all templates, fixed px dimensions everywhere (no percentages, no viewport units), and a font preload strategy that ensures JetBrains Mono and Orbitron are fully loaded before Playwright takes the screenshot. A visual regression test suite compares current output against approved reference images before publishing.
04 // OpenRouter LLM fallback and retry logic The primary GPT-4o-mini endpoint occasionally returns 429s or 503s during high-traffic windows. The caption generator uses exponential backoff: first retry at 2s, second at 4s, third at 8s. If all retries fail on the primary model, OpenRouter's routing layer redirects to an equivalent model (Haiku or similar). In practice, the fallback fires roughly once per 500 caption requests — uptime impact is negligible.

> results

7 Posts / Day Automated 3 carousels + 4 stories — zero manual steps
11 Content Templates Jinja2 — change once, affects every future post
GPT-4o mini Captions 120-150 chars + hashtags — fast and cost-effective
99.5% Publish Uptime Measured across 6+ months of daily operation

> lessons_learned

Jinja2 + Playwright is an underrated stack for programmatic image generation. The alternative — Pillow image drawing — requires writing layout logic in Python code. Every design change is a code change. With Jinja2 + Playwright, design changes are HTML/CSS changes. The template is the design document. Designers can contribute without touching Python.

Caption quality matters less than posting consistency. GPT-4o-mini captions at 120 characters are good enough — and "good enough consistently" beats "excellent occasionally" on social media growth metrics. The system running reliably every day is worth more than a perfect caption.

GitHub Actions CRON is surprisingly reliable for content scheduling at this scale. The main failure modes are: (1) runner startup delay of 1-5 minutes past the scheduled time — acceptable, and (2) scheduled workflows being paused after 60 days of repository inactivity — solved by a weekly keep-alive workflow that makes a trivial commit.

Session management for instagrapi deserves more upfront investment than it seems. The 30-day expiry is a hard constraint that Instagram won't negotiate, and silent auth failures are harder to debug than explicit errors. Build the refresh logic before you need it.