Verbomatique Secondaire — Project Knowledge
Verbomatique Secondaire — Project Knowledge
Last updated: August 24, 2026
Provenance of this rebuild (24 Aug 2026). The 20 Aug file was lost. This file was rebuilt from the 19 Aug copy, with the 20 Aug edits recovered verbatim from the conversation that produced them, plus the 24 Aug additions, the Reddit platform guidance, and four corrections. One passage could not be re-sourced and was deliberately left out — see “Known gap in this file” at the foot of the Advertising section.
What belongs here, and what doesn’t. This file holds knowledge that lives outside the repo: store declarations, ad account IDs, editor quirks, campaign decisions, legal constraints, and why choices were made. Facts inside the repo — code invariants, architecture, testing conventions — belong in the repo’s
CLAUDE.md, which Claude Code reads automatically every session and which is versioned alongside the code it describes.Rule of thumb: if a fact would be invalidated by a refactor, it belongs in
CLAUDE.md, not here. Never record file line numbers in this file; name symbols and give a command to re-derive the answer.
The Product
What it is: A Flutter mobile app that helps secondary school students practice French verb conjugations. Three practice modes: cover-and-recall, flashcards, and type-it-out. After practice, students test themselves and get a score report. Purely on-device, no server component.
Who it’s for: Teenagers in secondary school studying French. Two live markets:
- nl-BE (Dutch-speaking Belgium) — the original audience
- en-CA (English Canada) — French immersion students, excluding Quebec
The buyer is usually not the user. Students are typically minors; the decision-maker is usually the parent (30-55, stressed about homework) or teacher. Ad targeting, copy, and site messaging address parents, not students. Meta and Google both restrict targeting minors directly.
But the audience is not uniformly minors. Adult FSL/immersion learners in Canada (francisation, newcomers, federal bilingualism candidates), teachers evaluating on their own device, parents who installed it on their own phone, and adult self-learners are all real cohorts. For them no minor-related restriction applies. Any design that assumes 100% minors will be wrong; the correct shape is a fork.
Code & deployment: Flutter, stored on GitHub, exhaustive unit and integration tests. Deployed on both App Store and Play Store. App ID: be.teamwabisabi.verbomatique_secondaire. Apple App ID: 1611881585.
Developer: Dries Bellen (decibellen@gmail.com)
The basic set is 32 verbs, and there is currently no gated verb content
Verified 25 Aug 2026 by Dries, directly in the app: 32 verbs, all in the basic set, no IAP required for any of them. Independently derived the same day from the source, by tracing what the picker actually reads rather than by counting files:
FrenchRepository._internal()registers 32 verb instances, keyed by infinitive.- 32 distinct infinitives, no duplicates — the map collapses nothing.
- The picker is
verbs.keys.toList()..sort()with nowhere, no purchase or entitlement filter. Nothing is hidden from the wheel. - The only IAP product in the codebase is
remove_ads_lifetime. There is no verb-pack purchase at all, so no verb can be locked.
That chain — registration list → VerbRepository.verbs.keys → the wheel — is the re-derivable
source. scripts/list_verbs.py on harness/screenshot-pipeline parses it, so the number can
be regenerated rather than remembered.
This is stable knowledge: verb content changes only when someone deliberately adds verbs, not
as a side effect of any other work. It is recorded here rather than in CLAUDE.md because it
is a product scope fact that marketing, store copy and IAP planning all depend on, not a
code invariant.
Because extra verb-set IAPs remain Planned, NOT live, the basic set is currently the whole set — confirmed by the absence of any verb-pack product, not merely by the roadmap’s status label. Any of the 32 is fair game for a screenshot, a card or a store claim.
Nothing publicly states this number. Checked 25 Aug 2026: the App Store listing, the Play Store listing, verbomatique.com and secondaire.verbomatique.be all state no verb count; the .be site refers to “extra werkwoordensets” only as upcoming. A concrete “32 verbs” is the kind of specific the listings currently lack and search intent rewards — worth a decision rather than an omission by default.
Source: Dries, direct check in the app, 25 Aug 2026; file count from a Claude Code session the same day
Monetization (current state as of Aug 2026)
| Tier | Status | Price | What you get |
|---|---|---|---|
| Free | Shipped | $0 | Ad-supported. Basic verb set. |
| Paid (one-time) | Shipped | €1.99 (BE) / CAD $1.99 (CA) | No ads. Same basic verb set. Store floor price in each region. |
| Subscription | Planned | TBD | No ads + the entire verb catalogue, including sets added later. Pricing not decided. |
| Extra verb-set IAPs | Planned, NOT live | TBD | Verbs beyond the basic set, sold as sets — one SKU per set. Not built. |
Important: When extra verb-set IAPs ship, add IAP mention back to both site pricing sections. Currently deliberately omitted since they’re not purchasable.
In-app upgrade nudge (Free → Paid): Design work done 18 Aug 2026, tracked in Asana (“In-app upgrade nudge (Free → Paid)”). Three decisions are open as subtasks there. Do not re-litigate the design in this file; the Asana task holds the current state.
Design summary as decided: three lanes keyed on a platform age signal. Lane A (18+) gets the full nudge after the score report on a completed session — explicitly not ad-adjacent. Lane B (under 18) gets no in-app exhortation at all; Paid stays passively discoverable in Settings. Lane C (signal unavailable or declined) is treated as Lane B and is the safe default. For lanes B and C the offer relocates to adult-to-adult channels already owned: the Brevo Ouder/Parent segments and both sites’ pricing sections.
Legal constraint on any in-app purchase prompt
UCPD Annex I point 28 (Belgium: Boek VI WER, art. VI.100, 28°) blacklists, as unfair in all circumstances with no balancing test, “including in an advertisement a direct exhortation to children to buy advertised products or persuade their parents or other adults to buy advertised products for them.” The EU CPC common position applies this to in-app purchase prompts and extends it to apps merely likely to appeal to children, not only apps targeted at them.
Practical rule: an “ask a parent to unlock” / share-link / QR handoff shown to a minor is more exposed than a plain Buy button, not less. Neutral, non-imperative disclosure that a paid tier exists (the current Settings placement) sits outside the prohibition. Not legal advice — worth a Belgian consumer-law solicitor’s review before building any upsell.
Platform age signals (relevant to the lane design)
- Apple Declared Age Range API — global since February 2026. Available in BE and CA today.
- Google Play Age Signals API — beta. Brazil since March 2026, Canada from mid-August 2026, full global rollout stated for later in 2026.
Both return a band (0–12 / 13–15 / 16–17 / 18+ by default), plus whether the signal was parent-set, self-declared or verified, and both can return declined or verification required.
Market asymmetry: en-CA has Apple available and Play rolling out now. nl-BE has Apple available, Play not until end of 2026. So nl-BE Android is effectively unaddressable for an age-gated in-app nudge this autumn.
Affordability is the pricing principle, not just the Paid price point
Affordability is a stated product value and the reason behind pricing decisions, not merely a description of their outcome. It is why Paid sits at the lowest price each store allows in each region — a volume play, not a margin play — and it is what selects sets over individual verbs for paid verb content.
Recorded here because the value was previously visible only through its instances: the €1.99 / CAD $1.99 figure and the “store floor price in each region” note. An instance does not survive a change of circumstances the way a principle does, so a future pricing decision had nothing to re-derive the reasoning from.
Source: Dries, in conversation, 25 Aug 2026
Paid verb content is sold as sets; per-verb is the fallback, and it does not preserve the goal
Verbs beyond the basic set are intended to be sold as sets — one store SKU per set, unlocking several verbs — not verb by verb. Bundling is the whole point: it is what holds the per-verb cost down, which is the affordability principle applied to this decision.
If sets cannot be sold as a unit, the stated fallback is to sell verbs individually at the lowest price each store allows.
The fallback is a degradation, not an equivalent option, and should not be defaulted into. Unbundling keeps each purchase cheap while multiplying the cost of the catalogue: content that would have been one set SKU becomes N SKUs, none of which can price below its region’s store minimum. If the fallback is ever triggered, the responses that actually serve affordability are a smaller paid surface — more verbs moved into the basic set — or no paid verb content at all. The same catalogue at per-verb pricing is not one of them.
Status is unchanged by any of the above: not built, not live. The only IAP product in the
codebase is remove_ads_lifetime (verified 25 Aug 2026, see “The basic set is 32 verbs” under
The Product).
Source: Dries, in conversation, 25 Aug 2026
A Subscription buys the catalogue, not a snapshot of it
The Subscription tier includes the entire verb catalogue, including sets added after a subscriber joins — not “whatever IAPs happen to exist at the time of purchase”. The catalogue is maintained and extended at Verbomatique’s discretion; the commitment is to access, not to any rate of new content.
This has an implementation consequence. An entitlement flag that grants everything present and future satisfies the commitment; bundling the then-current set SKUs silently breaks it the first time a set is added, and breaks it for existing subscribers rather than visibly at purchase. The roadmap lists “unlock flag vs. bundling” as an open question — it is already decided in one direction by what the tier promises.
Source: Dries, in conversation, 25 Aug 2026
Store Declarations (verified 18 Aug 2026)
App Store Connect
- Category: Education. NOT in Apple’s Kids Category, so none of its restrictions on external links, purchase flows or third-party SDKs apply.
- Age rating: 4+ in 172 countries/regions; 12+ Brazil; All ages Korea; 00+ All ages Vietnam.
Google Play Console
- Target age groups declared: 13–15, 16–17, 18 and above. No band below 13.
- Therefore Google Play Families Policy does NOT apply — no certified-ad-SDK requirement, no mandatory neutral age screen, no Families review burden.
- Ads: “contains ads” declared, “Bevat advertenties” label shown on the listing.
Asymmetry worth remembering: Apple 4+ vs Play 13+. These answer different questions (Apple = content suitability, Play = target audience), so it is not an inconsistency to fix. But it means the App Store surfaces this app to every age band, so the real installed base likely skews younger than the Play declaration implies. Treat “unknown age” as the common case, not the edge case.
Primaire’s Play target-age declaration is wrong, and correcting it now would create a violation
Play Console → App-content → Doelgroep en content, last edited 17 jul 2021. Ticked: 13-15, 16-17, 18+. Unticked: five-and-under, 6-8, 9-12. That is identical to what Secondaire declares — almost certainly never updated when the two products split. The store listing itself says “leerlingen uit het lager onderwijs”.
This is why nothing has ever flagged: with no under-13 band declared, Google Play Families Policy has never applied to Primaire — no certified-ad-SDK requirement, no advertising-ID restriction, no enforced privacy-policy requirement.
Do not tick 9-12 yet. The moment an under-13 band is declared, Families Policy applies — and the shipped Android binary still contains Firebase Analytics. One checkbox would turn an app that is currently not in breach into one that is. Correct sequence: ship the Firebase removal first, then correct the age band, then work through Families Policy (content rating may need redoing, listing may need re-review, and the Advertentie-ID declaration of 23 aug 2023 needs revisiting).
Source: Asana 1217744288172445, comment of 22 Aug 2026 16:04
Apple binds the privacy policy URL to an app version; Play does not
App Store Connect, app 1587030454: the Privacy Policy URL field is read-only. Apple states, on both App Information and App Privacy: “To make changes to the app name, category, or privacy policy, create a new app version. All other changes will be immediately available.”
So a dead privacy-policy URL cannot be fixed on Apple without submitting a build. Play allows it as a standalone metadata change.
Source: Asana 1217744288172445, comment of 22 Aug 2026 16:21
Both Primaire store listings declared a 404 privacy policy — Play fixed 22 Aug, Apple still broken
Both listings declared https://verbomatique.be/privacy-policy/, which returns “Pagina niet
gevonden — 404”. On ASC it appears twice: inside the App Privacy panel and under Information.
- Play — fixed. New URL submitted and under review; Publicatieoverzicht confirms it. Managed publishing is OFF so it goes live automatically once review passes. Note: after the update the Privacybeleid row disappeared from the “Actie ondernomen” tab, which usually means pending publication rather than live — worth re-checking the public listing.
- Apple — still broken and will remain so until a new version ships (see above).
Source: Asana 1217744288172445, comments of 22 Aug 2026 13:02, 15:49 and 16:21
The Data Safety asymmetry: Android’s declaration is false, Apple’s is true
Play Data Safety for Primaire states “Geen gegevens gedeeld met derden” and “Geen gegevens verzameld”, while the shipped Android binary contains Firebase Analytics (confirmed in the merged release manifest from main: FirebaseInitProvider, AppMeasurement services, INTERNET, ACCESS_NETWORK_STATE, install-referrer permission). Play’s scanner detects SDKs by presence in the binary, not by whether they initialise.
Apple’s “Data Not Collected” is accurate — the SDK audit found no Firebase on iOS at all (Podfile.lock has only Flutter, integration_test and shared_preferences_foundation; no GoogleService-Info.plist). The exposure on the Apple side is a broken link, not a false statement. The Firebase problem is Android-only; do not over-scope the fix.
The Firebase removal is what makes the existing Play declaration true — it is not a tidy-up. Currently the declaration describes the app that was intended; the removal makes the binary match it.
Source: Asana 1217744288172445, comments of 22 Aug 2026 13:02 and 13:09
Store account identity and IDs (verified 22 Aug 2026)
- Google Play — developer shown as “Team Wabi Sabi” (missing the B.V.). Account flagged PERSOONLIJK ACCOUNT, Account-ID 5658320062707344136. If a personal account constrains the developer name, converting account type is substantial work with verification requirements, not a field edit.
- App Store Connect — provider is the individual “Dries Bellen”; the address already reads Gingelom, so only the entity is wrong. Changing an Apple account’s legal entity is a support case and may require a D-U-N-S number.
- Apple App IDs: Primaire 1587030454, Secondaire 1611881585.
- Primaire’s live iOS build is version 3.0.1, released 24 April 2022 (27,1 MB, age rating 4+, Education, free). Play’s listing was last updated 28 Aug 2025 (100+ downloads, PEGI 3). Version 3.0.0 was literally “Verbomatique heet nu Verbomatique Primaire”.
- applicationId mismatch:
be.teamwabisabi.verbomatiqueon Android vscom.teamwabisabi.verbomatiqueon iOS. Pre-existing, permanent, worth knowing before store work.
Before anything else on the entity question: check whether Team Wabi Sabi B.V. already has a D-U-N-S number. It is free, takes up to about two weeks, and is a prerequisite on both platforms — that single step may determine the whole timeline.
Sources: Asana 1217744288172445, comments of 22 Aug 2026 13:09 and 16:12; Asana 1217746710233128 (created 22 Aug 2026 16:15)
Ads SDK & Consent State (as of 24 Aug 2026)
Status: built, committed, not yet released. Branch consent/ump-flow, five commits ahead of main (verified 25 Aug 2026). Not merged, not shipped. Everything in this section — ConsentManager, ConsentApi, the two-flag split and canServeAds — exists only on this branch; on main and on the harness branches there is no consent flag at all and the single knob is AdsController.isAdvertisingEnabled via --dart-define=DISABLE_ADS=true. Cite this section’s invariants only for work on this branch.
Architecture as built
- ConsentManager (
lib/consent/consent_manager.dart) — ChangeNotifier, provider-injected viaChangeNotifierProvider.value, created inmain()beforerunApp. Deliberately not a singleton; matches how AdsController, PurchaseService and SettingsController are wired. - ConsentApi (
lib/consent/consent_api.dart) — thin seam over the UMP SDK’s static entry points so tests can inject a fake.DefaultConsentApiholds the realConsentInformationandConsentFormcalls. - Two-phase startup:
kickoffConsentInfoUpdatefires frommain()without await, sorunAppis never blocked.resolveConsentWithFormruns from SplashScreenWidget viaaddPostFrameCallbackand joins the in-flight call through a Completer.
The two-flag split — do not collapse these
isAdvertisingEnabled= entitlement. Written only atAdsControllerconstruction and in the purchase listener, both inmain(). Nothing else writes it._consentAllowsAds= consent gate, written only viaupdateConsentGatefrom theConsentManagerlistener.canServeAds= the AND of both. Every ad-serving decision readscanServeAds; zero ad-serving reads ofisAdvertisingEnabledremain.- Why it matters: merging these into one boolean would let a purchase-status re-evaluation silently resume ads for a user who withdrew consent.
- Verify with
grep -rn "isAdvertisingEnabled" lib/. The authoritative version of this invariant lives in the repo’sCLAUDE.md— if the two ever disagree,CLAUDE.mdwins, because it is versioned alongside the code.
Timeouts
- Info update: 5 seconds. On timeout or failure, falls through to the SDK’s cached
canRequestAdsvalue. Deliberate — a returning user who consented last session keeps ads on a slow connection. - Form load: 10 seconds on the load step only. Display is unbounded, because the user has to read and respond. Two-step
loadConsentForm+form.show, not the combined API. - Known trade-off: a first-launch user on a captive portal gets no ads that session regardless of jurisdiction, including in Canada where consent was never required. Fail-closed is correct here.
ATT
iOS only. Fires inside resolveConsentWithForm after the UMP form is dismissed and before notifyListeners. Called from addPostFrameCallback so the window is guaranteed visible, which the system prompt requires.
Revocation and re-grant
Revoking from Settings tears down rendered banners (Consumer-based teardown in the route widgets), not just a flag flip. Re-granting re-initialises without an app restart. Both paths covered by widget tests.
advertising_id
Removed entirely. Both AdvertisingId.id(true) call sites discarded their return value and nothing in the codebase consumed the advertising ID, so it was dead code as well as pre-consent GDPR exposure. Package dropped from pubspec.
Tests
178 before this work, 208 after. New files:
test/consent/consent_manager_test.dart— four consent states, both timeout paths, revoke/re-grant, privacy-options statustest/consent/ads_controller_test.dart— flag isolation, listener semanticstest/consent/banner_revocation_test.dart— rendered banner disappears on revocation
flutter analyze went from 10 issues to 9; all remaining are pre-existing in untouched files.
ATT sits outside the ConsentApi test seam (24 Aug 2026)
ConsentManager._requestATT() calls the plugin directly, bypassing the
ConsentApi seam. ATT is therefore unreachable by all 208 tests — injecting
the existing fake does not suppress the native prompt. Demonstrated 24 Aug when a
screenshot run popped the real iOS dialog despite the fake being in place.
Compounds with the known missing try-catch: the one path that can silently kill iOS ad serving for a whole session is also the one path outside the test seam.
Also: earlier marketing screenshots were clean only because that simulator’s ATT status happened to be already answered. It has since reset. That is direct evidence for why C6 device QA is the release gate.
Outstanding
_requestATThas no try-catch. IfrequestTrackingAuthorizationthrows,notifyListenersnever fires and no ads serve for the session on iOS. Two-line fix, not yet applied.tagForUnderAgeOfConsentis stillfalseat the call site. Blankettruefor nl-BE was decided (see C2); the one-line change is task C5.- No device testing yet. All 208 tests run against fakes; nothing has exercised a real UMP form or a real ATT prompt. That is C6 and it is the release gate.
Belgian digital age of consent is 13, not 16
Belgium took the GDPR Art. 8(1) option to lower it from 16; Belgian DPA decision 31/2020 treats 13 as the threshold. The declared 13–17 audience self-consents, so no parental-consent flow is needed for them. An earlier repo audit assumed 16 and over-scoped the work by days — keep this correction visible.
Residual: the App Store rating is 4+, so under-13s do install, and for those users Belgian parental consent genuinely does apply. That cohort is not currently identifiable.
The Primaire Android build carried a Firebase/AdMob fossil — removed 22 Aug, not yet shipped
The Dart layer and the iOS build are clean. The native Android build was not, and none of it
appeared in pubspec:
firebase-bom:28.2.1andfirebase-analyticsdeclared inandroid/app/build.gradlegoogle-services.jsonpresent — Firebase project verbomatique-7630e, project number 617354334057, containing an API key and OAuth client ID- an orphaned AdMob application ID in the manifest:
com.google.android.gms.ads.APPLICATION_ID = ca-app-pub-9014261072038331~2554378822, with no Mobile Ads SDK behind it — almost certainly a fossil of the original monetisation attempt, the one abandoned precisely because advertising to under-13s is not permissible
Removed on branch remove-firebase-admob (committed 22 Aug, not pushed, not merged, not
released). Merged release manifest before: INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK,
BIND_GET_INSTALL_REFERRER_SERVICE, FirebaseInitProvider, AppMeasurement services,
ComponentDiscoveryService, AdMob APPLICATION_ID. After: only the Flutter self-permission
DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION.
The rule this produced: built and committed is not shipped. Do not publish a policy claiming “fully on-device, no third-party data collection” until the removal is merged and released to both stores. The published Primaire policy handled this correctly — it states the shipped Android build includes the Firebase Analytics library, that the app does not use it, that the config is not activated so it probably does not initialise, that we cannot claim it never collects, and that it is being removed.
A claim that turned out to be false: the earlier audit said an AD_ID permission was
merging into a children’s app. It was not — firebase-bom 28.2.1 predates
play-services-measurement declaring that permission. Do not repeat the AD_ID claim. What the
removal actually strips is INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK, the install-referrer
permission, the Firebase init provider and the measurement services. Play’s SDK scanner would
still have detected Firebase Analytics in the binary.
Source: Asana 1217744288172445, comments of 22 Aug 2026 09:59 and 10:41
Websites
nl-BE: secondaire.verbomatique.be
- Hosting: Combell (self-hosted WordPress)
- Theme/builder: Kolibrie/Colibri (Customizer-based). Free tier — Shortcode and Subscribe Form components are PRO-locked.
- Editor quirks:
- “Bewerken in standaardeditor” hangs/freezes — do NOT use. Always use “Bewerken in Colibri” (opens Customizer).
- Inline text editing: single-click to select block → double-click for word-select → Cmd+A to select all → type replacement. Works for pre-existing elements.
- Freshly-added “Component” elements (dragged in) are NOT editable in the same session. Pre-built “Blocks” (clicked from the Blocks tab) ARE immediately editable. Use Blocks, not Components, when adding new content.
- The Customizer’s preview iframe is cross-origin — JS can’t access it directly.
- Always publish immediately after editing. Navigating away triggers a native “discard changes?” dialog that can wipe unsaved work. Colibri’s autosave sometimes recovers drafts.
- Structure: Header (Navigation + Hero [global]) / Content (page-specific blocks) / Footer (Copyright). Front Page has its own dedicated template that overrides the global Header hero.
- Site-wide hero: “Franse vervoegingen. Zonder ruzie.” (updated Aug 2026, was “Herexamen Frans?”)
- Pages: Front Page, Cookiebeleid (EU), Herexamen landing page, Hoe werkt, Hoeveel kost (pricing FAQ), Over, Wat kan er beter, Wat mag je verwachten
- Signup form: Brevo shortcode
[sibwp_form id=2]embedded on homepage via Colibri block, with envelope icon and “BLIJF OP DE HOOGTE” heading. Ouder/Leerkracht radio buttons. - Plugins: Brevo, Forminator, Defender, Complianz, Colibri, Contact Form 7, consentmanager
en-CA: verbomatique.com
- Hosting: WordPress.com Business plan
- Editor: WordPress Site Editor (Block editor / Gutenberg). Code editor via Options menu → Code editor, or Cmd+Shift+Alt+M.
- Template: Blog Home (single-page site, no separate pages except Cookie Policy)
- Current revision: 35 (as of 19 Aug 2026)
- Signup form: Brevo shortcode
[sibwp_form id=2], different form from nl-BE. Fields: First name, Last name, You are (Parent/Teacher/Other), Email. Linked to “Canada (English) Subscribers” list. - Plugins: Site Kit (GA4 + Search Console), Brevo, MailPoet (unconfigured), Complianz, Jetpack, WPCode, Code Snippets
- Site Kit Analytics config: Property 548294070, Measurement ID
G-7H79V4X8Y7, Google TagGT-MBNVS8CR. “Exclude Analytics: All logged-in users” is ON (by design — means you can’t test tracking while logged in).
WordPress.com MCP connector: usable for content edits since 24 Aug 2026. Scopes were raised to Read 25 of 38 and Write 62 of 90, with Content Authoring, Site Editing and Site Editor Context enabled for write. See Website Editing for the scoping model and how to reverse it.
Previously, until 24 Aug 2026: every content operation was disabled in MCP settings (pages., posts., page-sections., patterns., media., comments., tags., categories., content.search), and only site-editor context reads worked. That is why older notes in this file say all content edits must go through Claude in Chrome. Do not act on that instruction.
All five vb-screenshot-* assets are live on the homepage (verified 24 Aug 2026 by fetching the public page):
| File | Where it renders |
|---|---|
vb-screenshot-practice.png |
hero, under the store badges |
vb-screenshot-verb-select.png |
step 01, “Set up the session” |
vb-screenshot-practice-1.png |
step 02, “Practice with hints” |
vb-screenshot-evaluate.png |
step 03, “Test without them” |
vb-screenshot-results.png |
step 04, “See the score” |
Do not delete or replace any of them. Steps 02–04 are the three blocks already in an invalid state in the editor. practice and practice-1 are not duplicates — they are different screens in different slots. The 1732×2510 versions uploaded under -native names are bezelled captures — the output of add_bezel.py ipad_mini — added alongside rather than replacing. 1732×2510 is not a device capture resolution. The iPad mini panel captures at 1488×2266; the bezel wrap produces the larger canvas. Media Library uploads are publicly reachable by URL.
Where the confusion comes from, and why the filename cannot be trusted: the harness README uses the word “native” for both things — “Native frame 1488×2266” in one place, “the full native bezelled canvas, 1732×2510” in another. In the harness, -native actually marks BEZEL_WIDTH=0, which skips the downscale step, so it means un-resized, not un-bezelled. 1732 is also the ceiling: requesting more upscales by interpolation. Read -native as “full-size bezelled”, never as a capture spec. Verified 26 Aug 2026 against scripts/README-screenshots.md on harness/screenshot-pipeline. See “There are two screenshot harnesses, on different platforms, for different locales” under Technical Patterns.
New page: /french-homework (post 173). Published 24 Aug 2026, verified anonymously — HTTP 200, form present, store badges present, indexable. Renders with header, logo, tagline and footer; the page title renders as the H1. It is the destination URL all three Blaze campaigns point at.
Superseded 24 Aug 2026. This previously read “Draft as of 24 Aug … Outstanding: store badge images (currently text buttons), one screenshot block, and a live form submission test.” The page was published later the same day and the text buttons were replaced with real badges. Whether the remaining screenshot block was added is not verified. (Asana 1217802736470180, 24 Aug 15:37; Asana 1217801744397059, 24 Aug 19:41.)
Footer is theme placeholder — Product / Company / Resources columns each contain only “Cookie Policy (EU)”. Live on the homepage too.
Three invalid blocks in the “Practice, then prove it” section. Steps 02/03/04 (the tablet-mockup image blocks) show “Block contains unexpected or invalid content / Attempt recovery” in the editor. Confirmed pre-existing as of 18 Aug 2026. The public front end renders them correctly, so this is editor-side only. Risk: editing that section visually may let Gutenberg “recover” them and rewrite their markup. Use the code editor if it must change.
verbomatique.com and verbomatique.be are architecturally different, and that decides how each is edited
The 23 Aug conclusion that “the WordPress REST API does not work” was correct for verbomatique.be and wrongly generalised to verbomatique.com.
- verbomatique.com runs the block theme iotix, with its homepage in a block
template (
iotix//home) that is fully exposed over REST. The homepage is not under Pages —show_on_frontis “posts” andpage_on_frontis 0, so looking under Pages finds nothing and looks like missing content. - verbomatique.be runs Colibri, with content in theme mods, which REST does not expose. Its pages really are 144–172 character stubs.
Three edit routes, one per situation: MCP connector (preferred, .com), REST via browser JS (fallback, .com), Customizer (only option, .be).
Known fingerprint for iotix//home: 53 opening block delimiters, 52 closing, 4 store-badge
links. The 53/52 mismatch is pre-existing — the three invalid blocks in “Practice, then
prove it” — not damage, and not to be fixed in passing.
Source: Asana 1217790212813686 (created 24 Aug 2026 09:03), recording the rewrite of project-knowledge-website-editing.md
The live vb-screenshot-verb-select.png does not show the verb it claims to
Verified 25 Aug 2026 by reading the app source. VerbSelectionmodeSelectionRoute’s
selectForTesting helper updated the selection indices and the highlight colour but never
scrolled the four ListWheelScrollView controllers. The chosen item therefore stayed
off-viewport and was never built, so every screenshot taken through that helper captured
default wheel positions, not the selection.
This asset renders on the verbomatique.com homepage under step 01, “Set up the session”. The step caption describes choosing a verb; the image does not show one chosen.
Fixed in the app on branch harness/screenshot-pipeline (the helper now jumps each wheel,
tense last, because the mode wheel’s onChanged resets the tense wheel). The live asset
still needs regenerating and swapping — and the standing instruction not to delete or
replace the five vb-screenshot-* assets applies, so treat that as a deliberate swap rather
than a routine upload.
Source: Claude Code session of 25 Aug 2026 reading lib/routes/verb_selectionmode_selection_route.dart; asset inventory verified 24 Aug 2026
Website Editing
Consolidated 24 Aug 2026. Replaces all prior scattered notes on editing the two sites.
The two sites are not the same problem, and the difference is the theme. verbomatique.com runs a block theme: its homepage lives in a block template that the WordPress REST API exposes in full, so atomic writes work. secondaire.verbomatique.be runs Colibri, which stores page content in theme mods / Customizer data that REST does not expose at all. A finding about one site says nothing about the other. This distinction was collapsed on 23 Aug and cost a working route.
en-CA: verbomatique.com — Route 1, REST API (preferred, atomic)
This works. A live copy edit was made this way on 24 Aug 2026 in a single atomic write. Supersedes the 23 Aug conclusion that REST was unusable for this project — that finding was specific to Colibri and was wrongly generalised.
INCOMPLETE — fill this in. The operational detail was never captured in any surviving document: the endpoint used, whether it authenticates by browser-session nonce (
X-WP-Nonce) or by Application Password, the read-modify-write shape, and how the write was verified. The auth question is the one that matters: a nonce means REST is only a faster path inside a logged-in Chrome session; an Application Password means content editing on this site no longer depends on Chrome being alive at all. Cheap to re-derive — one authenticated read against the block template settles it.The wrong generalisation is also recorded in Asana task
1217542444995927, under a heading=== ROUTE RULED OUT ===, which states flatly that REST “does not work” with no site qualifier. Correcting this file does not reach it.
en-CA: verbomatique.com — Route 2, Site Editor code editor (fallback)
Editing template text via wp.data.dispatch('core/block-editor').updateBlockAttributes() does not work on this site. The block store accepts the change (a later getBlock() returns the new content) but wp.blocks.serialize() keeps emitting the ORIGINAL markup, so nothing reaches the save payload. Replacing the block wholesale with createBlock() + replaceBlocks() fails identically. The block reports isValid: true with a populated originalContent, and serialization prefers originalContent.
What works: open the code editor (Cmd+Shift+Alt+M in the Site Editor, or Options ⋮ → Code editor), write to textarea.editor-post-text-editor via Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set, dispatch new Event('input', {bubbles:true}), click “Exit code editor” to force a re-parse, verify with wp.blocks.serialize(wp.data.select('core/block-editor').getBlocks()), then Save.
Verify the saved byte length changed by exactly the expected delta — cheapest proof nothing else moved. (This cost four failed attempts on 18 Aug before the code-editor route was used.)
Still the right route for anything that must be seen rendered before saving, and for pasting new page markup — paste into the code editor, exit it, check nothing shows as invalid, then save.
en-CA: verbomatique.com — Route 3, MCP connector (available since 24 Aug 2026)
Usable since 24 Aug 2026. Read 25 of 38, Write 62 of 90 — Content Authoring, Site Editing and Site Editor Context enabled for write; Account, User Management and Plugin Management deliberately left off. Verified with wpcom-mcp-site-editing -> templates.lookup { slug: "home" } returning { id: "iotix//home", title: "Blog Home", theme: "iotix" }. Try the connector first for content edits on this site.
Previously: every content operation was disabled (pages., posts., page-sections., patterns., media., comments., tags., categories., content.search); only site-editor context reads worked. That is why this route was labelled “not available” and why older notes route everything through Claude in Chrome. The scoping model is account-wide with a per-site exclusion list — see the scoping entry at the foot of this section.
nl-BE: secondaire.verbomatique.be — the Customizer is the only way in
REST authenticates fine (nonce required; cookie alone gives 401) and returns all 8 pages, but the copy is not in them. wat-mag-je-verwachten is 172 characters; hoe-werkt-verbomatique-secondaire is 144. They are stubs. Colibri keeps homepage and page content in theme mods / Customizer data, which REST does not expose. There is no atomic-write route to this site.
WPCode is also not a route. It injects header/footer code; it cannot edit page content. (Floated on 23 Aug, corrected the same session.)
Inline editing technique. For simple pre-existing paragraph and heading blocks: single-click to select block → double-click for word-select → Cmd+A to select all → type replacement.
For a block containing multiple paragraphs or inline bold, Cmd+A is wrong — it flattens the formatting. Use: single-click to select → double-click to enter edit mode → triple-click to select just one paragraph → type.
Two traps, both hit on the first edit of 23 Aug:
- Triple-click swallows the paragraph’s closing line break, so the following line welds onto your sentence. Fix: click at the join and press Return.
- Clicking one character too far left puts the full stop at the START of the next line instead of the end of yours. Invisible at normal zoom — zoom in and check.
Budget four operations per edit, not one. Every edit needs visual verification; the editor gives no reliable feedback. Roughly an hour for fifteen edits, with a human on hand. Not safe to run unattended on a live campaign site.
Other Colibri quirks are listed under Websites → nl-BE (the standaardeditor hang, Blocks-not-Components, the cross-origin preview iframe, publish-immediately).
WordPress.com MCP scoping is an exclusion list, not an allow-list
Settings at wordpress.com/me/preferences/mcp (redirects from /me/mcp).
Before 24 Aug: Read 5 of 38, Write 1 of 90 — effectively everything disabled, which is why the 24 Aug landing-page edit had to go through a browser session instead of the connector. After: Read 25 of 38, Write 62 of 90. Enabled for write: Content Authoring, Site Editing, Site Editor Context. Enabled for read: Content Authoring, Site.
Deliberately left off, both read and write: Account, User Management, Plugin Management. None relate to content work; between them they can change the profile, invite collaborators, alter roles, and install or remove plugins on any site in the account.
The scoping model: “Site exceptions” is an exclusion list. The page states “MCP access is enabled at the account level. Disable it for specific sites here.” Access cannot be scoped to verbomatique.com only — it applies account-wide, and the only control is switching it off per site. No exceptions are currently set, so the default is access-everywhere.
To reverse: same three pages, toggle the groups back off.
Verified working after the change: wpcom-mcp-site-editing → templates.lookup { slug: "home" }
returned { id: "iotix//home", title: "Blog Home", theme: "iotix" }.
Source: Asana 1217790212813686 (created 24 Aug 2026 09:03)
Branding
Design Tokens
--vb-blue: #0072BC (logo frame)
--vb-teal: #0B5443 (headings, dark text, "FRENCH VERB CONJUGATION, SORTED.")
--vb-coral: #E5402A (accent, emotional highlight words, CTA buttons)
--vb-cream: #FAF9F5 (background)
--vb-ink: #16211D (body text)
--vb-line: #E4E1D8 (borders, subtle dividers)
Typography
Inter (or system sans-serif: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, sans-serif). No serif display font.
Logo
Blue robot-face icon. Cropped version saved as vb-logo-mark.png in a previous Claude Code session.
Key taglines
- en-CA: “French conjugation, without the fight.” / “FRENCH VERB CONJUGATION, SORTED.”
- nl-BE: “Franse vervoegingen. Zonder ruzie.” / “Franse werkwoorden. Knal van buiten.”
Ad creative style
Text-forward layout on cream background. No photography. Phone/tablet mockup showing real app screenshot on the right. Bold headline with coral accent on key emotional phrase. App Store + Google Play badges. Consistent across Meta, site, and print materials (judo club placemat ad).
Exception as of 24 Aug 2026: Blaze creative diverges deliberately — cream field plus a full-bleed 16–20px teal border, because cream-on-white has no edge against WordPress/Tumblr page backgrounds. Whether the border comes back to Meta creative is undecided. See Advertising → Blaze creative background.
REBRAND decided 22 Aug 2026: Secondaire → Verbomatique, Primaire → Verbomatique Junior
Verbomatique Secondaire becomes the flagship, renamed simply Verbomatique. Verbomatique Primaire becomes Verbomatique Junior.
Framing that matters for comms: from a user’s point of view the flagship rename is a shortening, not a replacement — the name they know loses a qualifier.
Why now: rebranding is cheapest when there is least brand equity to migrate — currently a few hundred installs, two small Brevo lists, ~17 en-CA leads, negligible organic search presence, almost no backlinks. Waiting for revenue means doing the expensive version later.
Timing gate: start only after both back-to-school flights end (nl-BE Meta ends 31 Aug; the en-CA work and the Blaze en-CA flight run later — the Reddit September flight was cancelled on 26 Aug 2026 and no longer gates this). Target window late September / early October, before ad spend scales. The reason is operational — store listing changes trigger review, ad creative referencing a shifting name confuses, and mid-flight measurement gets muddied.
What does not change and must not be attempted: app IDs are permanent
(be.teamwabisabi.verbomatique_secondaire stays forever), Apple App ID 1611881585 stays, and
the Play Store URL is built from the package ID so it does not change. Store renames keep the
same ID, so ratings, reviews and install history carry over.
Biggest open decision: domain architecture. Today the root verbomatique.be serves Primaire and the flagship sits on secondaire.verbomatique.be. After the rebrand the flagship should own the root. That inversion drives redirects, Search Console, Meta domain verification and every link ever shared — decide it first.
Source: Asana 1217743999112835 (created 22 Aug 2026 08:57)
Canonical legal identity — decided 22 Aug 2026, applied to all three sites
Team Wabi Sabi B.V. / Cicindriastraat 29, 3890 Gingelom, Belgium. Use this exact form on privacy policies, cookie policies, both store listings and Complianz on all three sites.
Applied and verified live 22 Aug: verbomatique.be (was “Team Wabi Sabi BV” / “3890 Jeuk”, verified corrected on /cookiebeleid-eu/), verbomatique.com (same old form, corrected), secondaire.verbomatique.be (was “TEAM WABI SABI” in block capitals with the address as just “Cicindriastraat 29”, no postcode or town — corrected and verified on /index.php/cookiebeleid-eu/).
Complianz-generated cookie policies embed the identity, so they regenerate when the setting changes.
Note the ASC listing shows Gingelom while Complianz originally said Jeuk — Jeuk is a deelgemeente of Gingelom, so both are arguably correct, but a privacy policy, a store listing and a cookie policy should not disagree on the trader’s address.
The pattern worth recording: three sites carried three different forms of the same company name and one address missing half its content, because each was configured at a different time and nothing propagates between them. The same drift produced two GA4 properties, a dead privacy-policy URL and a stale target-age declaration.
Source: Asana 1217744288172445, comments of 22 Aug 2026 15:25 and 16:12
Email voice — rules set by Dries, 26 Aug 2026
Five editorial rules were given during the welcome-email polish. They are house style for customer-facing copy in both locales, not one-off fixes:
- Explain the product before mentioning the price. The paragraph describing what Verbomatique is comes before the paragraph saying the app is free. Applied to all eight emails.
- Say what we don’t do, not what the reader doesn’t get. “Je krijgt van ons geen wekelijkse nieuwsbrief” reads stingy; “Wij sturen je geen wekelijkse nieuwsbrief” is the same fact told as a commitment. The English follows the same rule — “We won’t send you a weekly newsletter”, not “you won’t get one”.
- Lead an ask with the request, then the mechanism. “Let us know what got in the way. A reply to this email reaches the person who builds the app” — not the reply mechanics first.
- Full sentences, especially to teachers. A fragment used as a heading (“And the question.”) was rejected on the grounds that the audience is English teachers. Headings in teacher-facing copy are complete sentences.
- Dutch: prefer the inversion for a value statement. “Wij vinden betaalbaarheid belangrijk, daarom is de app gratis met advertenties.” And “activeer”, not “haal”, for switching on the ad-free version — you activate a purchase, you do not fetch one.
Source: Dries, in session, 26 Aug 2026
Site Copy Change Log
19 Aug 2026 — verbomatique.com pricing line. Changed “— CAD $1.99, no subscription.” to “— CAD $1.99, paid once.” Blog Home template revision 34 → 35; saved markup exactly 6 characters shorter, no other diff. Verified live on the public page.
Why: the roadmap plans a Subscription tier. “No subscription” sitting beside the price reads as a promise that none will ever exist, which becomes a misleading-pricing problem the day Subscription ships. “Paid once” describes the SKU instead of the roadmap.
Failure mode to remember: the phrase was written by Claude in an earlier session despite that same session’s own notes recording Subscription as Planned. Avoid forward-looking absolutes in pricing copy.
nl-BE needs no equivalent change — the Dutch pricing FAQ reads “Een eenmalige aankoop verwijdert de advertenties, aan €1,99” and never carried a “geen abonnement” clause.
Mailing Lists (Brevo)
Segmentation: Parent/Teacher (Ouder/Leerkracht for nl-BE) × locale. Both signup forms live on both homepages.
nl-BE Forms (secondaire.verbomatique.be Brevo plugin)
inline_form_parents_teachers_nlBE—[sibwp_form id=2]— embedded on homepageHerexamen nl_BE—[sibwp_form id=3]— separate campaign form
en-CA Forms (verbomatique.com Brevo plugin)
Default Form—[sibwp_form id=1]— basicen_CA_LandingPageForm—[sibwp_form id=2]— embedded on homepage, linked to “Canada (English) Subscribers” list
Consent text (both sites, updated Aug 2026): No longer mentions “paid version” or “ad-free version” as upcoming — references “new features, new verb sets, and product news” (en-CA) or “nieuwe functies, nieuwe werkwoordensets en productnieuws” (nl-BE).
Signup sends no email at all (verified 24 Aug 2026, en-CA form id=2)
- Send a confirmation email: No
- Double Opt-In: No
- Redirect after subscription: Yes — to
/thanks(changed 24 Aug 2026 ~15:36, after the rest of this section was verified at 14:50)
A subscriber is redirected to /thanks and receives nothing from the form itself —
Brevo’s form settings still send no confirmation and no double opt-in, and that is unchanged.
But the subscriber is no longer left with nothing, as of 26 Aug 2026. A welcome automation now fires on the contact being added to the list and sends a parent or teacher message within minutes. See “The welcome automations are live” below.
The cold-list-plus-cold-domain risk is spent. The domain was authenticated on 25 Aug and the lists were mailed on 26 Aug — 60 recipients across four backfill campaigns, zero bounces, zero unsubscribes, first opens inside five minutes. The two halves did not land simultaneously, which was the thing to avoid.
Formerly (until the 24 Aug redirect), the inline success message read “Thank you, you have successfully registered !” — the space before the exclamation mark is French typography in en-CA copy.
nl-BE equivalents not inspected.
List naming: Canada (English) Subscribers (id 3, en-CA) and België (Nederlands) Subscribers (id 2, nl-BE). The nl-BE list was called Your first list — Brevo’s default name — until it was renamed; verified renamed 26 Aug 2026.
Brevo substituted an unmonitored sender domain — FIXED 25 Aug 2026, and the reply half was never true
Resolved 25 Aug 2026 — campaigns now send as Verbomatique <hello@news.verbomatique.com>.
See “The sending identity is now news.verbomatique.com” below for the records and the
before/after measurements.
The reply claim in this section was wrong. Replies were never lost. The unauthenticated
messages already carried Reply-To: decibellen@gmail.com — Brevo adds it automatically when it
substitutes the sender — so replies from parents and teachers reached the Gmail inbox all along.
Verified 25 Aug by reading the raw headers of a message sent before any change was made.
The substitution was also not uniform. A single send on 25 Aug produced two different From
headers: decibellen@11688408.brevosend.com for the Gmail recipient, and the unchanged
decibellen@gmail.com — which then failed DMARC — for a non-Gmail one. “Everyone sees
brevosend” was never accurate.
The deliverability half of the original concern was real and is what the fix addressed: mail now passes DKIM, SPF and DMARC aligned to a domain we own, which matters most for whatever turns out to be this list’s first ever campaign.
DNS note, corrected 25 Aug 2026: verbomatique.com DNS is at WordPress.com, but the MCP connector’s DNS abilities are not enabled — the records were added through the dashboard. See “The WordPress.com MCP connector does not include DNS” under Technical Patterns. The .be domain was already fully Brevo-authenticated before any of this work; the note that it was manual and outstanding was stale.
Authentication alone does not fix the reply problem — an explicit Reply-To pointing at a monitored inbox is what does. Note the direction of risk has now inverted: Brevo stops adding Reply-To once the From is authenticated, so it must be set on every campaign by hand.
Source: Asana 1217778600752666 (created 24 Aug 2026 09:43)
Attribute completeness on the en-CA list (read 24 Aug 2026)
Brevo list #3 (Canada (English) Subscribers): 24 contacts, up from 21 on 22 Aug.
- 24 of 24 carry LOCALE — the locale-neutral migration held.
- 2 of 24 carry both CONSENT_MARKETING and AUDIENCE (ids 16 and 27).
- 22 of 24 carry neither.
All four arrivals since the baseline (ids 30–33, landing 23 and 24 Aug) came in with the Meta/Pabbly shape — FIRST_AND_LASTNAME + LOCALE only. At the time, every new lead arrived unusable for Parent/Teacher segmentation and with no consent record stored in the system that actually sends the email; the consent evidence sat in Meta instead.
Superseded on the AUDIENCE half, 26 Aug 2026. The figures above are a 24 Aug snapshot and should be read as history. A complete read of list 3 on 26 Aug 2026 18:03 returned 37 of 37 contacts carrying AUDIENCE — 19 Parent, 17 Teacher, 1 Other — after the backfill reconstructed from Meta’s lead records. The list is now fully segmentable, which is what let the four welcome campaigns split cleanly by audience.
The consent half is unchanged and still a gap. Only 3 of the 37 carry CONSENT_MARKETING (ids 16, 27 and 34 — the website-form signups). For every Meta lead the consent evidence still lives in Meta and not in Brevo.
Whether the en-CA Pabbly action now maps AUDIENCE by itself is not established. The seven contacts that arrived on 26 Aug (ids 67–73) do carry AUDIENCE, but the manual backfill touched every contact on the list, so their AUDIENCE cannot be attributed to Pabbly. Testing that needs a lead arriving after this date and read before anyone touches it.
Bookkeeping correction: the 22 Aug baseline recorded ids 3, 16 and 27 as complete. Id 3 is dries.bellen@wabisabiconsulting.be and the locale migration correctly moved it to list 2 (België) with LOCALE nl-BE. So the real en-CA figure was 2 complete out of 20 genuine leads, and is now 2 out of 24. Nothing was lost — the list never had 3.
CONSENT_NL_BE no longer exists — deleted 18 Aug, confirmed absent from the Brevo schema and from all contacts on 22 Aug. The current field is CONSENT_MARKETING.
Source: Asana 1217542445377430, comments of 23 Aug 2026 08:15 and 24 Aug 2026 07:59
The Belgian list had no working lead route until 25 Aug 2026 — RESOLVED
As it stood on 24 Aug: Brevo list 2 “België (Nederlands) Subscribers” held exactly one contact — Dries Bellen, created 22 July, attributes clean (AUDIENCE=1, CONSENT_MARKETING=true, LOCALE=nl-BE). Contacts added in the week to 24 Aug: 0. Because the weekly export had never completed successfully, there was no lead route into list 2 at all.
Resolved in two moves on 25–26 Aug. The 20 leads stranded on the Meta form were imported by CSV, and the nl-BE Pabbly workflow was built and activated — since proven to be delivering. List 2 holds 23 contacts as of 26 Aug 2026. Do not carry the “exactly one contact” figure forward.
The confounding was bypassed, not resolved. Whether the website form was also regressing was never established, and there is now a working lead-ad route masking the question. If website signups matter separately from lead-ad signups, that needs its own test.
Source: Asana 1217775027491967 (created 24 Aug 2026 09:31)
The two en-CA test contacts are gone — resolved 26 Aug 2026
Two test submissions on 24 Aug went into Canada (English) Subscribers, sent to a + alias. They
are no longer there. A complete read of all 63 account contacts on 26 Aug 2026 18:03 returned no
+-alias address in any list, and list 3 stands at 37.
This matters for the September baseline, which is now measured against 37 real contacts rather than 37 real ones plus two fakes. The four welcome backfill campaigns sent on 26 Aug went to 60 recipients — the two lists’ exact combined membership — so no test address received one.
Source: Brevo contacts_get_contacts (full account read) and lists_get_lists, 26 Aug 2026.
The sending identity is now news.verbomatique.com (25 Aug 2026)
Campaigns send as Verbomatique <hello@news.verbomatique.com>, a Brevo-authenticated
subdomain of verbomatique.com. Before this, Brevo sent as decibellen@gmail.com, which cannot
be authenticated because Google owns gmail.com.
Four records live in the verbomatique.com zone at WordPress.com, all added 25 Aug 2026:
| Type | Name | Value |
|---|---|---|
| TXT | news |
brevo-code:ac1b5c6fe7ef6c9afbd0e39bb6987bec |
| CNAME | brevo1._domainkey.news |
b1.news-verbomatique-com.dkim.brevo.com |
| CNAME | brevo2._domainkey.news |
b2.news-verbomatique-com.dkim.brevo.com |
| TXT | _dmarc.news |
v=DMARC1; p=none; rua=mailto:rua@dmarc.brevo.com |
The brevo-code value is account-level, not domain-level — verbomatique.be carries the
identical string. Do not treat a matching brevo-code as evidence that the right domain was
verified.
Measured on an identical message body before and after, sent from a draft campaign that was never sent to a list:
- mail-tester 3.6/10 → 7.7/10; “You’re not fully authenticated” (−3) → “You’re properly authenticated”.
- Gmail’s
Authentication-Resultsafter:dkim=pass header.i=@news.verbomatique.com header.s=brevo2,spf=pass,dmarc=pass (p=NONE) header.from=news.verbomatique.com.
The residual −1.8 on the after-test is SpamAssassin reacting to a deliberately short test body
(HTML_IMAGE_ONLY_24, HTML_SHORT_LINK_IMG_3), not to the sending configuration.
Brevo’s optional branded subdomain (own-domain tracking links in place of sendibt3.com) was deliberately not set up, to keep the change surface small. Both news.verbomatique.com and verbomatique.be currently read “Not branded” in Brevo.
Source: Asana 1217778600752666, comment of 25 Aug 2026; DNS verified against 8.8.8.8, 1.1.1.1 and 9.9.9.9; header verdicts read from the raw delivered messages.
Reply-To is per-campaign in Brevo, and nothing sets it for you any more
Brevo has no account-level or sender-level Reply-To. It is a per-campaign toggle: campaign → Additional settings → Sending and Tracking → “Use a different Reply-to address”.
While the From was unauthenticated, Brevo added Reply-To: decibellen@gmail.com automatically
as part of substituting the sender. Now that the From is authenticated, it does not. Every
future campaign must set Reply-To explicitly. Forget it once and replies go to
hello@news.verbomatique.com, which has no mailbox and hard-bounces.
This is the single most forgettable step in the whole setup, and its failure is silent to the sender.
Source: Asana 1217778600752666, comment of 25 Aug 2026.
news.verbomatique.com accepts no mail — the From address hard-bounces
The subdomain has no MX, no A and no AAAA record. Mail addressed to
hello@news.verbomatique.com fails with 5.4.3, observed directly from Google’s
mailer-daemon.
Normal replies are unaffected: Gmail honours Reply-To and pre-fills decibellen@gmail.com in
its reply box, verified 25 Aug by replying and confirming arrival. What does bounce is anything
that answers the From instead — some auto-responders and out-of-office systems, some
forwarding rules, clients that ignore Reply-To, and any recipient typing the address by hand.
There is also no route for postmaster@ or abuse@ on that subdomain.
Undecided: whether to add an MX or forwarder, move the sending address to a domain that already receives mail, or accept the gap. Tracked in Asana 1217814220575319.
Source: Asana 1217814220575319 (created 25 Aug 2026), which records the bounce verbatim.
verbomatique.be was already fully Brevo-authenticated before any of this
Read from live DNS on 25 Aug 2026, before any change was made: verbomatique.be already carried
the brevo-code TXT at the apex, brevo1._domainkey and brevo2._domainkey CNAMEs into
dkim.brevo.com, _dmarc with rua=mailto:rua@dmarc.brevo.com, and mail.verbomatique.be
as SPF plus a CNAME to mail-verbomatique-be.brand.brevosend.com. Brevo’s Domains tab showed
it as Authenticated.
No .be work was needed and none was done. Nobody had recorded that it was already set up, which is why the standing note said the .be side was manual and outstanding.
However: no sender exists on verbomatique.be. An authenticated domain with no sender on it changes nothing about what recipients see — which is exactly the trap the gmail.com sender fell into. Authenticating a domain and switching the From are two separate steps.
Source: live DNS read 25 Aug 2026; Brevo Senders, Domains & Dedicated IPs → Domains.
The Brevo MCP connector is read-only for contacts
The connector exposes contacts_get_contacts, contacts_get_contact_info,
contacts_get_contact_stats, contact_import_export_get_contacts_from_list, lists_get_lists,
lists_get_list, segments_get_segments, attributes_get_attributes, accounts_get_account,
senders_get_senders, and creation tools for campaigns, SMS campaigns and templates. There
is no create-contact, no update-contact and no import.
So any contact write — a manual import, a backfill, a field correction — has to happen in the Brevo web UI or through an integration like Pabbly. This is not a scoping choice that can be raised in a settings page, as with the WordPress.com connector; the tools do not exist.
The consequence that matters: any scheduled or unattended task whose success condition is “contacts land in Brevo” cannot succeed on the connector alone. A task written as “export → import” is not slow or fragile, it is impossible.
Source: connector tool inventory, re-read 26 Aug 2026 — unchanged since 25 Aug
The AUDIENCE attribute’s enumeration
AUDIENCE is a Brevo category attribute: 1 = Parent, 2 = Teacher, 3 = Other. The nl-BE
form emits ouder / leerkracht — lowercase, confirmed in the live Pabbly response — so
neither the label nor the id matches without a translation step. CONSENT_MARKETING is a
boolean, LOCALE and FIRST_AND_LASTNAME are text, and DOUBLE_OPT-IN is a category
(1 = Yes, 2 = No).
Source: Brevo attributes_get_attributes and the live Pabbly trigger response, both re-read
26 Aug 2026
Pabbly silently drops AUDIENCE on every nl-BE lead — found 26 Aug 2026
The nl-BE Pabbly action has AUDIENCE set to None deliberately, because ouder / leerkracht
do not map onto Brevo’s 1 / 2 without translation. The cost of that decision is now visible
in the data rather than theoretical.
The nl-BE form does ask the question, and Meta does capture the answer: lead
kirsten.fotovdw@gmail.com, submitted 25 Aug 22:30, reads Ik ben... Ouder in its
Formulierantwoorden panel. The contact Pabbly created from it (Brevo id 66) carries
CONSENT_MARKETING true, LOCALE nl-BE, FIRST_AND_LASTNAME Kirsten — and no AUDIENCE at
all. The answer is collected and then thrown away in transit.
That is the one real gap in an otherwise working route. The 20 leads imported by CSV on 25 Aug
all carry AUDIENCE (14 Parent, 6 Teacher, verified by reading them back), so the automated path
is now producing worse data than the manual one it replaced. Fixing it needs a translation
step in Pabbly — a router or a formatter turning ouder → 1 and leerkracht → 2 — not a
change to the form.
Source: Meta Leads Centrum and Brevo contacts_get_contacts, both read 26 Aug 2026
Three contacts sit outside every list
Ids 42, 43 and 44 — decibellen@gmail.com and two srv1.mail-tester.com addresses, all created
25 Aug during the sender-authentication work — have listIds: []. They belong to no list, so
they are invisible in every list view while still counting against the account. Harmless, but
they will confuse the next person who compares an account total against the sum of the lists.
Account total is 63 contacts as of 26 Aug 2026 18:03 (list 3 en-CA: 37 unique; list 2 nl-BE: 23; plus the three orphans). It was 36 on 25 Aug before the nl-BE backfill, and was read as 65 earlier on 26 Aug — do not carry either older figure forward.
The drop from 65 to 63 is the two en-CA test contacts leaving the list: a complete account read
of all 63 contacts contains no +-alias address at all, and the id sequence has a gap at 35 and
36, between the 24 Aug 14:21 arrival and the 25 Aug 02:14 one — the exact window the test
submissions were made in. That they were deleted is the obvious reading of those two facts, but
the deletion itself was not observed.
Source: Brevo contacts_get_contacts and lists_get_lists, read 26 Aug 2026
The welcome automations are live, and the whole list has now been mailed (26 Aug 2026)
Both Brevo automations are Active as of 26 Aug 2026 17:33. Verified by reading the Automations → Workflows listing directly: “Welcome · nl-BE” (#2) and “Welcome · en-CA” (#1), both showing Active, with the tab counters reading All 2 · Active 2 · Paused 0 · Inactive 0.
Each automation is triggered by a contact being added to its locale’s list and branches on AUDIENCE:
- Branch A — condition
AUDIENCE is equal to [Teacher]→ the teacher message. - Branch B —
Does not match any conditions→ the parent/other message.
Branch B is a catch-all, not a Parent test. A contact with no AUDIENCE at all therefore receives the parent message rather than falling out of the automation. That is deliberate and worth knowing, because the nl-BE Pabbly route still drops AUDIENCE (see “Pabbly silently drops AUDIENCE on every nl-BE lead”) — those contacts get a welcome, just the parent one.
“Re-entry after exit” was left off on both, so a contact re-added to a list will not be mailed twice.
Source: Brevo Automations → Workflows, read 26 Aug 2026 18:05
The backfill: four one-off campaigns to the 60 pre-existing contacts
The automations only fire on new additions, so the 60 contacts already on the two lists would never have received a welcome. Four classic campaigns were built and sent on 26 Aug 2026 to cover them, mirroring the automation branches one-for-one so nobody got copy written for someone else:
| # | Campaign | List | Filter | Sent | Time |
|---|---|---|---|---|---|
| 14 | Welcome backfill · nl-BE · Leerkracht | 2 | AUDIENCE = Teacher | 7 | 17:43 |
| 15 | Welcome backfill · nl-BE · Ouder en Andere | 2 | AUDIENCE = Other, Parent | 16 | 17:48 |
| 16 | Welcome backfill · en-CA · Teacher | 3 | AUDIENCE = Teacher | 17 | 17:53 |
| 17 | Welcome backfill · en-CA · Parent and Other | 3 | AUDIENCE = Other, Parent | 20 | 18:01 |
60 sent in total, 7 + 16 + 17 + 20, which is exactly the two lists’ combined membership — so the coverage is provably complete, not approximately complete. Final delivery across all four, read once #17 finished at 18:09: 59 of 60 delivered, zero hard bounces, one soft bounce (on #17), zero unsubscriptions, zero complaints. First opens arrived within five minutes; open rates settled at 12.5% (#15), 17.65% (#16) and 15.79% (#17), with one click on #17.
This is the first email either list has ever received. The cold-list-plus-cold-domain risk recorded on 24 Aug was real but is now spent: the domain was authenticated on 25 Aug first, and the first send went out to 60 opted-in contacts a day later.
Source: Brevo email_campaign_management_get_email_campaigns with globalStats, read 26 Aug 2026
18:03; recipient counts cross-checked against the Recipients panel before each send
Brevo campaigns do not need segments — filter the list by attribute instead
There are no segments in this account (segments_get_segments returns {}), and none are
needed. A campaign’s Recipients panel has Advanced options → Filter recipients → Add a
condition, which filters a chosen list by contact attribute at send time. All four backfill
campaigns above used it, e.g. AUDIENCE is equal to [Other] [Parent] on list 3 → 20
recipients.
The filter UI resolves the AUDIENCE category ids to their labels, so it offers Other / Parent / Teacher rather than 3 / 1 / 2. Do not go looking for the numbers there.
A quirk in that panel: the condition’s attribute search box does not filter on a value that is pasted or typed all at once — the React input event never fires. Press BackSpace and retype the last character and the list filters correctly.
Source: built and sent four campaigns through this panel, 26 Aug 2026; segments_get_segments
read the same day
Every campaign defaults to an unauthenticated sender and the wrong reply-to
A new campaign does not inherit the authenticated sender. It opens with “Team Wabi Sabi BV · decibellen@gmail.com” preselected and shows the warning “Your domain is not authenticated. To ensure delivery, we replace your domain with [@brevosend.com].” It must be switched to Verbomatique · hello@news.verbomatique.com by hand, on every campaign. The warning disappears once it is, which is the check that it took.
The same is true of reply-to, which defaults to decibellen@gmail.com and has to be set per
campaign under Additional settings → Sending and Tracking. The locale split was applied to
all four backfill campaigns: feedback@verbomatique.be on #14/#15,
feedback@verbomatique.com on #16/#17.
Two defaults, both wrong, both silent, on every single campaign. This is the same class of trap as “Reply-To is per-campaign in Brevo” recorded on 25 Aug — the sender half had not been written down.
Source: observed on all four campaigns as they were created, 26 Aug 2026; final values confirmed
from the API, which returned sender.email hello@news.verbomatique.com and the expected
replyTo for each
The welcome email exists as eight separate copies, not two
The same four emails exist twice over, and editing one does not touch the other. Any copy change has to be applied to both members of each pair:
| Locale · audience | Template | Automation-step message |
|---|---|---|
| en-CA · Parent and Other | 6 | 11 (Automation #1, step #4) |
| en-CA · Teacher | 7 | 10 (Automation #1, step #3) |
| nl-BE · Ouder en Andere | 8 | 13 (Automation #2, step #5) |
| nl-BE · Leerkracht | 9 | 12 (Automation #2, step #3) |
The templates are what the one-off campaigns use; the automation-step messages are what the
automations send. Both are reachable through templates_get_smtp_template, which is the only
reliable way to confirm an edit actually saved.
Source: Brevo templates_get_smtp_templates and the two automation editors, 26 Aug 2026
Meta Lead Ads → Brevo sync (Pabbly Connect)
Site signup forms write to Brevo directly via the WordPress plugin. Meta Lead Ad forms do not — they need a bridge, and that bridge is Pabbly Connect (free tier, 100 tasks/month). Brevo has no usable native Facebook Lead Ads connector; Pabbly was the fallback after that route failed.
Workflows
| Workflow | Status | Trigger (page / form) | Brevo action |
|---|---|---|---|
...Brevo." (en-CA) |
Live since Aug 10, 2026 | Verbomatique.com / Verbomatique.com - Newsleter Signup Form-copy |
Create or Update Contact → Canada (English) Subscribers |
...Brevo (nl-BE) |
Trigger only, action never saved | Verbomatique secondaire / VS nl-BE Back to School - Nieuwsbrief |
— |
en-CA action config (the working reference)
- Email ←
res2 email, FIRST AND LASTNAME ←res2 full_name - List: Canada (English) Subscribers · Update Enabled: True
- Email blacklisted: False · SMS blacklisted: False
- AUDIENCE RADIO NL, CONSENT NL BE, AUDIENCE NL, LOCALE, LASTNAME, FIRSTNAME: all unset
Gotchas — read before touching this again
- Field mapping requires a captured test response. With none, Pabbly’s
/mapper says “No fields available in this step”, Email is mandatory, so the action step silently fails to save and the canvas reverts to trigger-only. There is no manual-token workaround worth trusting. - The nl-BE page has no associated Meta developer app, so
developers.facebook.com/tools/lead-ads-testingrefuses to create a test lead for it. The en-CA page does have one, which is why en-CA was buildable and nl-BE is not. Do not re-attempt the testing-tool route for nl-BE until the app exists. - The App Dashboard shows no “Create App” button, which almost certainly means the Facebook account has never completed developer registration.
- Ads Manager’s lead-form preview can’t substitute — a draft campaign has no ad to preview.
- The blacklist dropdowns default to displaying True on a freshly-added action step. True means the contact is blocked from receiving email. Always set both to False before saving. (Checked Aug 16: en-CA is correctly False; no leads were ever blacklisted.)
- Polling triggers mark leads as seen. An active workflow with no action will consume incoming leads and write them nowhere — deactivate any half-built workflow.
- Manual “Test Workflow” fetches only the most recent lead(s), not history. It cannot backfill.
- The workflow only captures leads created after it was built. Anything from before Aug 10, 2026 (en-CA) was never synced and needs manual CSV import.
Manual fallback (no Pabbly)
Ads Manager → the Leads ad → Download leads, or Business Suite → All tools → Leads Center. Import into Brevo sorted by the audience column, in one pass per audience value, setting AUDIENCE NL and LOCALE as static values for the whole import. Adequate for campaigns producing leads in the low tens.
How leads actually arrive, and what the mapping drops
Meta leads arrive through Pabbly in a batch at roughly 02:15 CEST. They land with FIRST_AND_LASTNAME + LOCALE only — the static AUDIENCE and CONSENT_MARKETING mappings have never been added to the en-CA action. LOCALE was added and is confirmed working on every live lead since the backfill.
The gap is not Pabbly alone: of the 18 contacts missing both fields on 23 Aug, 13 had the Meta/Pabbly shape and 5 came from the CSV backfill, so the manual import route drops them too.
Known obstacle: Pabbly field mapping requires a captured test response. Without one the mapper reports “No fields available in this step”, Email is mandatory, and the action step silently fails to save, reverting the canvas to trigger-only. There is no manual-token workaround worth trusting. For nl-BE this was recorded as blocked behind generating a test lead, itself blocked behind Facebook developer registration. That was the wrong diagnosis. Pabbly refuses to capture a response unless the workflow is switched ON, and a trigger-only workflow left ON consumes leads — a catch-22 no number of test leads resolves. It was escaped on 25 Aug 2026 by toggling ON, capturing, and toggling OFF within a minute; the nl-BE action is now built, live and delivering. See “The real blocker was a catch-22 in Pabbly, not a missing test lead”. The separate claim that the nl-BE page has no associated Meta developer app was not re-tested and nothing here disproves it.
Source: Asana 1217542445377430, comment of 23 Aug 2026 08:15
The nl-BE workflow is LIVE — and PROVEN DELIVERING as of 26 Aug 2026
When New Lead in Facebook Lead Ads, Create or Update Contact in Brevo (nl-BE) — built 25 Aug,
Active, and since verified to be doing its job. The nl-BE list has a working automated lead
route for the first time since it was created on 17 July.
The whole chain was traced end to end on 26 Aug, rather than trusting Pabbly’s own success message:
- Meta Leads Centrum shows lead
kirsten.fotovdw@gmail.com, Leadformulier-ID 1363649725367766, Verzonden op 25 Aug 2026 22:30,Ik ben... Ouder. - Pabbly’s dashboard shows this workflow at 2 tasks consumed — it was 0 when built.
- Brevo holds contacts 65 and 66, created 26 Aug 02:08,
listIds [2],CONSENT_MARKETING true,LOCALE nl-BE, first name only, no AUDIENCE — which is exactly this action’s saved configuration.
Action config, read live from the saved step on 26 Aug: Email ← res2 e-mailadres ·
FIRST AND LASTNAME ← res2 voornaam · LOCALE nl-BE (static) · CONSENT MARKETING True ·
List(s) België (Nederlands) Subscribers · Update Enabled True · email blacklisted False ·
sms blacklisted False · AUDIENCE None · LASTNAME and SMTP Blacklist Sender empty.
Trigger, same read: Facebook Page Verbomatique secondaire, Lead Ad Form VS nl-BE Back to School - Nieuwsbrief, Response A still captured.
Source: Pabbly Connect, Meta Business Suite and the Brevo API, all read 26 Aug 2026
The real blocker was a catch-22 in Pabbly, not a missing test lead
Pressing Save & Send Test Request on the trigger raises a blocking dialog — “You need to turn ON the workflow to capture the webhook response.” And a workflow turned ON with no action step is precisely what this file warns against, because its poller consumes incoming leads and writes them nowhere. Capturing a response required entering the one state the standing guidance forbids.
The escape, used successfully on 25 Aug: toggle ON → Save & Send Test Request → toggle OFF immediately → build and save the action offline with the captured response in hand → toggle ON for real. Two exposures of well under a minute rather than one lasting the whole build.
The captured response survives the toggle-off — confirmed 26 Aug: the trigger still shows “Response A”, and the action’s Email field still renders its captured sample value. That part is verified. The dialog’s exact wording is carried from the 25 Aug session and was deliberately not re-triggered, since doing so means pressing the button that starts the exposure.
The exposure is also smaller than it first reads: a lead consumed in that window is only marked seen by Pabbly. It remains in Meta for its full 90 days. The worst case is a lead that does not auto-sync, not a lost lead.
Whoever built this on 16 Aug hit that dialog and stopped, which was the right call. What was missing was not a test lead but a safe sequence.
Source: Cowork session of 25 Aug 2026 for the dialog; Pabbly re-read 26 Aug 2026 for the persistence
The Brevo action’s dangerous defaults — one of them undocumented
On a freshly added Brevo “Create or Update Contact” step:
| Field | Ships as | Must be |
|---|---|---|
| Do you want to use email blacklisted? | True | False |
| Do you want to use sms blacklisted? | True | False |
| List(s) | Canada (English) Subscribers | the list you actually want |
The blacklist trap was already recorded. The list default was not. It pre-selects the account’s other list — and saved unexamined, every nl-BE lead would have landed on the en-CA list and been blocked from receiving email, while the workflow reported success.
Verification note: the saved nl-BE values were confirmed correct on 26 Aug (both blacklists False, list = België). The defaults on a fresh step were not re-tested, because reproducing them means adding a new action step to a live workflow. Treat the defaults as asserted from 25 Aug and re-check them on the next workflow anyone builds.
Source: Cowork session of 25 Aug 2026; saved values re-read 26 Aug 2026
The nl-BE trigger payload is Dutch, and has no full name
The three form answers are named in Dutch — a consequence of the form being authored in Dutch,
and a reason the en-CA mapping recipe does not transfer: res2 e-mailadres, res2 voornaam,
res2 ik_ben... (value ouder / leerkracht, lowercase). The rest is ad metadata on the
res1 prefix: created_time, id, ad_id, form_id, ad_name, adset_id, adset_name,
campaign_id, campaign_name, is_organic, platform. res1 form_id reads
1363649725367766, matching the nl-BE form.
There is no full_name. The nl-BE form only ever asked for a first name, so
FIRST_AND_LASTNAME can only be populated with a first name — unlike the en-CA action, which
maps res2 full_name. Any CSV backfill for this list should use first names too, or the list
ends up with two naming conventions and a campaign greeting that reads differently for each half.
The 25 Aug backfill was re-cut to first names for exactly this reason.
Source: Pabbly trigger response panel, read field by field 26 Aug 2026
Free-tier consumption
100 tasks/month. 31 consumed, 69 remaining as of 26 Aug 2026 — 22 on 25 Aug, so the two live workflows are burning roughly ten a day between them at back-to-school volume. The en-CA workflow (built 10 Aug) accounts for 29 of the 31; the nl-BE one for 2. At this rate the ceiling is not close, but it is no longer irrelevant.
Source: Pabbly Connect dashboard, read 26 Aug 2026
The nl-BE form and its lead inventory (read 26 Aug 2026)
VS nl-BE Back to School - Nieuwsbrief, Formulierantwoorden ID 1363649725367766, Actief,
created 16 Aug 2026 20:40. Forms library shows 23 leads, 0 verlopen.
That is 22 real leads plus one test submission. The test lead is worth knowing by sight: it
displays in the table under the name “Dries Bellen”, dated 16 Aug, and only its detail panel
gives it away — email test@meta.com, Verzonden op Zondag 16 augustus 2026 21:36, with
Ik ben... and First name both reading <test lead: dummy data for ...>. Anyone filtering on
the display name will miss it.
Of the 22 real leads, the 20 that existed on 25 Aug were imported to Brevo by CSV — 14 Ouder, 6 Leerkracht, confirmed by reading the AUDIENCE values back off contacts 45–64. The two newest (25 Aug) arrived automatically through Pabbly.
A second, older form also exists — “Verbomatique - Newsletter signup F…”, created 4 Aug 2026, 1 lead. It is not part of the nl-BE back-to-school route and nothing reads from it.
Source: Meta Business Suite forms library and Leadscentrum, read 26 Aug 2026; Brevo API same day
Leads Centrum is the readable route to lead data
Meta Business Suite → Leadscentrum had never been opened before 25 Aug; its first screen was a terms-of-service gate, accepted that day with Dries’ explicit authorisation. Confirmed 26 Aug: the gate is gone and the centre opens straight to data.
Tabelweergave renders every lead on screen, and clicking a row opens a Formulierantwoorden
panel carrying the Leadformulier-ID, the “Verzonden op” timestamp, and every custom answer — so
lead data is readable by a Claude session without a file download. The table paginates at
20; check for a second page before concluding a count. Each lead’s panel also shows its expiry
inline as “Lead verloopt op
The Page-context caveat. /latest/leads_center and /latest/instant_forms open on whichever
Page the account last used, so an empty leads list is far more likely to mean wrong Page context
than zero leads. Pin it: ?business_id=1159242368262681&asset_id=110739691610636. The 25 Aug
note said the unpinned URL lands on jouwsportclub.be specifically; that did not reproduce
on 26 Aug, but the test was worthless — the pinned URL had just been visited, so “last used” was
already Verbomatique. Treat the mechanism as established and the specific wrong Page as an
artefact of that day’s state.
Read-only rules for this surface: moving a lead between pipeline stages is an edit, as are “Lead verwijderen” and “Lead rapporteren”, both of which sit at the bottom of every detail panel.
Source: Meta Business Suite, read 26 Aug 2026
Advertising
Google Ads Account: 795-802-5846
- Linked to GA4 property (verbomatique.com) as of Aug 15, 2026
- Two conversion actions imported:
click→ Outbound click,form_submit→ Sign-up - 22 Aug 2026:
Search-BackToSchool-en_CA-2026switched to optimise for Aanmeldingen (campaign-specific) instead of the account default Paginaweergaven. The underlying action is the GA4form_submitimport from property 548294070. - This did not fix conversion reporting, and cannot.
form_submiteffectively never fires — twice in 28 days, from a single user, against 92 outbound store-badge clicks. A campaign optimising for an event that does not occur will report zero conversions indefinitely. This is not a GA4 plumbing fault; do not send anyone to debug the GA4 import. If a usable conversion signal is wanted here, it has to beclick(outbound), or the form has to start converting. - Note the bid strategy (Klikken maximaliseren) does not consume conversion data, so this affects reporting and attribution, not bidding.
- A
Leads van telefoongesprekkenconversion action still exists although neither site has a phone number. Leftover from old Display/Smart campaigns; flag once for removal, then ignore. - Account currency: EUR · Account language: Dutch (interface shows in Dutch)
- Decimal separator is comma, not period (€6,91 not €6.91)
Campaigns: | Campaign | Status | Budget | Targeting | Dates | |—|—|—|—|—| | Search-Herexamens-nl_BE-2026 | Active | €6/day | Belgium (Brussels), Dutch | Jul 24 – Aug 27, 2026 | | Search-BackToSchool-en_CA-2026 | Active — delivering | €6.91/day | Canada — no Québec exclusion configured, English | Aug 15 – Sep 14, 2026 |
Meta Ads Account: 263136948126381
- Pixel ID: 2835951779840335 (“verbomatique pixel”), linked to verbomatique.com
- Custom event
PlayStoreClickfires on store badge clicks (manually configured, working) - Enhanced Measurement events auto-fire (
PageView,SubscribedButtonClickon clicks) - Account has been running awareness campaign since mid-July 2026
Current Meta ads (running):
- Traffic Ad: “French conjugation, without the fight.” — €0.19/landing page view, 4,120 impressions
- Lead Ad: “French verb drills your teen won’t fight you on” — €3.81/lead, 11 leads
nl-BE back-to-school campaigns — LIVE since 19 Aug 2026
Both published 19 Aug, running 20–31 Aug 2026. Flemish schools start Tue 1 Sept.
| Campaign | Creative | Budget | Copy |
|---|---|---|---|
| VS nl-BE Back to School — Leads | Score report, 4/6, two visible mistakes with corrections | Campaign level | “De app houdt de fouten bij. Niet jij.” · Kop: “Franse werkwoorden die nog niet goed zitten” · CTA Registreren |
| VS nl-BE Back to School — Verkeer | Afdekken screen, three pronouns answered, three blank | €4.00/day (raised from €1.00) | “Franse vervoegingen. Zonder ruzie.” · Kop: “Huiswerk Frans begint weer. De ruzies hoeven niet.” · CTA Download |
The €4/day was forced, not chosen. On publish Meta warned “Je campagne behaalt mogelijk 0 resultaten met je huidige budget” and named €4.00/day as the minimum for ~1 result/day. At €1 the Verkeer ad set likely would not have delivered. ~€48 over the flight.
Advantage+ creative enhancements: four OFF, one ON. Visuele retouchering, Overlays toevoegen, Tekstverbeteringen and Animatie toevoegen all default to ON and would have altered a deliberately composed 1080×1080 layout — Meta adding its own text over an ad that already has a headline. Turned off. Muziek left on. Campagnescore dropped 81 → 67 as a result; that is Meta’s optimisation score, not a quality judgement, and the trade was made knowingly. Meta remembers these per account — the second ad already had them off.
Creative production: screenshots captured from a real Android emulator at 1080×2424, nl-BE locale, verb aller, via an integration-test harness. Composed to exactly 1080×1080. An earlier Verkeer creative was rejected by Meta as “Media niet breed genoeg” (under 500px wide); resolved. Only remaining warning on both ads is the harmless Audience Network Rewarded Video notice, which requires video.
The Verkeer ad also carries an Expresformulier lead-form extension, so it can generate leads as well as traffic. Decide whether that is wanted.
Back-to-school ads (prepared, not yet uploaded):
- Variant A: “French homework starts again. The fights don’t have to.”
- Variant B: “Tired of ads interrupting homework? Ad-free for CAD $1.99.”
- Both produced by Claude Design at 1080×1080, matching existing style
Reddit Ads
- Account created Aug 6, 2026. A brand-awareness campaign went live same day.
- Rep contact: Rati Agarwal (v.rati.agarwal@reddit.com), CC: Samuel Richard
- Reply sent Aug 15 accepting optimization help, asking about Belgium viability, requesting call slots in CET hours
- Rep as of 24 Aug 2026: Ussama Sayeed (Client Partner, SMBs EMEA, v.ussama.sayeed@reddit.com). See the Reddit — platform guidance section.
First flight (6–18 Aug 2026, 13 complete days): €85.59 spend, 30,982 impressions, 165 clicks, €2.76 eCPM, €0.52 CPC, 0.533% CTR, €6.58 average daily spend. Account created 5 Aug; first delivery 6 Aug. Paused 19 Aug.
Relaunch campaign 2567099998208211794 — paused, €43.72 of a €100 cap spent, €56.28 of cap headroom never used. It will not be used: Reddit advertising was discontinued on 26 Aug 2026. Leave the campaign paused; do not reactivate, rename or archive it.
Re-corrected 25 Aug 2026. The 24 Aug note here said €56.28 was “no longer the budget September runs on” because a €500 credit had appeared. That credit does not exist — see the corrected block below. €43.72 spent against a €100.00 cap was confirmed directly in Ads Manager on 25 Aug, so €56.28 is the accurate remainder. But it is cap headroom, not a budget: it is how much more this paused campaign is permitted to spend, charged to the card, not money held on the account. (Asana 1217796325932409)
Corrected 24 Aug 2026. This previously read “Paused to bank the remaining ~€13 for the Sept 1–25 back-to-school window.” That figure was wrong: it came from reading an account-level lifetime total as a single campaign’s spend. The reasoning behind the pause still stands — off-season spend buys less than the same money in September.
Reddit warned on pause that the campaign was “still learning” and recommends 14 days minimum — we stopped at day 13. The standing question for the rep — does relaunching restart the learning phase from scratch, or does reactivating preserve it? — was never answered and is moot since 26 Aug 2026: advertising is discontinued, so there is no rebuild-vs-resume decision to make. (Partly answered 24 Aug: renaming a campaign or ad group resets the learning phase at both levels — see Reddit — platform guidance.)
There is no Reddit conversion pixel, and there will not be one. Reddit traffic does not auto-tag, so it landed in GA4 as Unassigned and its conversions were invisible in both directions. With advertising discontinued on 26 Aug 2026 this is now permanent: the €86.72 spent bought 168 clicks whose value can never be established. If Reddit ads ever resume, install the pixel and UTMs before the first euro, not during.
AdMob revenue baseline (19 Jul – 17 Aug 2026, pre-campaign)
| Segment | Revenue | eCPM | Impressions |
|---|---|---|---|
| Canada iOS | €0.28 | €6.41 | 44 |
| Belgium iOS | €0.22 | €3.96 | 56 |
| Canada Android | €0.13 | €2.67 | 49 |
| Belgium Android | €0.07 | €1.21 | 57 |
| NL/UK/FR/MA/BR/other | €0.05 | — | 29 |
| Total | €0.75 | €3.21 | 235 |
Match rate 92.96% overall; 341 requests.
Read this correctly. The eCPMs are healthy — Canada iOS at €6.41 is a good rate. What’s missing is traffic, not monetisation quality. This is a volume problem and marketing has been limited so far, which is why the ad-supported tier is being kept rather than removed.
Consequence: Belgium is ~€3.50/year. A blanket tagForUnderAgeOfConsent costs €0.70–€1.40/year, so it is not a revenue decision at this scale. Revisit only when Belgian ad revenue passes ~€10/month, at which point conditional TFUA driven by the real consent signal becomes worth the extra work.
WordPress.com Blaze — $200 credit
Verified 24 Aug 2026 in Tools → Advertising on verbomatique.com.
- US$200.00, unspent, expiring 1 Aug 2027. Zero campaigns, zero payments, 0 ad views, 0 clicks.
- Site-scoped to verbomatique.com. The nl-BE site is self-hosted with no Jetpack, so it cannot use this.
- Inventory: WordPress.com blogs + Tumblr. CPM model.
- Targeting: language, location down to province (include-only — no exclusion control, so Quebec is handled by never adding it), interest category, device. No age or parent demographic targeting.
- Only two promotable items exist (Cookie Policy, Hello World!) because the site is a single-page build — but Advanced Settings → Destination URL offers “The site home”, and a real page can be promoted directly once it exists.
- URL parameters field exists (free text, 2096 chars) — UTMs are supported.
- Trap: schedule defaults to “Run until you cancel it” with recurring weekly charges to the saved card once credits are exhausted. Use “Specify the duration”.
- Estimate at US$5/day: 29.8k–40.3k weekly ad views, 67–91 weekly clicks (worldwide/all-topics — narrowing will change it).
Blaze creative background — DECIDED 24 Aug 2026
Cream field #FAF9F5 plus a full-bleed 16–20px border in --vb-teal
#0B5443, identical on all three variants. Cream-on-white has no edge against
WordPress/Tumblr page backgrounds.
This means the Branding section’s claim that ad creative style is “consistent across Meta, site, and print materials” is no longer true. Blaze diverges deliberately. Whether the border comes back to Meta creative is undecided.
Known gap in this file — CLOSED 25 Aug 2026
The gap recorded here was the unverified claim that €43.01 of historical Reddit spend
sat in an ended flight 2562145456864748615. Both halves are correct. Read from
Ads Manager on 25 Aug 2026: campaign 2562145456864748615 has spent exactly €43.01
(17,096 impressions, 92 clicks) and reads “Not delivering — Campaign Schedule ended”,
its ad group 2562145456864828991 scheduled Aug 5 to Aug 12 2026.
The recommendation attached to it — rename or archive — should not be acted on: renaming resets the learning phase at both levels (Ussama, 24 Aug). Leave it alone.
The dropped sentence was right; leaving it out until it could be checked was still the correct call.
Reddit: the “€500 ad credit” was never a credit — CORRECTED 25 Aug 2026
This section previously stated that a €500 ad credit existed on the account. It does not, and did not. Ussama Sayeed’s email of 24 Aug 2026, 12:42 (“Verbomatique x Reddit”) described it as a credit “to check for and activate”; the account itself shows an empty Ad Credit panel and an unclaimed spend-matching offer — €500 in credit when you spend €500. See “Reddit: the €500 is a spend-matching offer, not a credit” below for the verified terms, and note that the offer was deliberately not claimed on 25 Aug.
Ussama’s verbal recommendation on the call was €50–100/day, on a cold-audience argument. It was worth weighing while the €500 looked free; it is not free, so treat the recommendation as what it always was — a vendor advising more spend.
The lesson worth keeping: a figure quoted by a platform’s own sales representative is a claim, not a reading. This one travelled through four documents and reshaped a whole month’s plan before anyone opened the billing page. Verify money in the account that holds it.
Source: Asana 1217796325932409, created 24 Aug 2026 14:58, corrected by the comment of 25 Aug 2026
Reddit: renaming a live campaign resets the learning phase — build new instead
Confirmed by Ussama Sayeed on the 24 Aug call: renaming causes a learning-phase reset at both campaign and ad-group level. This inverts the standing plan, which was to rename the badly-named existing entities so learning would survive.
Current state of the account, read from the Ad Groups tab with View Setup on, 24 Aug: both campaigns are named “VS EN-CA Launch - Reddit Traffic” and both ad groups (2562145456864828991 and 2567099998209021441) are named “Asset Group 1”. Both ad groups also carry identical targeting — Canada with no exclusions; communities Teachers / languagelearning / alberta / French / ontario / britishcolumbia / Parenting; keywords french conjugation, french grammar, french homework help, language learning apps, study tools.
The resolution: do not rename anything. The plan was to build the September flight as new campaigns with correct names, since a new campaign enters its learning phase regardless. That flight was cancelled on 26 Aug 2026, but the do-not-rename instruction outlives it: campaign 2567099998208211794 — paused 19 Aug at day 13 of a recommended 14 while Reddit still flagged it “still learning” — keeps whatever partial learning it holds only for as long as nobody renames it. Leave both campaigns exactly as they are.
Never answered by Ussama: whether reactivating a paused campaign preserves its partial learning or restarts it. Moot since 26 Aug 2026 — nothing will be reactivated.
Source: Asana 1217633033987496, comments of 24 Aug 2026 07:59 and 14:59
Reddit: the cold-creative benchmark to beat
The 13-day cold-creative flight produced €0,52 CPC, 0.533% CTR, 165 clicks. That was the number a promoted-organic test would have had to beat. No such test will run — advertising was discontinued 26 Aug 2026 — so this stands as the channel’s closing cost record, not a target.
Source: Asana 1217633033987496, comment of 24 Aug 2026 17:11
Cost per store visit by channel, 18–24 Aug 2026
Google Search en-CA €7,06 per store visit vs Meta Traffic €12,45. Google Search is the best-performing channel per outcome on this evidence.
Search-BackToSchool-en_CA-2026 over that window: 4.831 impressions, 63 clicks, 1.30% CTR, €1,12 average CPC, €70,61 spent, 0 conversions. GA4 google/cpc for the same window: 61 sessions, 10 key events — a 9.84% key-event rate, the best of any channel.
Worth carrying forward because it is easy to misread: essentially all of this campaign’s lifetime delivery happened in that one week. Lifetime 25 Jul–23 Aug was 4.296 impressions / 59 clicks; 18–24 Aug alone was 4.831 / 63. There is no declining-CTR trend to read — the 1.69% / 1.37% / 1.30% readings are the same week measured at different cut-offs on a base that started at zero.
Source: Asana 1217582438101941, comment of 24 Aug 2026 07:59
Keyword Planner, Canada — the volumes that decide the en-CA rebuild (verified 24 Aug 2026)
Average monthly searches, Canada, with competition and top-of-page bid range:
| Term | Vol/mo | Comp | Bid range |
|---|---|---|---|
| conjugate french verbs | 8.100 | Low | €0,64–3,62 |
| french conjugation practice | 880 | Low | €0,73–3,22 |
| practice french verbs | 320 | Low | €0,81–3,91 |
| french conjugation app | 50 | Low | €1,03–1,44 |
| french homework help | 50 | Low | — |
| french verb conjugation app | 30 | Low (+300% YoY) | — |
| french immersion resources | 30 | High | €0,42–2,69 |
| french verb app | 10 | Medium | — |
| best french conjugation app | 10 | Low | — |
| french immersion homework help | 10 | Low | — |
| app to learn french verbs / french immersion practice / core french practice | no data | — | — |
conjugate french verbs is 16× the entire app cluster and cheaper than the account’s €1,12
average CPC. Its trend peaks near 14K in September and troughs in July — the −45% three-month
change is summer holidays, not decline.
The French-immersion angle is dead — 10–50 searches/month across every variant, and the only term with real competition is 30/mo and almost certainly school boards. Do not revisit without a Canadian-curriculum landing page.
The structural diagnosis behind this: the campaign buys reference intent while the product serves practice intent. Queries containing practice / conjugate / app / online convert; grammar / tenses / passe compose / prepositions do not. On 18–24 Aug the nine largest reference queries produced 381 impressions and 1 click.
Sources: Asana 1217582438101941, comments of 24 Aug 2026 08:16 and 08:37; Asana 1217775004294568 (created 24 Aug 2026 08:37)
A pre-existing broad negative on “free” is blocking qualified traffic
Search-BackToSchool-en_CA-2026 carries pre-existing broad negatives: dictionary, free, italian, jobs, spanish, translator. “free” is broad match, so it blocks any query containing it — including “free french conjugation app” — while the site’s own positioning is “Free to start. No card required.” Left in place on 24 Aug only because removing an existing negative was out of scope. Needs a decision.
Sixteen phrase-match negatives were added at campaign level on 24 Aug (passe compose, passé composé, prepositions, grammar rules, for beginners, tenses, etre conjugation, avoir conjugation, translation, translate, meaning, definition, worksheet, pdf, bescherelle, conjugaison), bringing the campaign to 22 negatives total. If clicks fall materially, “tenses” is the one most likely over-broad — it also blocks “french verb tenses practice”.
Source: Asana 1217582438101941, comment of 24 Aug 2026 08:37
nl-BE Search has the account’s best CTR and is capped by budget
Search-BackToSchool-nl_BE-2026, all-time as read from the platform 24 Aug 2026: 932 impressions, 57 clicks, 6,12% CTR, €0,71 average CPC, €40,45 spent, 0,00 conversions. Google Ads explicitly flags it “Beperkt door budget” at €6,00/day, and the last two days ran roughly €9/day against that €6,00 ceiling.
Not a small-sample artefact: against the 22 Aug baseline (478 impressions, 29 clicks, €22,33) the increment is 454 impressions and 28 clicks — 6,17% CTR on the new traffic, 4,7× the en-CA campaign’s 1,30%.
The nl-BE keywords (“frans oefenen”, “werkwoorden frans oefenen”) run 4,44–12,00% CTR at €0,49–0,73 CPC because they are practice-intent by construction. The en-CA ones are not. That is the whole difference.
Source: Asana 1217775026626294 (created 24 Aug 2026 09:31); nl-BE/en-CA CTR comparison from Asana 1217582438101941, comment of 24 Aug 2026 08:16
Quebec exclusion — the real state per channel (re-confirmed 24 Aug 2026)
- Meta — does exclude Quebec. Ad sets are named “Parents CA (excl. QC) 30-55”.
- Reddit — targets Canada with no exclusion. This is intentional and correct; an earlier brief wrongly claimed otherwise. Do not “fix” it.
- Google Ads Search-BackToSchool-en_CA-2026 — no Quebec exclusion configured.
Source: Asana 1217776432181383 (created 24 Aug 2026 08:02)
Blaze — operational limits beyond the schedule trap
- Minimum daily budget is US$5 per campaign. Three concurrent campaigns = $15/day, so 12 days × $15 = $180 against the $200 credit with a $20 buffer.
- The recurring-charge consent commits against a saved Mastercard ending 7400 once credits are exhausted.
- Blaze is CPM and the daily budget is a ceiling, not a commitment — actual spend often comes in under, and leftover credit funds a second flight.
- Location targeting is include-only at state/province level. The nine provinces to add individually: Ontario, British Columbia, Alberta, Manitoba, Nova Scotia, New Brunswick, Saskatchewan, Prince Edward Island, Newfoundland and Labrador. Quebec is handled by never adding it.
- Proof that no charge occurred is the Payments tab still showing no payments — not the absence of an email.
Source: Asana 1217802736470180 (created 24 Aug 2026 15:37)
Blaze pre-fills AI-generated ad copy, and it overclaims
Observed 24 Aug 2026 when opening the wizard on post 173. Blaze had already written, into the live fields:
| Field | Blaze’s default |
|---|---|
| Title | “Stop homework fights” |
| Snippet | “Your child can master French verbs solo in 10 minutes—no nagging, no stress, just results.” |
| CTA | “Try it free now” |
“Master French verbs solo in 10 minutes” is a performance promise the app does not make. Overwrite all three text fields on every campaign — the defaults are pre-populated, not placeholders, so they ship if left alone.
Two smaller wizard facts from the same session:
- The Duration field is a controlled component and ignores scripted input. Use the
+button; the default is 7. - The wizard does not save drafts. A reload loses a part-built campaign entirely — an attempt on 24 Aug got campaign A roughly 80% built before the browser connector dropped, and all of it was lost. Build one campaign per sitting.
Source: Asana 1217801744397059 (created 24 Aug 2026 19:41), which records the snippet verbatim, the overwrite rule and both wizard facts. The Title and CTA strings are from the same 24 Aug session as reported by Dries; they are not quoted in that task.
Meta en-CA lead cost, 18–24 Aug 2026
Meta produced 6 leads in the six days to 24 Aug at €4,46 each.
Source: Asana 1217542445377430, comment of 24 Aug 2026 07:59
Organic social — the shape decided 25 Aug 2026
Meta ads were producing Instagram followers who landed on an effectively empty profile. The
plan written that day is claude/verbomatique-social-media-plan.md; the decisions worth
keeping outside it:
- The profile is a trust checkpoint inside the ad funnel, not a traffic channel. At two posts a week from a young account, organic reach will not be a meaningful source this autumn. Nearly all the available value is in the one-off fix — bio, link, nine seed posts — so that ad traffic tapping the account name finds something.
- Market split by platform, not by account: one Instagram account carries en-CA, the Facebook Page carries nl-BE. Same asset, swapped caption. This is a judgement, not a measurement; Business Suite → Insights → follower demographics settles it.
- Content is product-led on every card — practice modes, score report, no-account/offline, pricing, brand line. Chosen over generic French-teaching content, which travels further but sells nothing.
- The product deck is finite: five cards × three headlines × two markets ≈ fifteen weeks. New screenshots refill it; conjugation cards are the reserve behind that.
- Kill criterion, set in advance: review 20 Oct. Fewer than three outbound store clicks a month from organic social → stop posting, keep the profile as a static shopfront. The bar is Google Search at €7,06 per store visit.
- Engagement Custom Audiences (Instagram + Facebook, 365-day window) should be created now regardless — the window only accrues from the moment the audience exists.
Neither site links to any social profile. Verified 25 Aug 2026 by fetching both live public pages and reading every link including footers: store badges and a cookie policy only. The verbomatique.com footer is still the theme placeholder, so there is an obvious slot.
Handle discipline for the rebrand: never register a handle containing “secondaire”. Instagram usernames can be changed but every saved screenshot, tag and link breaks; Facebook Page name changes go through review.
Source: Cowork session of 25 Aug 2026; site link check by public fetch the same day
The app icon is a wide mark, not a square tile
The logo supplied on 25 Aug is 523×355 — a rounded-rectangle robot face with a French
tricolour visor, on an opaque white background, in the blue family (--vb-blue and a
brighter blue), not the teal/coral of the card system. Two consequences for any creative work:
- It must be contained at its own aspect, never cropped into a square app tile.
- On the cream field the outer white has to be keyed out or it reads as a white plaque. The white stripe inside the flag must survive that — key from the border inwards, not by global colour replacement.
Worth a deliberate decision at some point: the logo is blue-based while the card and site
system is teal and coral. This file already treats --vb-blue as “logo frame”, so the split
is longstanding, but it becomes visible once the icon appears on every marketing card.
Source: Cowork session of 25 Aug 2026, working from the file supplied by Dries
Reddit: the €500 is a spend-matching offer, not a credit — verified 25 Aug 2026
There is no €500 credit on the account, and there never was one. Read directly from Ads Manager → Billing → Overview on 25 Aug 2026: the Ad Credit panel is empty. It contains an “Add promo code” link and nothing else — no balance, no expiry line. Credit Card Balance €3.13, billing threshold €10.00, Mastercard **7400 on file, business location BE, currency EUR.
What exists is an unclaimed promotional offer, surfaced as a green “Get €500 in ad credit” pill in the Ads Manager top bar. Opening it produces a modal reading “Get €500 in ad credit when you spend €500!”, with this fine print, near-verbatim:
Offer valid through 9/4/2026. Ad credits expire 14 days after receipt. To earn ad credits, payment for the minimum amount stated in the offer must be made to Reddit within 14 days. Limit one per account, and non-transferable.
The real shape of the deal is two clocks and €500 of our own cash:
- Claim the offer (by ~4 Sept — Reddit’s UI is US-formatted and 9 April is already past, so 4 September is the only consistent reading; this is an inference from date format, not something the UI spells out).
- Pay Reddit €500 of real money within 14 days of claiming. Spend before the claim does not count toward progress.
- The €500 credit is then issued, typically within 24 hours, and expires 14 days after issue.
So taking it means roughly €1,000 of delivery inside about four weeks, half of it cash out of pocket, against an account whose entire lifetime spend is €86.72.
Claiming is effectively irreversible. Per Reddit’s Reddit Ads Credits help article (linked from the modal as “Additional terms apply”): once an offer is accepted, an advertiser cannot claim another one unless it expires or all credits are used. Other constraints from the same article: one active promo per Ads Account, non-transferable, cannot be split across accounts or moved between them, only the account owner may accept, an expired promo cannot be reinstated, offers are void where prohibited and may be revoked at any time and for any reason by Reddit.
How it interacts with the €100 cap: it does not, directly. Ads Credits are account-level and auto-apply to spend accrued after issuance; they never clear an outstanding balance from before. The €100 figure is a campaign spend cap on 2567099998208211794 and would still throttle that campaign independently.
Decision, 25 Aug 2026: not claimed. Dries declined, on the grounds that claiming locks out any other promo and commits to spend the project has not decided to make.
Note the tension in the vendor’s own advice. The same rep who asked us to “check for and activate the €500 ad credit” also wrote, by email on 24 Aug, that “a minimum spend of €3,000 is required to gather sufficient data and achieve actual results during the learning phase” and otherwise recommended organic only. Read together with what the offer actually is, the €500 reads as a spend incentive rather than a gift.
Source: Reddit Ads Manager account jgqslt6433w2, Billing page and offer modal, read 25 Aug 2026; business.reddithelp.com “Reddit Ads Credits” article, same date; Asana 1217796325932409, comment of 25 Aug 2026
Reddit: the account’s real lifetime numbers, per campaign (read 25 Aug 2026)
Dashboard, Jan 1 – Aug 25 2026, campaign view with View Setup on:
| Campaign | Status | Impressions | Clicks | Spend | Cap |
|---|---|---|---|---|---|
2567099998208211794 |
Inactive — campaign paused | 14,083 | 76 | €43.72 | €100.00 |
2562145456864748615 |
On, but not delivering — campaign schedule ended | 17,096 | 92 | €43.01 | none |
| Account total | 31,179 | 168 | €86.72 |
Account-level: €0.52 CPC, €2.78 eCPM, 0.539% CTR.
This settles what “€56,28” is. €100.00 cap − €43.72 spent = €56.28. It is headroom under a campaign spend cap — how much more that one paused campaign is permitted to spend before Reddit stops it — not a credit, not a balance, and not money Reddit owes us. Every euro of it would be charged to the Mastercard on file. The figure has been carried through several documents as though it were available funds.
Campaign 2562145456864748615 is toggled ON but cannot spend. Its ad group
2562145456864828991 reads “Not delivering — Campaign Schedule ended”, with start Aug 5
and end Aug 12 2026. Harmless, but it reads as a live campaign in the campaign list.
Source: Reddit Ads Manager account jgqslt6433w2, dashboard read 25 Aug 2026
Reddit: the September flight re-plan — SUPERSEDED 26 Aug 2026
No September flight will run. Reddit advertising was discontinued on 26 Aug 2026 — see “Reddit advertising discontinued” in Advertising. The planning below is kept as the record of what was considered, including the unresolved €4/day-vs-€5/day question, which no longer needs settling.
With the €500 declined, the September window is funded from the card. Two constraints bind, and neither is the money:
- The learning phase. Ussama’s guidance is 14 days minimum; the August flight was paused at day 13 while Reddit still flagged it “still learning”. A flight shorter than the learning phase repeats that mistake and buys nothing durable.
- The pixel. There is still no Reddit conversion pixel (Asana 1217633136866788). Any spend without it buys clicks at a known €0.52 and teaches nothing past the click — which is exactly what the €86.72 already spent has taught. The pixel, not the budget, is the gate on the September flight.
Reactivating the paused campaign to consume its €56.28 headroom is the weakest path. At any plausible daily rate it funds well under 14 days, and it is still unknown whether reactivating a paused campaign preserves its partial learning or restarts it — Ussama was asked on 24 Aug and did not answer. Building new (consistent with the 24 Aug do-not-rename decision) and funding a full learning phase is the cleaner call.
Reddit’s minimum daily budget is not established. Two different unverified figures are in circulation — €4/day in the 24 Aug rep-call brief, €5/day in the 25 Aug P7 re-plan — and they size the flight differently. Settle it in the campaign creation wizard before committing to a flight length. (Do not borrow the Blaze figure; that is a separate platform.)
Source: Asana 1217633033987496 and 1217542445406771, comments of 25 Aug 2026
Reddit advertising discontinued — decided 26 Aug 2026
Dries decided on 26 Aug 2026 to stop advertising on Reddit once the currently allocated budget runs out. No September flight will be funded and no further Reddit ad spend will be committed.
The rationale is the learning-phase floor. Reddit’s own rep put it at €3,000, quoted in Asana 1217633033987496 (comment of 24 Aug 2026) from Ussama Sayeed’s second email: “Our data shows that a minimum spend of €3,000 is required to gather sufficient data and achieve actual results during the learning phase. If you plan to commit to that budget level, we can certainly start working on setting up the new campaigns. Otherwise, I would recommend focusing exclusively on organic reach for the moment.” €3,000 is not a sum Dries is willing to spend now, and every flight length previously costed in this file — €70, €100, €125 — is between a twentieth and a twenty-fifth of that floor, so it buys an unoptimised campaign. That is what the €86.72 already spent bought.
In practice the decision takes effect immediately, because nothing is spending. On the
25 Aug reads recorded elsewhere in this file, campaign 2567099998208211794 is paused at €43.72
against a €100.00 cap, and campaign 2562145456864748615 is toggled on but its schedule ended
12 Aug, so it cannot deliver. There has been no Reddit spend since 19 Aug. The “€56.28” is cap
headroom on a paused campaign, not funds. So there is no allocated budget still running down:
nothing needs to be stopped, only left stopped. Ads Manager was not re-opened on 26 Aug —
this rests on the 25 Aug reads plus the absence of action since.
Leave the account exactly as it is. Do not reactivate, do not rename, do not archive. Renaming resets the learning phase at both campaign and ad-group level (Ussama, 24 Aug), and the duplicate names are now cosmetic clutter in an account nobody will report on again.
Final channel numbers. Reddit’s lifetime total is €86.72 for 31,179 impressions and 168 clicks — €0.52 CPC, €2.78 eCPM, 0.539% CTR — and, because the pixel was never installed, no outcome figure at all. Reddit is therefore not rankable against Google Search (€7,06 per store visit) or Meta Traffic (€12,45), and never will be. Treat €86.72 for 168 clicks of unmeasured value as the closing figure for the channel, not an interim one.
Organic Reddit is unaffected. The rep’s own advice was to focus exclusively on organic reach, and the listening exercise (Asana 1217639836805570) stays open and undated.
Source: Dries, 26 Aug 2026; Asana 1217542445406771, 1217633136866788, 1217633033987496 and 1217632914852105, all closed 26 Aug 2026 with the decision recorded on each
Reddit — platform guidance
Source: 2026_Creative_Best_Practices.pdf and
Reddit_Pros_organic_playbook.pdf, sent by Ussama Sayeed (Client Partner,
SMBs EMEA, v.ussama.sayeed@reddit.com) on 24 Aug 2026.
Read this before using any figure below
Study population: ~144k in-feed ads from ~7k US advertisers over 12 months. Eligibility required English-language ads with conversion tracking enabled. Verbomatique is Belgian-run, targets Canada, and has no Reddit pixel — so these findings describe a cohort we are not in.
Reddit’s own caveat, verbatim: “these aren’t rules. Just learnings.”
Most of it is correlational. Advertisers who add logos, run three formats and vary their headlines are probably better funded and more sophisticated overall. Treat aspect ratio and headline length as the most trustworthy — they’re cheap, specific, and mechanically plausible. Treat the multi-format multipliers as the least.
Creative — image and video
Aspect ratio, average conversion rate impact
Image: 4:5 = +9.7% · 1:1 = +1.3% · 4:3 = −0.9% · 16:9 = −30.7% Video: 4:5 = +14% · 4:3 = +4.7% · 1:1 = +1.6% · 16:9 = −11.9%
4:5 can produce black bars on desktop. Reddit suggests 4:3 if desktop appearance matters.
Video length: 1–6 sec = +16.8% · 6–10 sec = +11.3% · 10–15 sec = +1.7%. Keep under 10 seconds. The last frame with the replay button may be all anyone sees — design the end card like an image ad.
Logos: images with a logo saw 108% higher average conversion than those without. Placement grid (nine positions):
| left | centre | right | |
|---|---|---|---|
| top | +29.1% | 23.5% | <10% |
| middle | <10% | 10.2% | <10% |
| bottom | 13.2% | 24.9% | +28.7% |
Video: logo in the first 3 seconds = +6.6%; final 3 seconds = +9.3%.
Faces in images: +18.5%. Lifestyle backgrounds with low contrast/brightness outperformed solid bright, saturated backgrounds. Videos with low brightness: +11.7%.
Overlay text — the direction differs by medium. On IMAGES it lowers conversion by an average of 13%. The exception is urgency: mention of a season = +16%, plus ALL CAPS, time frame, discount language, call to action. On VIDEO it raises conversion by 8.2%; overlay using “Sale” or “Save” = >10%. Include closed captions — video plays with sound off by default.
Format variety: 2+ formats = 2.5× vs Reddit baseline; 3+ = 3.1×. Reddit suggests uploading a video as a looping GIF for extra format density.
Conversation Ads placement differs from in-feed: headlines under 100 characters, 4:3 aspect, remove overlay text and logos, prioritise visual clarity, be authentic (people, low colour and saturation).
Creative — headlines
On Reddit users see the headline before the asset, and everyone knows it’s an ad. Say who you are, what you’re selling, why they should care.
- Under 150 characters: +8–25%. Under 50 drove the highest conversion.
- Leading with a question: +9.3%
- Mentioning the brand name: +6.5% conversion, −6.5% CPA
- At least 1-in-3 headlines unique across a campaign: +12.4%
- Use first and second person (we, our, you, your)
- Hero the brand name; highlight price points and discounts; mention a relevant time frame, season or moment; include a simple CTA
- Read headlines aloud — they should sound like a person, not a brand
Organic — Reddit Pro
Reddit Pro is free: business.reddit.com/pro
Platform context from the playbook: 110M daily active unique visitors, 21% YoY growth (Reddit Earnings Q2 2025). 51% of total online mentions of purchasing discussions happen on Reddit (Brandwatch, Oct 2023 – Mar 2024). Reddit is the #1 most-cited source across ChatGPT, Perplexity and other AI per Profound.
Three phases, in order. Do not skip ahead.
1. Lurk — track and listen. Guiding line: “you are not here to market, you are here to help.” Use the Trends tab. Add keywords for business name, product names, category, competitors, and trends worth watching (max 5 smart keywords). Then: find which communities mention them, read top conversations to see what problems the audience is solving, identify where your expertise adds value.
2. Comment. The first step for a business. Find conversations via Pro, review community rules first, answer straightforward questions, give expert advice. Listen before speaking; don’t force it. Keep it casual — write as if talking to close colleagues. Be a person, not a brand. Always reply back. Do not sell — most communities forbid self-promotion.
3. Post — only after commenting has built credibility. Contact mods via ModMail before posting, especially in a subreddit dedicated to your category. Use post check. Timing, quality, relevance and luck all matter.
Do: listen; read the room; start where your brand is already discussed; communicate as an individual; comment frequently, post sparingly; consider made-for-Reddit content such as AMAs; get moderator blessing before posting.
Don’t: be self-promotional; spam; antagonise; use corporate speak; leave a redditor hanging; post in a community you don’t own without talking to mods first (comments are fine); panic when something doesn’t land.
Username options: business (u/reddit), employee (u/brandy_at_reddit), or role (u/Reddit_engineering).
Organic — what to measure
The playbook is explicit about the order, and it’s the opposite of how paid campaigns are usually judged:
- Content quality first. Upvote rate — 75%+ is best in class for businesses. Are you getting comments back? Total account upvotes and comments should grow over time.
- Growth second — account-level reach, only once content is resonating.
- Brand awareness and traffic long term — brand mention volume, share of voice, profile followers, all trackable in Pro.
The bridge from organic to paid
Reddit’s own recommended route, and the one that suits a small budget:
- Reddit Pro performance tab → find your best-performing organic post
- Ads Manager → when creating the campaign, choose creative from the “Post Library”
- Promote that post as an ad
The creative is pre-validated by actual redditors, native by construction, and free to produce.
Also flagged: run an AMA with someone the audience wants to hear from, and promote it with AMA ads before, during and after.
Budget threshold — stated by Reddit, 24 Aug 2026
Ussama Sayeed, by email, verbatim: “Our data shows that a minimum spend of €3,000 is required to gather sufficient data and achieve actual results during the learning phase. If you plan to commit to that budget level, we can certainly start working on setting up the new campaigns. Otherwise, I would recommend focusing exclusively on organic reach for the moment.”
Also from the same call: renaming a campaign or ad group resets the learning phase at both levels. And app-install conversion tracking requires a Mobile Measurement Partner (Adjust, AppsFlyer, Branch) — a pixel tracks web events only and cannot see what happens after the store link.
Worth recording that Reddit’s own representative advised against buying Reddit ads at our budget. That is unusually straight advice from someone whose job is selling ad space, and should carry weight accordingly.
Website Consent & Tracking (verified 19 Aug 2026)
Distinct from the app’s UMP work. These are the websites’ consent gates, which had never been tested.
The Meta pixel was firing before consent on nl-BE — fixed 19 Aug
With all Complianz cookies cleared, secondaire.verbomatique.be requested connect.facebook.net, fired facebook.com/tr, defined fbq, and showed zero Complianz placeholders. The banner was displayed but blocked nothing. This had been true since the pixel was installed.
Two independent causes — fixing one does nothing:
- Complianz → Integraties → Diensten: the Facebook toggle was off. That toggle is what tells Complianz to gate a service.
- The pixel was hardcoded in WPCode as a plain
<script>. Complianz can only block scripts it can rewrite. Enabling the toggle alone was tested and changed nothing.
The fix: enable the toggle AND rewrite the pixel as
<script type="text/plain" data-service="facebook" data-category="marketing">.
Verified both directions — blocked pre-consent, activates on Accepteren, placeholder consumed.
verbomatique.com was already correct
Pixel already blocked pre-consent as a facebook/marketing placeholder. No change made.
GA fires pre-consent there deliberately. Complianz is set to “Yes, with Google Analytics” with all three conditions ticked (Google data processing amendment, no other Google services, IP anonymisation) — Complianz’s documented consent-less analytics setup. Defensible under PIPEDA, which permits implied consent for non-sensitive uses within reasonable expectations; the OPC’s stricter line targets behavioural advertising, which here is opt-in.
Not yet reviewed: GA on the nl-BE site. It fires pre-consent there too, and nobody has checked whether the equivalent statistics configuration is set. The GDPR bar is higher than PIPEDA’s.
PlayStoreClick custom event
Existed on verbomatique.com, missing entirely on the nl-BE site despite 8 store links. Added 19 Aug via WPCode footer: delegated click listener, closest() on an anchor, matching apps.apple.com and play.google.com, plus a locale: 'nl-BE' parameter — both sites share one pixel, so without it the markets blur together. Worth adding the equivalent locale param on verbomatique.com.
Left as a plain script deliberately: it guards on typeof fbq and is inert without consent.
OPC guidance on children (Canada)
The Office of the Privacy Commissioner states it is difficult to ensure meaningful consent from children to online behavioural advertising, and that organizations should avoid tracking children and tracking on websites aimed at children. Both sites address parents, which is already the stated position — but this is now a documented regulator expectation on the Canadian side too, mirroring UCPD Annex I point 28 in Europe. Same fork as the in-app nudge.
Expect nl-BE numbers to drop now that declining visitors send nothing. That is the fix working, not a performance change — do not read the gap against Canada as campaign effectiveness.
Lesson: a consent banner being present proves nothing about whether anything is blocked. Test with cookies cleared and read the network requests.
verbomatique.be (the Primaire root site) had no consent management at all — fixed 22 Aug 2026
Discovered while adding the Meta domain-verification tag. This is a separate WordPress install on Combell from secondaire.verbomatique.be, with its own plugin set and its own login.
Before: no Complianz, no banner, no Consent Mode, no cookie policy, no privacy statement. GA4
(_ga_E5G7YHYQ2R) fired on page load. Verified empirically — gtag defined, googletagmanager
requested, zero consent commands in dataLayer, zero banner elements in the DOM. Plugin
set was Akismet, Colibri Page Builder, Contact Form 7, Contact Form Submissions, Elementor,
Limit Login Attempts Reloaded, Site Kit, WPT Demo Importer.
Fixed 22 Aug: Complianz installed and run through the wizard (EU/GDPR region, Belgium-targeting answered Ja, cookie policy generated, statistics via Google Analytics, DNT/GPC enabled, Google Fonts declared, banner and script blocker on); Site Kit Toestemmingsmodus enabled; WP Consent API installed via Site Kit’s own installer.
The deliberate jurisdictional split, worth recording because it looks like an inconsistency: the three consent-less conditions are unticked on verbomatique.be so GA requires consent (GDPR), and ticked on verbomatique.com so GA runs without consent (PIPEDA).
Verified both directions on .be: pre-consent the banner shows and Consent Mode DEFAULT fires
with analytics_storage and ad_storage denied; after Accepteren the UPDATE fires granting both,
cmplz_statistics=allow, cmplz_marketing=allow, _ga then set.
Honest limits, still open: no privacy statement exists on this site (Complianz free cannot generate one, so it was set to “Geen”); Google Fonts still loads pre-consent — declared but the blocker produced no script placeholders; the script blocker produces no placeholders at all (gtag loads and Consent Mode restricts it, same as Secondaire — acceptable under Consent Mode v2 but not full blocking); the cookie policy page is not in any menu.
Cookie-observation trap: _ga_WNNYZ2EVKE and _fbp appear on verbomatique.be but belong to
secondaire.verbomatique.be — cookies set on .verbomatique.be are shared across subdomains.
This site has no Meta pixel of its own.
Source: Asana 1217744003003412 (created 22 Aug 2026 09:28)
secondaire.verbomatique.be Consent Mode v2 verified working (21 Aug 2026)
Read from window.dataLayer on the live site. Default command fires first with ad_storage,
ad_user_data, ad_personalization, analytics_storage, functionality_storage, security_storage and
personalization_storage all denied, scoped to an EEA region list including BE, with
wait_for_update: 500. Update command on accept grants all seven, matching
cmplz_statistics=allow. So the chain Complianz → WP Consent API → Site Kit Consent Mode → gtag
works end to end. Banner offers Accepteren / Weigeren / Bekijk voorkeuren.
Source: Asana 1217714544140891, comment of 21 Aug 2026 12:07
GA4 & Tracking
verbomatique.com property
- Account: 198335918 (“verbomatique”)
- Property: 548294070 (“Verbomatique.com”)
- Stream: 15374194366 (web, verbomatique.com)
- Measurement ID:
G-7H79V4X8Y7· Google Tag:GT-MBNVS8CR - Enhanced Measurement: ON (Pageviews, Scrolls, Outbound clicks, Form interactions)
- Search Console: linked (Aug 7, 2026) · Google Ads: linked (Aug 15, 2026)
- “Exclude All logged-in users” is ON — testing tracking requires temporarily disabling this in Site Kit → Settings → Analytics → Edit, then re-enabling afterward.
Engagement time is not a quality signal on this site — read this first
Do not rank channels by average engagement time, and do not use the “Meta 1s vs Reddit 18s” comparison at all. GA4 stops counting when the page loses focus, and the primary conversion here is an outbound click to the App Store or Google Play. A visitor who lands, sees the badges and taps through converts and ends the session — recorded as a few seconds. A fast conversion and a bounce are indistinguishable by that metric. This comparison was misread for weeks, in both directions: Meta’s short times may mean it converts, Reddit’s long times may mean it reads without converting. Reddit additionally has no conversion pixel, so its conversions are invisible either way.
Report key events instead. The only key events carrying data are click (outbound store badges) and form_submit (newsletter); both starred on property 548294070. purchase, qualify_lead and close_convert_lead are empty — ignore them.
Key events by channel, verified 18–24 Aug 2026:
| Channel | Sessions | Key events | Rate |
|---|---|---|---|
| google / cpc | 61 | 10 | 9.84% |
| facebook / paid_social | 133 | 3 | 2.26% |
Note this reverses the reading the 19 Aug baseline invites: Paid Search shows as the dead channel there (10 sessions, 0 engaged, 0s) and is in fact converting at more than four times the rate of Paid Social.
The newsletter form is not a conversion path
form_submit fired twice in 28 days, from a single user. Outbound store-badge clicks fired 92 times in the same window. Treat the form as a mailing-list capture, not a conversion metric, and do not re-plan the page around it — moving it higher up the page is not a fix for a 2-in-28-days event. The download proxy (click) is the metric that carries signal. See also the Google Ads note on Aanmeldingen in the Advertising section.
Event names (actual, not placeholder)
click— Enhanced Measurement outbound click (fires on store badge clicks)form_submit— Enhanced Measurement form submissionpage_view,first_visit,session_start,scroll,user_engagement,form_start— standard auto-events
The property and tag inventory (audited 21 Aug, corrected 21 Aug, re-read 24 Aug 2026)
en-CA (verbomatique.com): GA4 property 548294070.
nl-BE (secondaire.verbomatique.be): account 226199012 (verbomatique_secondaire) holds two properties:
- 311442599 — the live one. Measurement ID G-WNNYZ2EVKE, data stream 3453917046, connected via Site Kit, Enhanced Measurement enabled, plugin conversion tracking disabled.
- 375937063 — measurement ID G-E298W2THX6, has never received data. Dead duplicate, to be deleted.
Also loading on the site: GTM container GTM-K22HFHPJ alongside gtag.js (Site Kit has both Analytics and Tag Manager connected), and a second measurement ID G-EQDN3BWDSD that is still unaccounted for — probably inside the GTM container or a leftover hardcoded snippet.
Both sites have “Excluded from Analytics: all logged-in users” switched on. This is a standing source of false “tracking isn’t working” conclusions — you cannot test collection while logged in.
Source: Asana 1217714544140891 (created 21 Aug 2026 11:53) and its comments of 21 Aug 12:00 and 12:07
nl-BE GA4 is no longer non-functional — and click is one toggle from being countable
Property 311442599, read 24 Aug 2026. Last 28 days: 50 active users (+138,1%), 280 events (+95,8%), 0 key events. Last 7 days: Belgium 28 users; Paid Search 27 sessions, Paid Social 14, Direct 4, Organic Social 4, Referral 1.
Events actually arriving on stream “verbomatique secondaire”: click, first_visit,
page_view, scroll, session_start, user_engagement.
The three configured events — page_view_apple, page_view_google, purchase — all read
“Geen streamgegevens gevonden” over 28 days. Two of them are starred as key events, which is
exactly why the key-event count reads 0 despite live traffic. They are GA4 “create event” rules
that have never matched anything.
click — the outbound store-badge click, the same event the whole en-CA conversion picture
rests on — is being collected and is NOT marked as a key event.
Still absent from this property: any form_submit, and any PlayStoreClick (the event added
to the site on 19 Aug does not appear under that name). Unverified: whether the
locale: 'nl-BE' parameter is present, and per-event counts — the property has no report
configured.
Source: Asana 1217773872320434 (created 24 Aug 2026 09:31)
en-CA event volumes behind the form_submit finding (read 24 Aug 2026)
GA4 property 548294070, event counts by name:
- 18–24 Aug:
click14 events / 10 users;form_start1;form_submitdoes not appear at all. - 27 Jul – 23 Aug (28 days):
click92 events / 45 users;form_start5;form_submit2 events from one single user — almost certainly a test.
The site converts at roughly 7% (294 sessions, 20 key events over 7 days), carried entirely by
two starred key events: click and form_submit.
Sources: Asana 1217776430661860 (created 24 Aug 2026 08:01); Asana 1217714544140891, comment of 23 Aug 2026 19:32
Stale starred key events on verbomatique.com
close_convert_lead, purchase and qualify_lead are starred as key events but show no
stream data — GA4 e-commerce defaults that have never fired. Harmless, but they clutter every
conversion report.
Source: Asana 1217796351575595 (created 24 Aug 2026 15:36)
Store Links
App Store (Canada): https://apps.apple.com/ca/app/verbomatique-secondaire/id1611881585
App Store (Belgium): https://apps.apple.com/be/app/verbomatique-secondaire/id1611881585
Google Play (global): https://play.google.com/store/apps/details?id=be.teamwabisabi.verbomatique_secondaire
Play Store badges on verbomatique.com carry a referrer tag: utm_source=google_play_badge&utm_medium=referral&utm_campaign=back_to_school_2026 (updated Aug 15, 2026).
Primaire’s privacy policy is live on GitHub Pages (22 Aug 2026)
https://teamwabisabi.github.io/verbomatique/privacy/nl-be — verified serving real content,
HTTP 200. Committed as 56b6aa5.
Structural fact worth recording: there is no standalone teamwabisabi.github.io repo. Both
privacy policies are GitHub Pages project sites on the respective app repos — source
main branch, /docs folder, Jekyll minima theme. Pages was not enabled on the verbomatique
repo before 22 Aug; it is now (main/docs, legacy build).
Primaire’s is nl-BE only (Dutch-only app). Secondaire’s, at
https://teamwabisabi.github.io/verbomatique_secondaire/privacy/nl-be, has locale-aware paths
including en-ca and was last updated 17 July 2026.
Deliberately not copied from Secondaire into Primaire’s: app-ads.txt (declares an ads-seller
relationship, wrong for a no-ads app), and the AdMob, Crashlytics, newsletter and
in-app-purchase sections.
After the rebrand these paths will say verbomatique_secondaire and verbomatique for what
are by then Verbomatique and Verbomatique Junior. Store listings point at these URLs, so
changing them is not free — either accept the mismatch (as with the app IDs) or plan redirects.
Source: Asana 1217744288172445, comment of 22 Aug 2026 15:49
Technical Patterns That Work
Editing the two websites
Moved to its own section — see Website Editing, which now holds the REST route, the code-editor route, and the Colibri techniques in one place.
Brevo form editing
The form HTML lives in the sibformmarkup textarea on the Brevo plugin’s form edit page. Use the same native setter JS pattern to replace text, then click Save. The form preview updates on save, not on edit.
Google Ads campaign wizard
When adding ads in the wizard, you MUST click the “Klaar” (Done) button at the bottom of the ad editor to commit the ad. Without it, the Review summary shows “Advertenties: Geen” and the publish button doesn’t appear — this is not a display bug, it’s the ad being in uncommitted editing state.
Meta Ads Manager “Invalid request (#1)” — it is a LOCAL fault, not Meta
Symptom: campaigns table empty, “Kan concept niet opslaan: Invalid request. (#1). Je kunt in dit account geen advertenties aanpassen”, reporting unavailable. Two days were spent treating this as a Meta-side outage and preparing an escalation.
It is client-side. The fix has two parts and both matter: clear cookies for facebook.com and instagram.com, then enter Ads Manager via the base URL (adsmanager.facebook.com), not a bookmarked or history-completed deep link carrying saved view state. Clearing cookies alone may not be enough.
Diagnostic that isolates it in 30 seconds: open the account in a private window. If it works there, it is local.
General lesson: when a platform surface fails but the platform’s own account-health checks are clean, test in a private window before assuming the platform is broken.
Meta uses the File System Access API for uploads
The Uploaden button calls showOpenFilePicker(), not an <input type=file>. There is no file input in the DOM to write to and the native dialog is outside the page, so file uploads to Meta cannot be automated by browser tooling. Everything either side of the dialog can be.
Complianz blocks only scripts it can rewrite
See the Website Consent section. A hardcoded WPCode script is invisible to Complianz regardless of the service toggle. Requires type="text/plain" plus data-service / data-category.
WPCode’s editor auto-closes script tags
Typing a full <script>…</script> produces a duplicated closing tag and a syntax error. Type the opening tag and body only; the editor supplies the close.
Complianz site scan location
Not under Tools. It is Wizard → Toestemming → Sitescan.
Brevo form styling — the Form Style panel, and the quote trap
The Brevo plugin’s form edit page has two editable areas, and it is easy to see only the first:
#sibformmarkup(sib_form_html) — the field markup#sibcssmarkup(sib_form_css) — the Form Style panel, further down the page, radio set to “Custom style”
[form] in that CSS is a placeholder, substituted by the plugin with
#sib_signup_form_2 at render time. It is not a broken attribute selector. Do
not “fix” it.
The plugin HTML-encodes double quotes on save. input[type="text"] ships to
the front end as input[type="text"], which is an invalid selector, so
the browser silently drops the entire rule. Rules containing no quotes survive,
which produces the confusing symptom of some elements being styled and others
not. Write the whole stylesheet without quotes — unquoted attribute values
and unquoted font names are valid CSS. A comment at the top of the current
stylesheet says so; keep it there.
Diagnosing: don’t read the CSS textarea and conclude. Load the front end and
check getComputedStyle on an actual input, and grep the emitted <style> block
for ".
en-CA form was wearing the nl-BE stylesheet (fixed 24 Aug 2026)
en_CA_LandingPageForm (id=2) carried CSS written for the Dutch form:
selectors referenced sib-AUDIENCE_NL-area and sib-CONSENT_NL_BE-area. The
en-CA form uses AUDIENCE and CONSENT_MARKETING, so every rule for the radio
group and the consent row matched nothing. Only locale-neutral rules applied —
which is why the “Email address” label was styled and “First name” was not.
Rewritten 24 Aug with the correct attribute names. The markup was also fixed:
the AUDIENCE block had <div> inside <p> (invalid — browsers auto-close the
<p>), now <span class="vb-radio">; and a stray <label>*</label> before the
consent checkbox was removed.
This affects the homepage too — same shortcode [sibwp_form id=2]. The live
homepage form had been unstyled since it was installed.
Not checked: whether the nl-BE form has the mirror-image problem.
Working with Claude Code on this repo
Observed consistently across the 18–19 Aug consent work: work that produces code gets done; work that produces evidence gets dropped from the summary. Test runs, greps and file dumps were repeatedly requested, performed, and then omitted from the report. Treat unreported items as not done. Ask for raw output (! shell escape) rather than a summary, and read the numbers yourself — on two occasions the summary’s conclusion contradicted the data directly above it.
Test behaviour, not markup
Two confident false negatives in this project came from the same mistake — reading page source instead of observing behaviour.
On 21 Aug the finding “there is no gtag('consent','update') call” came from regexing inline
script text. Complianz fires that call dynamically from its own JS file, so it could never
appear in inline markup: the detection method was incapable of finding the thing it
concluded was missing. Reading window.dataLayer on the live site showed Consent Mode v2 fully
working.
The 22 Aug typographic-apostrophe false negative is the same shape — and note the inverse trap: template source uses ASCII apostrophes while the rendered page uses typographic ones.
The rule: before reporting something absent, satisfy yourself the check could have detected it
if it were present. Read dataLayer, read network requests, read getComputedStyle.
Sources: Asana 1217714544140891, comment of 21 Aug 2026 12:07; Asana 1217790212813686 (24 Aug 2026)
A GA4 property ID and a measurement ID are different identifiers for the same property
Property 311442599 is G-WNNYZ2EVKE. Comparing a property number against a measurement ID proves nothing — a whole “data is going to an unknown property” finding on 21 Aug came from exactly that, and was wrong.
Source: Asana 1217714544140891, comment of 21 Aug 2026 12:00
Instruct Claude Code to paste raw output and flag ambiguity rather than resolve it
This is what surfaced the Firebase SDK in the Primaire Android binary on 22 Aug. A summary
would have said “no ads, no analytics” — true of pubspec, false of the shipped binary. It
also produced an unprompted correction of the auditor’s own earlier AD_ID claim.
Source: Asana 1217744288172445, comments of 22 Aug 2026 09:59 and 10:41
Google Ads single-key shortcuts hijack mistyped input
Which is why form values in a browser session should be set programmatically rather than typed.
Source: Asana 1217790212813686 (24 Aug 2026), recording the contents of project-knowledge-website-editing.md
The WordPress.com MCP connector does not include DNS
The raised scopes of 24 Aug cover content, not DNS. wpcom-domain-update-dns-records returns
“The ‘wpcom/domain-update-dns-records’ ability is not enabled in your MCP settings”, as do
wpcom-user-sites and the DNS read abilities. The DNS records on 25 Aug were added through the
WordPress.com dashboard at wordpress.com/domains/manage/verbomatique.com/dns instead.
Enable at wordpress.com/me/mcp if a future session should do DNS directly. Until then, plan
DNS work as browser work.
Related trap: wpcom-ai-agent-sites-list is enabled but is a public discovery tool — it
returns unrelated WordPress.com blogs, not this account’s sites. Do not read its output as
“the user’s sites”.
Source: tool errors observed 25 Aug 2026.
A reply sent by API is not a test of Reply-To
The Gmail MCP reply tool addresses the message’s From, not its Reply-To. Using it to test
a reply path produces a bounce that looks like a broken Reply-To and is not one.
To test Reply-To, reply from the actual mail client and read the To field it pre-fills. On 25 Aug the API reply bounced off hello@news.verbomatique.com while the Gmail web client, on the same message, correctly pre-filled decibellen@gmail.com and delivered.
The bounce was still worth having — it is what surfaced the missing MX. But it would have been reported as a Reply-To failure by anyone who stopped at the first result.
Source: Asana 1217778600752666 and 1217814220575319, both 25 Aug 2026.
DKIM CNAME chases time out from the cloud container — check a known-good control first
Resolving brevo1._domainkey.<domain> for its TXT (chasing the CNAME into dkim.brevo.com)
times out from the cloud container against 8.8.8.8, 1.1.1.1 and 9.9.9.9 alike, while the CNAME
itself and the target’s TXT both resolve fine when queried separately.
This is an artefact of the container’s resolver path, not a broken record. Confirmed by
running the same query against brevo1._domainkey.verbomatique.be, a domain Brevo already
reports as Authenticated — it times out identically.
Before reporting a DKIM record as unresolvable, run the same query against a domain known to work. Otherwise the check cannot distinguish “record missing” from “resolver path broken”.
Source: DNS queries run 25 Aug 2026.
Check marketing claims against the app’s actual verb and tense inventory before publishing them
On 25 Aug a set of conjugation marketing cards was built covering twelve verbs across
présent, imparfait, futur simple and passé composé. Reading the repo showed devoir is not
in the app (no verb_devoir.dart), passé composé is not a tense it drills, and
conditionnel présent, which it does drill, was absent from the cards. Fifteen of
forty-eight cards taught content the app could not deliver.
The error’s origin is worth recording: the verb list was taken from the marketing artefact itself and then asserted back as the app’s inventory. Nothing checked the two against each other because both came from the same place.
A file count is not a shipped inventory — even when it turns out to match. A Claude Code
session reported thirty-two files matching verb_*.dart in lib/repository/verbs/. Within
one message that became “the app ships thirty-two verbs” and then a planning conclusion about
content runway. The number was later confirmed correct by a direct check — but nothing in the
chain that produced it justified asserting it, and it would have been repeated with equal
confidence if it had been wrong. A glob cannot distinguish the basic set from gated content,
test fixtures, or unreachable code; it happened to here because no gated verb content exists yet.
Carry the scope with the number, always. Thirty-two and forty-three are both correct counts
of verb_*.dart — scoped to lib/repository/verbs/ and recursive across lib/ respectively —
and a figure quoted without its command invites a reader to treat one as refuting the other.
The eleven-file difference is not gated or unshipped verb content: it is routes, models and
widgets whose names merely begin with verb_ (verb_card.dart, verb_metadata.dart,
verb_indicative_test_route.dart and eight more). The recursive count is not a verb count at
all, which is the sharper reason never to quote it as one.
The number that actually means something is the registration list. FrenchRepository._internal()
instantiates each verb explicitly, so its length is the shipped inventory by construction, and a
verb file that exists but is not registered would not ship. Re-derive with
ls lib/repository/verbs/verb_*.dart | wc -l and by counting the entries in that constructor —
both read 32, re-verified 26 Aug 2026 at commit f2ff92f.
The pipeline’s --verb all is eleven invented verbs, not the app’s set
ALL_VERBS in scripts/screenshots.sh holds eleven verbs. They are not a subset anyone
chose from the product. They are the twelve most common irregular French verbs, picked from
general French knowledge by a Cowork session on 25 Aug 2026 for a marketing card deck, before
any repo had been read — minus devoir, dropped when Claude Code found no verb_devoir.dart.
That list was then written into the Claude Code brief as though it were the app’s inventory, hardcoded into the pipeline, and read back out as “the marketing eleven” — a guess laundered through two artefacts and returned as fact. The same brief told Claude Code to verify the tense axis and not the verb axis, for no reason other than that the author happened to feel uncertain about one and not the other.
Fixed 25 Aug 2026. --verb all now derives the set from the app’s own registration via
scripts/list_verbs.py, so it cannot drift from the picker; the invented eleven moved to
--verb marketing with a comment recording what they are; and every run prints which set it
resolved and how many. The lesson survives the fix: a short hardcoded list inside a script is
indistinguishable from a derived one at a glance, which is how this travelled four artefacts
without being questioned.
Source: Cowork session of 25 Aug 2026, tracing its own artefacts
ls lib/**/verb_*.dart | wc -l returns 43, not 32. Do not count verbs this way.
The recursive glob sweeps in eleven files that are not verbs: verb_metadata.dart,
verb_test_model.dart, verb_repository.dart, six verb_indicative_* route files,
verb_selectionmode_selection_route.dart and verb_card.dart. Scoped to
lib/repository/verbs/ it gives 32; so does the registration list. This is not a hypothetical
trap — that exact command was written into a brief on 25 Aug as the way to settle the question,
and it would have produced a fourth different number in a conversation already juggling three.
The rule: any marketing asset naming a specific verb, tense or mode is a claim about the product. Re-derive it from the repo rather than from another marketing document — and derive the right thing. A glob answers “what files exist”; the question a marketing card raises is what a user can open today, and only the registration-to-wheel chain answers that.
Note that the tense set is not uniform: futur proche and passé récent exist only on
aller. A sweep that assumes every verb defines every tense will silently produce nothing
for most of them.
Source: Claude Code session of 25 Aug 2026, repo read; marketing deck corrected the same day
The git operating notes are in the merge-protocol doc, not CLAUDE.md
The lock-file enumeration, the fact that git status is a write, the no-push constraint and
the commit identity all live in the project doc claude/knowledge-file-merge-protocol.md
under “Operating notes for Claude sessions on this repo”. They are not in the repo’s
CLAUDE.md on any branch — verified 25 Aug 2026 when a brief pointed there and Claude Code
found nothing on either main or the harness branch.
Worth fixing at the source: these are operating notes for anyone working the repo, and they are filed inside a document about merging a knowledge file, which is not where anyone looks for them.
Source: Claude Code session of 25 Aug 2026, having read both branches’ CLAUDE.md
There are two screenshot harnesses, on different platforms, for different locales
Easy to miss because the file describes screenshot work in two places without connecting them.
- iOS, en-CA —
harness/en-ca-screenshots: iPad mini A17 Pro simulator by UDID, locale viasimctl+ reboot, capture viaxcrun simctl io … screenshot, bezelled byadd_bezel.py ipad_mini. Native 1488×2266, bezelled 1732×2510. - Android, nl-BE — an untracked
integration_test/marketing_nl_be_test.darton main plus amarketing_screenshots_nl_be/folder. Native 1080×2424. This is the harness behind the nl-BE back-to-school creative.
Both were superseded on 25 Aug by a single parameterised pipeline
(scripts/screenshots.sh + integration_test/screenshot_pipeline_test.dart) driving either
platform. The 1080×2424 figure quoted elsewhere in this file is the Android frame; the
iOS branch harness never produced it.
Source: Claude Code session of 25 Aug 2026, reading both harnesses
ATT can be suppressed on the iOS simulator without touching the ConsentApi seam
Settings → Privacy & Security → Tracking → Allow Apps to Request to Track, switched off,
makes the request resolve denied without drawing a prompt. This is the clean way to take iOS
screenshots despite _requestATT sitting outside the test seam — it does not require
weakening the seam.
Consistent with Apple developer-forum reports; not yet confirmed on this project’s simulator. Confirm before relying on it.
Separately, on the branch the pipeline was built from, the Dart-side
app_tracking_transparency channel mock does suppress the dialog — ATT never fires
there at all, because ConsentManager only exists on consent/ump-flow. The 24 Aug incident
where a real dialog appeared is therefore specific to running against that unmerged branch.
Source: Claude Code session of 25 Aug 2026; Apple Developer Forums threads 669490 and 702582
A brief assembled from this file asserted four stale facts in one page
On 25 Aug a Claude Code brief was written by quoting this knowledge file. Four claims were wrong by the time it ran, and Claude Code caught all four only because the brief told it to verify before acting:
- the harness branch described as unpushed (it was pushed),
consent/ump-flowdescribed as two commits ahead (five),- the
canServeAdsinvariant cited as authoritative for a branch that does not contain it, - a platform recommendation resting on an ATT failure mode that does not occur on that branch.
The lesson is narrower than “the file goes stale”. Branch state and commit counts are the fastest-decaying facts in here and should be re-derived at use, never quoted. Anything scoped to an unmerged branch must say so at the point of citation, because a reader will otherwise apply it to whatever branch they are on. Instructing the agent to verify is not a substitute for not asserting.
Source: Claude Code session of 25 Aug 2026, reported verbatim
Brevo’s HTML editor: the working URL, and two different save semantics
The editor URL is https://app.brevo.com/editor/classic/html/{id}. The path
https://app.brevo.com/templates/editCode?id={id} 404s — it is a plausible-looking guess
that does not exist. Reach a template’s editor from the Templates listing pencil icon
(/templates/email/edit/{id}) if the direct URL is ever in doubt. An automation-step message
takes query parameters: .../editor/classic/html/{id}?automation_id={a}&step_id={s}.
Saving works differently for the two object types, and neither is trustworthy from the UI:
- Template. “Save & Quit” in the HTML editor only stages the change. The commit is the Save item on the template page that follows. Skipping the second step loses the edit silently.
- Automation-step message. “Use this design in automation” persists on its own — no second step. But the click silently no-ops often, with no error and no visual difference between the failed click and the successful one. On messages 10, 12 and 13 it took two clicks, and once it took three.
“Save & Quit” on templates also no-oped at least once each on templates 7, 8 and 9.
The only reliable confirmation is reading the object back from the API with
templates_get_smtp_template and grepping the returned htmlContent for the new string. Roughly
thirty edits were made across eight objects on 26 Aug; every one was verified that way, and the
verification caught a silent failure several times. Do not trust a green toast.
Source: ~30 edits across templates 6–9 and automation messages 10–13, each verified from the API, 26 Aug 2026
Editing Brevo’s HTML editor by script, not by typing
Typing into the editor’s <textarea> does not reliably register — React does not see the
change. The pattern that works, used for every edit on 26 Aug, sets the value through the native
setter and dispatches the events React listens for:
const ta = document.querySelector('textarea');
const o = '<old string>';
const n = '<new string>';
let v = ta.value;
const c = v.split(o).length - 1;
if (c !== 1) throw new Error('count ' + c); // abort unless the anchor is unique
v = v.replace(o, n);
const s = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
ta.focus(); s.call(ta, v);
ta.dispatchEvent(new Event('input', {bubbles: true}));
ta.dispatchEvent(new Event('change', {bubbles: true}));
ta.blur();
ta.value.includes(n); // returns true if it took
The uniqueness assertion is the part that matters. The welcome emails repeat phrases between
paragraphs, and a replace on a non-unique anchor silently edits the wrong one. Assert the count
is exactly 1 and throw otherwise — the same discipline the knowledge-file merge protocol
requires, for the same reason.
For a structural change rather than a string swap — the 26 Aug pass moved the product-explanation paragraph above the free-app paragraph in all eight emails — build the new value by slicing on asserted markers and check the result’s ordering before dispatching, rather than writing a long literal.
Source: the 26 Aug 2026 editing pass across all eight Brevo email objects
browser_batch races the Brevo UI
Batching clicks against Brevo lands them on stale coordinates: a campaign name typed into a page
that had already navigated, a subject field missed because the panel had scrolled underneath.
Interleave explicit wait actions and screenshot at every decision point. For the irreversible
steps — anything that sends — drop to one action per call and read the result before the
next.
Source: repeated failures and recoveries while building campaigns 14–17, 26 Aug 2026
Key Files in This Project
| File | What it is |
|—|—|
| CLAUDE.md (in the app repo, not this project) | Code invariants, architecture, testing conventions, and the working agreement for Claude Code. Read automatically every CC session. |
| verbomatique-secondaire-roadmap.md | Living product roadmap (monetization tiers, feature status) |
| en-CA-back-to-school-launch-plan.md | Complete campaign plan with budget, timeline, tasks, A/B test spec |
| claude-design-brief-meta-ads.md | Brief for producing Meta ad creative variants |
| verbomatique-pricing-signup-copy-update.md | Copy changes for both sites (mostly applied) |
| verbomatique_secondaire_ad_preview.png | Original judo club placemat ad (source of brand color extraction) |
| Verbomatique_EN_Canada_Campaign_Plan_FINAL.docx | Original en-CA campaign plan document |
| verbomatique-inline-signup-form.html | Standalone nl-BE signup form markup |
| harness/en-ca-screenshots (git branch) | Screenshot harness split off main 24 Aug. Committed and pushed — origin/harness/en-ca-screenshots exists, verified 25 Aug 2026. iOS/iPad-mini only. See scripts/README-screenshots.md on that branch. Superseded 25 Aug by the parameterised pipeline on harness/screenshot-pipeline. |
| blaze-en-CA-landing-page-and-ads.md | Blaze page copy, ad variants, targeting config |
| blaze-creatives-revision-2.md | Creative revision brief incl. the background decision |
| claude-design-brief-blaze-creatives.md | Original Claude Design brief |
| verbomatique-com-wordpress-build-guide.md | How the en-CA site was built: theme, fonts, Brevo setup, block-by-block page construction |
| File | What it is |
|---|---|
project-knowledge-website-editing.md |
Rewritten 24 Aug. Supersedes the 23 Aug “REST API does not work” note. Three edit routes, the iotix//home fingerprint, the verification protocol, rollback recipes. Sits in the outputs folder — not yet pasted into project knowledge. |
google-search-en-CA-keyword-rebuild-2026-08-24.md |
Full keyword and negative lists behind the Phase 1/Phase 2 Google Search rebuild. |
claude/tfua-review-log.md (project doc) |
Append-only log for the monthly AdMob Belgium / TFUA review. |
Source: Asana 1217790212813686 and 1217582438101941 (both 24 Aug 2026); project doc claude/tfua-review-log.md
| File | What it is |
|---|---|
claude/verbomatique-social-media-plan.md |
Organic social plan for Instagram + Facebook, 25 Aug 2026. Six-week calendar, seed posts, measurement, kill criterion, and the two legal constraints on any post. Artifact version at claude.ai/code/artifact/2b55ae33-beb4-49d1-afb0-e0c2fc5f719c. |
claude/claude-code-screenshot-pipeline-prompt.md |
The brief that produced the screenshot pipeline. Retains the tool-choice reasoning (why Claude Code and not a design tool) and the platform-per-mode rule. |
scripts/screenshots.sh (repo, harness/screenshot-pipeline) |
Parameterised marketing-screenshot pipeline. Flags for platform, locale, verb, tense, mode, screen, score. Both iOS and Android. Output gitignored. |
| Card studio (artifact) | Generates the five product cards + captions at 1080×1350, both markets, app icon embedded. claude.ai/code/artifact/f7317cc1-f7e7-4cdd-a0c2-4081ff3c0c42 |
Source: Cowork session of 25 Aug 2026
What’s Still Open (as of Aug 24, 2026)
Each task: owner · effort · when · done when. “Dries” = manual, needs a human in the loop. “Claude” = can be done in a Claude in Chrome session.
Consent & ads compliance (Asana: C1–C6, Product & Release)
- C1 · English ATT prompt string — built and delivered to ASC 18 Aug, awaiting Apple approval. iOS-only (ATT is an Apple framework). Done when: live in the App Store. Matters more than its size: Canada iOS is the best-earning segment (€6.41 eCPM) and those users were seeing an unreadable Dutch prompt.
C2 · Interim TFUA policy for nl-BE— decided: blankettrue. Belgian revenue ~€3.50/yr makes conditional logic not worth building. Review trigger at €10/month.- C3 · UMP consent flow — built and committed on
consent/ump-flow. Not merged, not released. Outstanding: wrap_requestATTin try-catch. C4 · Gate advertising ID on consent— absorbed into C3; both calls were dead code and were deleted.- C5 · Set RequestConfiguration — one-line change,
tagForUnderAgeOfConsent: trueat thegatherConsentcall site. LeavetagForChildDirectedTreatmentUNSPECIFIED (app is not child-directed under COPPA; no under-13 audience declared). - C6 · Device QA matrix — the real release gate. All 208 tests run against fakes; nothing has touched a live UMP form or a real ATT prompt. Cover nl-BE grant/deny/revoke and en-CA on both platforms, plus cold-start ordering.
Urgency note: exposure scales with marketing success. Today a consent gap touches a handful of Belgian sessions a week; if nl-BE back-to-school lands, it touches far more people in the EEA during the month traffic is actively being driven at it.
In-app upgrade nudge (Asana subtasks)
- Decide: accept “no in-app exhortation” for lanes B and C?
- Decide: build age-signal integration now, or ship Lane C-only first? — C3 provides a lifecycle hook where routing can attach, but not the age signal itself. Consent status says nothing about age; that needs the Apple/Play APIs separately.
- Decide: is a staged-rollout A/B acceptable as the measurement? — no backend, so store-layer A/B avoids adding an analytics SDK (and its consent, privacy-policy and minors-data implications).
Campaign & data hygiene (carried over)
Weekly: export nl-BE leads and import to Brevo— superseded 25–26 Aug 2026. The nl-BE Pabbly workflow is live and proven delivering, so new leads reach list 2 automatically, and the 20 leads stranded before it existed were imported the same day. The stopgap cloud task that bridged the gap was deleted 26 Aug. Nothing recurring remains here — but note that nothing now diffs Meta against Brevo on a schedule either.- Backfill the pre-Aug-10 en-CA leads — Dries · 20 min
- Set
LOCALE=en-CAon backfilled contacts during import - Add static
LOCALE=en-CAto the en-CA Pabbly Brevo action — Claude · 2 min Deactivate the half-built nl-BE Pabbly workflow— moot as of 25 Aug 2026. It was never active and consumed 0 tasks; it has since been completed and activated deliberately, with an action step attached, and shows 2 tasks consumed as of 26 Aug. The underlying warning still stands for any future half-built workflow.Rename Brevo list “Your first list” → “België (Nederlands) Subscribers”— done. Verified 26 Aug 2026:lists_get_listsreturns id 2 named “België (Nederlands) Subscribers” (23 contacts) and id 3 “Canada (English) Subscribers” (37 as of 18:03; it read 39 earlier the same day — see “Three contacts sit outside every list”). No list called “Your first list” exists.Welcome email copy check— done 26 Aug 2026. All eight objects (templates 6–9 and automation messages 10–13) were rewritten over five passes and each edit was verified by reading the object back fromtemplates_get_smtp_template. No “paid version upcoming” language survives anywhere: every version now names the shipped one-time upgrade at the correct currency (CAD $1.99 / €1,99). The practice-method names were corrected to the canonical set at the same time.
Unblocking real Lead Ads automation (sequential)
- 1. Complete Facebook developer registration — done when: developers.facebook.com shows a “Create App” button
- 2. Create a Meta app and associate it with the Verbomatique secondaire page
- 3. Generate one test lead on
VS nl-BE Back to School - Nieuwsbrief - 4. Finish the nl-BE Pabbly action and activate
Back-to-school campaigns — both markets
Flemish schools start Tue 1 Sept; Canadian schools around 7 Sept (Labour Day).
nl-BE back-to-school campaign— LIVE since 19 Aug, running 20–31 Aug. See the Advertising section.- en-CA back-to-school pivot — baseline now frozen (see below), so this is unblocked.
Baseline frozen 19 Aug 2026 (pre-pivot, en-CA)
- Meta Traffic: 217 landing page views, €0.21/LPV, €46.39 spent
- Meta Leads: 17 leads, €3.61/lead, €61.39 spent
- en-CA Meta spend in window: €107.78 (20 Jul – 18 Aug)
- GA4 28-day: 508 sessions, Paid Social is 84.84% of all sessions.
- Reddit 6–18 Aug: €85.59, 30,982 impressions, 165 clicks, €2.76 eCPM, €0.52 CPC, 0.533% CTR
This baseline has no key-event figure, which is the metric that matters. The engagement figures originally recorded here (16.14% engagement, 4s avg; Paid Search 0s, Organic Search 11s, Unassigned/Reddit 18s) have been removed as a channel-quality measure — see “Engagement time is not a quality signal” in GA4 & Tracking. There is therefore no clean before-picture on key events; the 18–24 Aug figures are the earliest reliable reading. Note that limitation explicitly rather than substituting engagement time for it. Any re-baselining drawn on engagement will conclude the campaign degraded traffic quality when it may have improved conversion.
Near-term (product)
- Extra verb-set IAPs (dev work, not yet started)
- Subscription tier pricing decision
- Write-mode feedback tick hidden behind keyboard (Android only; iOS keyboard is semi-transparent and shows it)
- Gender-neutral pronouns as default, with settings toggle
Deliberately deferred
- Mobile measurement partner (Adjust/AppsFlyer) — not worth building for current budget
The monthly TFUA review runs as a cloud scheduled task
Scheduled task trig_019nKHy9B2n9KLJMQquJwosf — “[CLOUD] Monthly TFUA review —
Verbomatique Secondaire (AdMob Belgium)”, cron 0 7 6 * * (6th of each month, 09:00 Brussels),
created 18 Aug 2026 13:19, last updated 18 Aug 2026 13:30, enabled, next run 6 Sept 2026.
Confirmed present in the scheduled-task registry on 24 Aug 2026.
The [CLOUD] prefix is deliberate: scheduled tasks stored locally by the Cowork desktop app
are invisible to Claude sessions and cannot be edited or deleted from one. The prefix is how
you tell which is which.
Decision rule: Belgian revenue < €10 / 30 days → blanket TFUA stays; ≥ €10 → build conditional TFUA off the consent signal and reopen Asana task C2; match rate < 85% → note separately as a fill/config problem rather than a volume one.
Open item for the 6 Sept run: if two entries land in the log for that date, the old local Cowork task still exists and must be deleted by Dries in the desktop app. One run means the cleanup was complete.
Source: project doc claude/tfua-review-log.md; scheduled-task registry read 24 Aug 2026
The weekly nl-BE export scheduled run is broken — resolved 25–26 Aug 2026
Something stalled at 09:07 on Monday 24 Aug after listing Brevo lists, and four leads were believed stranded.
Three corrections to the 24 Aug account. The stranded-lead count was not four: the form held 21 leads on 25 Aug (20 real plus one test submission), none of which had reached Brevo. The task as specified could not have completed anyway, because the Brevo connector cannot write contacts and the opt-in attestation needs a person. And “Weekly nl-BE lead export → Brevo import” is not the name of any scheduled task — it is a phrase from the Asana notes that was quoted back as if it named something. What ran at 09:07 remains unidentified; do not assert an answer.
The durable fix landed the same evening: the nl-BE Pabbly workflow was built, tested against live data and activated, and the 20 stranded leads were recovered from Leadscentrum and imported. Verified delivering on 26 Aug — see “The nl-BE workflow is LIVE — and PROVEN DELIVERING as of 26 Aug 2026”. The stopgap cloud task built to bridge the gap was deleted 26 Aug.
Source: Asana 1217775027491967, created 24 Aug 2026 09:31, corrected by the comments of 25 Aug 2026; Pabbly, Meta and Brevo all re-read 26 Aug 2026
Reddit — closed out 26 Aug 2026
Four Asana tasks were closed on 26 Aug when Reddit advertising was discontinued:
P7 · Relaunch Reddit for September(1217542445406771) — the flight itself.R1 · Install the Reddit Pixel(1217633136866788) — it was the gate on spend; with no spend it gates nothing. The pixel measures Reddit ad traffic only, so nothing else loses measurement by its absence.R2 · Naming convention(1217633033987496) — no September entities to name.R3 · Creative(1217632914852105) — including the promote-a-proven-organic-post route, since promoting a post is still paid spend.
Still open, deliberately undated: the Reddit listening exercise (1217639836805570). It is unpaid research and is now the only Reddit activity on the board.
The MMP verdict is reinforced, not changed. The only recent push to build one came from Ussama’s 24 Aug follow-up (“the necessity of implementing a Mobile Measurement Partner (MMP) for accurate conversion tracking for app installs”). That request now has no campaign behind it. “Not worth building for current budget” stands, and removing a channel makes the spend smaller.
Source: Asana, tasks closed 26 Aug 2026
The scheduled-task inventory — and a task name that never existed
Dries listed the Cowork desktop scheduled tasks on 25 Aug. Six existed, and the recorded name “Weekly nl-BE lead export → Brevo import” matched none of them:
| Task | Schedule | Where |
|---|---|---|
| Weekly ad performance review | Mondays ~09:00 | local — disabled 26 Aug |
[CLOUD] Weekly nl-BE lead export → Brevo handoff |
Mondays 09:00 | cloud — deleted 26 Aug |
| Weekly nl be ad performance review verbomatique | Mondays ~11:00 | local |
[CLOUD] Monthly TFUA review |
6th, 09:00 | cloud |
| Notion knowledge layer review | once, 28 Sep | local |
| Verbomatique brevo meta lead check | weekly, Paused | local |
“Weekly nl-BE lead export → Brevo import” is not a task. It is a phrase. It originates in Asana 1217775027491967’s own notes, was copied into this knowledge file, and was then quoted back — by a later session — as if it named something in a registry. Nobody had looked.
Consequences worth keeping: there was never a duplicate export task, so never a double-run risk to clean up; and what actually ran at 09:07 on 24 Aug remains unidentified. Task 1 fits the time and does open Brevo’s contact-list page, but its brief is scoped to en-CA and contains no export step. Task 3 is the nl-BE counterpart but runs at ~11:00. Neither is a clean match and neither should be forced into one.
Verification note, 26 Aug. The two [CLOUD] rows were confirmed independently against the
scheduled-task registry: ids, cron expressions and next-run times all match. The four local rows
cannot be confirmed from a Claude session at all — the registry returns cloud tasks only.
That is not a gap in this table; it is the same invisibility the [CLOUD] prefix exists to
signal, and it means any claim about a local task is only ever as good as the human who read it
off the screen.
Source: desktop scheduled-task list screenshotted by Dries 25 Aug 2026; cloud registry read 26 Aug 2026
The weekly nl-BE handoff task is deleted — 26 Aug 2026
trig_01YATwUuS4j8LmtKkq34FmTh — “[CLOUD] Weekly nl-BE lead export → Brevo handoff
(Mon 09:00 Brussels)”, cron 0 7 * * 1 — has been deleted. It never ran.
It was created 25 Aug as a stopgap: read the leads out of Meta, diff them against Brevo list 2, hand Dries an import-ready CSV, and stop short of the import the connector cannot perform. Its own step 5 told each run to check whether list 2 had gained contacts it did not hand over, and if so to declare itself redundant.
That condition was met on 26 Aug, four days before its first run: contacts 65 and 66 arrived in list 2 automatically via Pabbly. The task was answering a question that no longer needed asking, so it was removed rather than left to run once and retire itself.
What is lost with it, and does not matter: the CSV handoff. What replaced it is better — Pabbly writes the contact directly, with consent and locale, within minutes of submission. What is lost and does matter slightly: nobody now diffs Meta against Brevo on a schedule, so a silent Pabbly failure would go unnoticed until someone looked. The free-tier ceiling (100 tasks/month) is the most likely cause of such a failure. If that becomes a worry, the check to rebuild is a count comparison, not a CSV pipeline.
Source: scheduled-task registry, deleted 26 Aug 2026 at Dries’ instruction
The en-CA weekly ad-performance brief is carrying superseded facts
The prompt of the local “Weekly ad performance review” task was read in full on 25 Aug. It is a detailed and careful brief, and several of its stated facts have since been corrected elsewhere in this project — so every Monday run re-injects them:
- Reddit. It attributes €85.59 over 13 days to the relaunch campaign and says ~€13 remains. Both are the error this file already records. It is now doubly stale: Reddit advertising was discontinued on 26 Aug 2026, so the brief instructs a weekly review of a dead channel.
- Brevo list #3 completeness. It gives a 22 Aug baseline of “21 contacts, only 3 carry CONSENT_MARKETING and AUDIENCE (ids 3, 16, 27)”. Id 3 was moved to list 2 by the locale migration, so the true figure was 2 of 20. List 3 holds 37 as of 26 Aug 2026 18:03, and every one of them now carries AUDIENCE.
Uitgaande klik. It instructs the run not to flag this as misconfigured. That contradicts an open priority task to make it the primary conversion. A standing “do not flag” is the most dangerous kind of stale instruction: it suppresses a real finding silently, every week.- Google Ads figures. The 22 Aug snapshot (2,441 impressions, 1.69% CTR) invites a false declining-CTR reading, since essentially all lifetime delivery happened in one week.
- Meta Leads Center. It says the terms screen may be unset up and instructs the run not to accept it. Accepted 25 Aug and confirmed gone 26 Aug; the fallback is no longer needed.
- Its Asana anchor. It tells each run to find a recurring task called “Weekly channel performance review and reallocation” — which another open Asana task states does not exist.
The general shape, which is the durable lesson: a long-lived scheduled prompt is an unversioned copy of the knowledge file. It goes stale silently, and unlike the knowledge file nothing prompts anyone to re-read it. Any fact worth putting in a recurring brief should either be re-derived at run time or carry the date it was true.
Dries disabled this task on 26 Aug 2026, rather than patching the prompt. Two of the six
defects above justified that on their own: the brief planned around a channel discontinued the
same day, and its standing “do not flag Uitgaande klik” instruction was actively suppressing a
live conversion finding every week. A review that reports on a dead channel while silencing a
real one is worse than no review.
This is Dries’ report, not a verified state. It is a local Cowork task, and no Claude session can read local tasks — the registry returns cloud tasks only, so nothing here can confirm it is off. The next Monday it would have fired was 31 Aug.
If it is ever re-enabled, the prompt still needs the six repairs above — disabling froze the staleness, it did not fix it. The durable lesson stands regardless: a long-lived scheduled prompt is an unversioned copy of this file, and it goes stale silently.
Source: full prompt supplied by Dries 25 Aug 2026, read against this knowledge file; figures re-checked 26 Aug 2026