Cost structure (target: <€30/month total)

ItemCostNotes
Supabase free tier€0Keep-alive cron prevents pause; upgrade (€25/mo) only when real traffic demands it
Vercel hobby€0Marketing site
IG Graph API€0Official Business Discovery — no scraping risk
Firebase Analytics + FCM€0
Apple Developer€99/yrThe one unavoidable cost, needed for TestFlight/App Store — defer until Phase 1
Google Play$25 once
Content production€0Digest tool auto-generates carousels

Advertising plan — pre-launch, Instagram-first (€0 media budget)

Principle: the digest IS the ad. @fommquell proved a weekend-digest page grows organically in this exact niche; Flow’s tool automates what they do by hand.

  1. Cadence: 2 carousels/week — Wed “anteprima weekend” (per-target grouping: TECHNO / APERITIVO / LIVE slides), Fri full digest (per-day VEN/SAB/DOM). Stories daily: countdown, polls (“dove andate stasera?”), repost of venue stories. All formats already documented in Instagram Strategy (pillars, timing, hashtags).
  2. Distribution hacks at €0: tag every venue/organizer that appears in a digest (they reshare — their audience is the acquisition channel); DM the digest to RE-area student group-chats admins (Unimore); collab posts with 2-3 micro venue pages per month.
  3. Conversion: every carousel CTA → chasetheflow.it (waitlist). Target: 1k RE-area followers → 20 beta testers → public beta.
  4. Measure weekly: follower growth RE-area %, saves/share per digest (the algorithm signals), waitlist conversions. Kill/adjust formats that don’t get saves.

Event sourcing from Instagram (the moat)

Pages like @italghisa publish every RE event as a post. Flow collects them via the official IG Graph Business Discovery API (zero ban risk, zero cost — client: tools/instagram_post_creator/pipeline/ig_graph_client.py):

  • Reads public posts of configured handles (IG_NIGHTLIFE_HANDLES=italghisa,...), extracts facts only (date/time/venue/title — captions are copyrighted and never stored).
  • Never-miss guarantee: posts the parser can’t date go to queue/ig_unparsed.json for 1-minute manual review — no event silently lost.
  • Merged with ERT open data behind one normalized shape; dedup keeps the richer source.
  • Digest generator groups by day or by target (skin targets) — non-repetitive covers via real per-weekend stats (free count, dominant vibe, busiest night).
  • Token-free path — LIVE 2026-07-22: ig_thirdparty_client.py fetches public posts via instagram120 on RapidAPI (free tier 1000 req/mo, single POST /api/instagram/posts with {"username":"…","maxId":""}; response result.edges[].node with caption.text + taken_at + code). The provider fetches on its own servers → the user’s IP and account are never used. Verified end-to-end: real posts from arci_tunnel / laviadellespezie / gargottapopolgiost / italghisa.re merged with ERT events into a digest. Handles are managed in the dashboard, not env. instagram-looter2 (GET, 2 calls/handle, 150 req/mo) is kept as a documented fallback in .env.example. Gotcha: use the EXACT username (italghisa.re, not italghisa) — a wrong handle returns a misleading provider error.
  • Not possible, for the record: reading a private saved-posts collection without the account — that data only exists behind the owner’s login. Sourcing works from public pages (italghisa etc.), not private saves.

Credit-efficient collection engine (2026-07-23)

275 pages are monitored (271 imported from @fommquell’s followings — the local page that posts every RE event — plus the hand-picked ones). A full sweep would cost 275 credits, so collection is deliberately bounded and incremental:

  • Never re-pull the same post. Each handle remembers the ids of posts already processed (state.seen_ids, rolling 60) — verified: re-fetching a page immediately returns 0 events, nothing reprocessed.
  • Never re-fetch a fresh page. refresh_hours (default 144 = 6 days) skips recently-checked handles entirely; handles_due() returns never-fetched pages first, then stale ones ordered by past productivity, so scarce credits go to pages that actually post events.
  • Bounded batch. max_per_run (default 40) caps calls per run; the rest roll over. Combined with the hard budget guard, the 1000/mo free tier can never be blown.
  • Credit memory. budget.used persists across runs with monthly auto-reset, plus a usage_log of the last 60 runs (calls / handles / events).

Event selection & text quality (no invention)

  • Is this post an event? looks_like_event() requires a parseable date plus a real signal (event word, time, or venue cue), and rejects recaps (recap, aftermovie, grazie a tutti, past dates). Doubtful posts are skipped, not published as guesses.
  • Normalisation. normalize_title() folds styled unicode (𝐎𝐅𝐅𝐂𝐋𝐔𝐁 → OFFCLUB), strips emoji, collapses whitespace and de-SHOUTs all-caps (“LA VIA DELLE SPEZIE” → “La Via Delle Spezie”).
  • Nothing invented. Venue stays empty when the caption doesn’t state one (it used to fall back to the city — that was inventing a location). Recap year roll-forward removed, so past events no longer surface as fake future dates.
  • No overloaded posts. Digest capped at 12 events (2 slides × 6 rows), grouped by day or by target/vibe.

Concurrency note: the store uses PID-unique temp files + retry on os.replace, and get_state() no longer writes on every read — on Windows the dashboard’s 30s polling was locking the file and silently dropping real writes.

Control Room dashboard (built 2026-07-22)

A local web dashboard at http://localhost:5050/dashboard (Flask, tools/instagram_post_creator/app.py + ui/dashboard.html) is the single control plane:

  • Pagine monitorate — add/remove the IG handles to collect from (persisted in config/monitor.json).
  • Budget richieste — live gauge of third-party API calls used vs the monthly free-tier cap; the reader (ig_thirdparty_client) records every call and hard-stops before exceeding the cap (counter auto-resets each month).
  • Genera — one click builds a digest grouped by day or by target (vibe).
  • Coda — preview each generated post (full-screen viewer, swipe all slides), Modifica its texts (edit event title/time/venue/category → regenerate via /api/render), then Approva / Programma (schedule publish time) / Scarta.

Event data comes from the ERT open-data API (real exhibitions/festivals), not fake data. Long-running events (a mostra open through the weekend) now render as “IN CORSO · fino al …” instead of their stale start date — and any residual data glitch (e.g. mojibake in an ERT title) can be hand-fixed in the Modifica editor before publishing.

Security hardening applied same day: secure_filename on the export route (path-traversal), Flask debug gated behind FLASK_DEBUG, XSS-safe DOM construction in the dashboard. Note: tools/ is not yet under git — version it before running from a cloud cron.

Dashboard v2 + post variety (2026-07-23)

Desktop Control Room (ui/dashboard.html) — full-width, zero vertical page scroll (verified: document height == viewport), five tabs, each panel scrolls internally with numbered pagination:

TabWhat it does
PanoramicaCredit gauge, collect button (the only credit-spending action), events by category, most productive pages, page coverage (checked / productive / mute / never seen), last collection runs
EventiFull cached-event table with search, source filter (IG vs open data), sort, pagination
Studio postThe post recipes, with a live preview of which events each would include
CodaApproval cards + full-screen slide viewer
Pagine275 monitored handles with per-page stats (events found, posts seen, last check), filters (productive / never / mute), limits config

Post recipes — same event pool, visibly different posts (verified in the queue: 4/5/6/12 events, two different skins): weekend_full (12) · top_picks (5, by engagement) · by_vibe (12, grouped by vibe) · free_night (free only) · tonight (6, soonest) · most_liked (8, by likes).

Real ranking signalslike_count, comment_count, view_count, usertags (tagged collaborators) and Instagram’s own location are now captured per post. Sort modes: smart / likes / engagement / soon / free / tagged. IG’s location tag now beats the caption regex for venue (authoritative, not guessed).

Speed

Generation went from minutes to ~2s (3.3s on a cold server). Two root causes fixed: (1) generation used to trigger a live fetch of 40 pages — collection is now decoupled behind an event cache; (2) Gemini was quota-exhausted and its SDK retried with 25s+ backoff on every post — now a 6s timeout plus a persisted circuit breaker (config/gemini_down.txt, 90 min) so a restarted server doesn’t re-pay it.

Anti-invention fix

The city_info “posto” prompt literally told the model “invent a plausible venue if you don’t know one”. Replaced: it must name a real venue or return {"unknown": true}, which falls back to generic city copy. A fabricated venue in a published post is a credibility risk.

Event pool: collect wide, select narrow (2026-07-23)

A design flaw was found by asking the obvious question — “the sweep found 98 events, why does the Events tab show 15?”:

  • Generation used to overwrite the pool. A post build ran a default weekend-window pull and cached its 15 results, discarding everything a wide sweep had collected. Now only an explicit collection writes the pool.
  • Events outside the window were discarded at fetch time. Credits were spent parsing a post, then the event was thrown away for being out of window. Now the IG reader keeps everything it extracts; in_window() filters at post-build time, so one sweep feeds a weekend digest, a “tonight” post and a monthly preview alike.
  • The pool accumulates and is retained. New findings are merged, not replaced, and pruned only RETAIN_DAYS_AFTER = 7 days past an event’s date. Verified: pool went 15 → 23 events while spending 7 credits.

Never re-pay for the same post

  • seen_ids (120/handle) — a post is parsed into the pool once.
  • newest_ts per handle — anything older than the newest post ever seen is not re-added, so a page that stops publishing never costs re-processing of its back-catalogue.
  • Parsing still runs for already-known posts (it is local regex, the API call is already paid) so the audit trail below is always complete.

Auditable fetching

Each handle stores a per-post trail — id, timestamp, permalink, and either what was extracted (date + title + place + likes) or why it was skipped. The dashboard’s Pagine tab makes a page clickable and shows this table, so a failed extraction is visible instead of silent. Verified on @arci_tunnel: 12 posts, 2 events extracted, 10 explained. Caption text is never stored (copyright) — only derived facts and the public permalink for verification.

Post styles: the design handoff was under-used (2026-07-23)

The Claude Design export at Downloads/Flow Design System (3)/instagram/ contains seven distinct Instagram post designs — only DenseListPost had been ported, which is why every post looked the same:

DesignPortedPurpose
DenseListPosttemplates/digest.pyDense warm list, 6 rows/slide
GenreGroupedPosttemplates/genre.pyDark, one full slide per event, giant vibe name cover
SingleEventPosteditorial.spotlightOne event, hero treatment: tinted cover + warm info slide with giant numerals
CityRoundupPosteditorial.roundupWarm city hero (186px name) + dark hour-by-hour timeline
PromotionPosteditorial.promoBrand manifesto — no event list; uses real aggregate stats
PresentationPostseditorial.presentationOnboarding carousel, 290px numerals, coral cover
Stories9:16 canvas — needs a separate size, not ported

genre.py is deliberately the opposite of the digest — where the digest packs six rows on a warm list, genre gives each event a full dark slide with a 116px title, numbered #n/total, a vibe-coloured pill and a “Quando · Dove” info box. Same brand tokens, very different rhythm.

Six visually distinct shapes (verified)

Dominant colour of each style’s first slide — proof they don’t read as the same post:

StyleOpening surface
digest / genre / promoink #0D0D0D (but different layouts: dense list vs one-event-per-slide vs manifesto)
spotlightvibe-tinted cover
roundupwarm #F0EEE9
presentationcoral #FF5E57

Ten recipes now map onto these formats and all render end-to-end: weekend_full, top_picks, by_vibe (multi-post), vibe_mix, free_night, tonight, most_liked, spotlight, roundup, promo, presentation.

Recipes now drive format, not just skin

A recipe sets its format, sort, size and window. Fixed: recipes used to fall through to pick_format(), so “stasera” could land on an editorial template with no event list at all.

  • by_vibeone post per vibe with ≥2 events (verified: 3 posts — musica 8, altro 5, arte 4). Vibes below the threshold are skipped, not padded.
  • vibe_mix → the single combined multi-vibe post.
  • spotlight → genre format, top 3 by engagement.
  • tonight → strictly events starting today; no silent fallback to the whole pool (that bug made a “tonight” post span three days). Ongoing exhibitions still render honestly as “IN CORSO · fino al …“.

Also fixed: queue JSON was read without an explicit encoding and crashed on accented titles under Windows cp1252.

Path to first revenue (from Target Study)

  1. Now→3 mo: free everything; grow channel + beta. Revenue: €0 by design.
  2. 3–6 mo: venues seen resharing digests get offered a free “featured” slot → relationship. PR list tool concept validated in conversations (PR economics run on WhatsApp/paper).
  3. 6+ mo (only if 100+ WAU): commission-only guestlist/ticketing (Clubbing/Xceed model: free for users, % on transactions). No subscriptions — the market rejected them.

Roadmap · Target Study · Instagram Strategy