Cost structure (target: <€30/month total)
| Item | Cost | Notes |
|---|---|---|
| Supabase free tier | €0 | Keep-alive cron prevents pause; upgrade (€25/mo) only when real traffic demands it |
| Vercel hobby | €0 | Marketing site |
| IG Graph API | €0 | Official Business Discovery — no scraping risk |
| Firebase Analytics + FCM | €0 | |
| Apple Developer | €99/yr | The one unavoidable cost, needed for TestFlight/App Store — defer until Phase 1 |
| Google Play | $25 once | |
| Content production | €0 | Digest 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.
- 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).
- 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.
- Conversion: every carousel CTA → chasetheflow.it (waitlist). Target: 1k RE-area followers → 20 beta testers → public beta.
- 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.jsonfor 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.pyfetches public posts via instagram120 on RapidAPI (free tier 1000 req/mo, single POST/api/instagram/postswith{"username":"…","maxId":""}; responseresult.edges[].nodewithcaption.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.usedpersists across runs with monthly auto-reset, plus ausage_logof 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:
| Tab | What it does |
|---|---|
| Panoramica | Credit gauge, collect button (the only credit-spending action), events by category, most productive pages, page coverage (checked / productive / mute / never seen), last collection runs |
| Eventi | Full cached-event table with search, source filter (IG vs open data), sort, pagination |
| Studio post | The post recipes, with a live preview of which events each would include |
| Coda | Approval cards + full-screen slide viewer |
| Pagine | 275 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 signals — like_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 = 7days 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_tsper 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:
| Design | Ported | Purpose |
|---|---|---|
| DenseListPost | ✅ templates/digest.py | Dense warm list, 6 rows/slide |
| GenreGroupedPost | ✅ templates/genre.py | Dark, one full slide per event, giant vibe name cover |
| SingleEventPost | ✅ editorial.spotlight | One event, hero treatment: tinted cover + warm info slide with giant numerals |
| CityRoundupPost | ✅ editorial.roundup | Warm city hero (186px name) + dark hour-by-hour timeline |
| PromotionPost | ✅ editorial.promo | Brand manifesto — no event list; uses real aggregate stats |
| PresentationPosts | ✅ editorial.presentation | Onboarding carousel, 290px numerals, coral cover |
| Stories | ⬜ | 9: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:
| Style | Opening surface |
|---|---|
| digest / genre / promo | ink #0D0D0D (but different layouts: dense list vs one-event-per-slide vs manifesto) |
| spotlight | vibe-tinted cover |
| roundup | warm #F0EEE9 |
| presentation | coral #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_vibe→ one 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)
- Now→3 mo: free everything; grow channel + beta. Revenue: €0 by design.
- 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).
- 6+ mo (only if 100+ WAU): commission-only guestlist/ticketing (Clubbing/Xceed model: free for users, % on transactions). No subscriptions — the market rejected them.