fetchmail.mjs → 0 new messages. fetchbrowser.mjs: only the standing SNB921080203146 (ew-rohmund.de) attempted, failed again with function 400 — 12th consecutive occurrence on this exact URL (logged every pass since 2026-09-04); still not escalating since brows…
Germany
Workspace countries/de/ — a market, with its own goals and its own money.
Guardrails
countries/de/country.yaml — every run is admitted against these- On exceed
- alert reported and carried on — API-equivalent dollars are a quota proxy, not money
- A task may declare
- autonomy autonomous · at most 600 turns · at most 240 min
- Per-run ceiling
- $100 — alert only, a finished run is already spent
- Warns at
- 80%
- Refused so far
- nothing has been refused
- Declared, not enforced
- outbound-email, form-submission
Read country.yaml
# Germany. Field reference: ../COUNTRY.md. Falls back to ../defaults.yaml.
identity:
name: Germany
domain: energiemarie.de
language: de-DE
guardrails:
autonomy_allowed: [autonomous]
turns_max: 600 # wasserversorger-discover sits exactly here
minutes_max: 240
budget:
run_max_usd: 100
daily_max_usd: 500
monthly_max_usd: 4000
monthly_spend_max_usd: 50 # real money; 0 while this country is on the subscription
runs_per_day: 40
on_exceed: alert
# Inert until phase 3 — the two rules that matter most for this country are
# "research only, never contact anyone" and "never edit seed.csv".
forbid: [outbound-email, form-submission]
Campaigns
Tasks
the whole fleet| Task | Does | Policy | Next | Last 12 | Spent |
|---|---|---|---|---|---|
| netzbetreiber-enrich | Research German grid operators (Netzbetreiber) and enrich the campaign roster with contact emails and portal details. | autonomous | on demand | $344 | |
| netzbetreiber-send-daily | Send the next wave of up to 50 outreach emails to German grid operators from the campaign roster. Deterministic script, no LLM. | autonomous | in 6d | $0 | |
| netzbetreiber-push-contents | Upsert normalized findings into the contents repo (german_grid_operators). Deterministic script, no LLM; never touches review_status. | autonomous | in 6d | $0 | |
| netzbetreiber-preisblatt | Find, download and extract the published §14a Preisblatt from the websites of German grid operators that never replied to the outreach. | autonomous | on demand | $0 | |
| wasserversorger-discover | Discover which German water suppliers exist and which municipalities each serves — the population-weighted coverage sweep that builds the Wasserversorger roster. | autonomous | on demand | $1,881 | |
| wasserversorger-enrich | Enrich German water suppliers (Wasserversorger) with contact details and the published water-analysis URL, and append results to the campaign JSONL. | autonomous | in 26m | $1,891 | |
| netzbetreiber-replies | Triage new replies from German grid operators and extract data from their answers, then send the queued follow-ups and reminders via the guarded sender. | autonomous | in 1h | $295 |
Spend by month
The brief this workspace runs on
countries/de/CLAUDE.md · every run reads it firstRead CLAUDE.md
Country OS — Germany
Unlike tn/fr, this country dir does not manage a site. It runs data-collection campaigns for the German market (project "Smart research", with Stefanie Schäffer/SEO).
Current campaign: netzbetreiber
Goal: contact data for ~850 German electricity grid operators (Stromnetzbetreiber),
to later request their §14a EnWG module prices. Files live in campaigns/netzbetreiber/:
seed.csv— Stefanie's MaStR export (source of truth for the list; key:MaStR-Nr.). Read-only: never edit this file.enrichment.jsonl— one JSON record per researched operator, append-only. A re-research appends a new record; exports take the latest record per MaStR-Nr.export_csv.py— rendersenrichment.csvfrom the JSONL for import into the team's Google Sheet.
Hard rules
Never invent contact data. A website counts only if verified official (the Impressum names the operator or its Stadtwerke parent). An email counts only if found on the operator's own site or its public MaStR record — never guessed from patterns like info@domain. If nothing verifiable is found, record
contact_status: "no_contact_found"— an honest gap beats a wrong address.Never contact anyone by hand. No form submissions, no hand-written emails, not even "test" ones. Enrichment and discovery tasks are research-only. One exception, and it is not a loophole: the netzbetreiber campaign sends §14a follow-ups through
campaigns/netzbetreiber/followups.mjs send, a deterministic script thenetzbetreiber-repliesskill runs at the end of every pass (its step 7). It only mails drafts already infollowups-proposed.jsonl, it self-skips unless the mailbox is fully triaged, and it re-checks every draft against current data before SMTP (do_not_contact / data already complete / operator replied since drafting → superseded). Running it exactly as step 7 directs is expected and required — withholding it is the deviation. What stays forbidden is composing and sending mail yourself, outside that script. (Reconciled 2026-08-24: this rule predated the sending stage, and its old "this task family never contacts anyone" wording made runs flip-flop on step 7.)Anti-bot policy (decided by Frédéric 2026-07-20): simple challenge questions (arithmetic like "addieren Sie 1 und 6", basic quiz fields) may be answered when sending a legitimate inquiry. reCAPTCHA/hCaptcha/Turnstile/Cloudflare challenges and image captchas are never solved or bypassed — those operators get the email channel or the human queue. Honeypot ("leave empty") fields must never be filled.
One exception, for documents an operator sent us (decided by Frédéric 2026-08-29). A URL that appears in an operator's own reply may be opened with a real browser engine, and the document behind it downloaded, for that operator only. This is retrieval of material handed to us in correspondence, not discovery — the operator chose the address, and the §14a figures behind it are publication-obliged under EnWG anyway. A challenge that resolves on its own when an ordinary browser loads the page is allowed to resolve. A cookie-consent banner may be answered, and is always answered by refusing everything optional — that is a truthful answer about tracking, not a click-through to make the page work.
What stays forbidden is unchanged and applies here too: no captcha is ever solved, answered, or sent to a solving service; no login or paywall is passed; no honeypot is filled; nothing is submitted; and no URL we found ourselves is fetched this way. The browser must be an ordinary one — no stealth plugin, no fingerprint patching, no solver service, and browserless's own
stealthlaunch flag stays off. Those exist and they work; using them is what turns "the page loaded normally" into "we defeated a control", which is the thing this rule is for. If a page needs any of that, it does not "resolve on its own" and the operator goes to the human queue, exactly as before.The preconditions are machine-checked in
campaigns/netzbetreiber/fetch_reply_doc.mjs, never left to a run's judgment: the URL came out of a message whose mastr_nr matches, its host is the operator's own domain or a statutory portal (enwg-veroeffentlichungen.de), the request is a plain document GET, anddo_not_contact.txtis refused outright. Fetching a link by hand bypasses all four, which is why the skill tells you to run the script rather than curl the URL yourself.Second exception, for Preisblätter published on the operator's own website (decided by Frédéric 2026-09-01). The clause above says "no URL we found ourselves is fetched this way". For this campaign's §14a price sheets, that no longer holds, and the reason is the operators' own answer: Stefanie phoned the silent ones and was told, over and over, "das finden Sie auf unserer Website". Three email rounds got no reply, the phone round got a redirect to the website, and §14a figures are publication-obliged under EnWG. So we read the website — both with a plain GET and, where a WAF stands in the way, with an ordinary browser engine.
The host rule is what keeps this from becoming general crawling, and it is unchanged: every URL fetched must sit on the operator's own domain (their verified
website, their email domain, or a brand-sharing sibling persameOperator()) or onenwg-veroeffentlichungen.de. A web search may be used to locate a Preisblatt, but the URL it returns is subject to the same check — hand it tohunt_preisblatt.mjs --mastr … --url …, which enforces it, rather than fetching it yourself. A hit on a third-party host is refused, not argued about; when the search finds nothing on their own domain, go back to the operator's site and look there.Everything the first exception forbids stays forbidden here, without exception: no captcha is solved, answered or outsourced; no login or paywall is passed; no form is submitted; no honeypot is filled; the browser stays ordinary, with browserless's
stealthflag off. Cookie banners are answered by refusing everything optional, as above. A page that does not resolve on its own goes to the human queue. What we are allowed to do is read what an operator chose to publish; we are not allowed to get past anything they put in the way.hunt_preisblatt.mjsperforms the discovery and enforces the host rule anddo_not_contact.txt; a URL it cannot turn into a document lands infetch-queue.jsonl, whichfetch_browser.mjsdrains under this same rule.Record a
sourcefor every field group so freshness re-checks are possible later.As few comments as possible in the code you ship. A comment says why, and only when the reason is genuinely unusual — something a competent reader could not reconstruct from the code itself. Never narrate what the code does, never restate a name, never gloss a self-describing constant. Test: delete the comment and re-read the code; if it still makes sense, it stays deleted. What survives looks like a workaround for one specific device or vendor quirk, or a value that looks arbitrary and is not. Exception: configuration files (PHP config,
.env.example, YAML manifests) — there a comment is the only documentation an option has, so comment freely. This governs shipped code only; PR bodies, register and journal entries are where you explain yourself at length.
Language
Operator websites are German. Search in German (Impressum, Kontakt, Netzbetreiber, Störung). Notes in the JSONL may be English.
Memory
memory/journal.md is the run journal — read it at the start of every run, append
a dated entry at the end (batch processed, hit rate, oddities worth remembering).
Country facts
countries/de/info.yaml — what outreach is allowed to state about us# Country facts — Germany. Single source for anything outreach needs to state # about who we are (forms, signatures, follow-ups). Values verified against # https://energiemarie.de/impressum on 2026-08-09 — update there first, then here. brand: Energiemarie.de legal_entity: Selectra SAS registration: RCS Paris 499807147 manager: Aurian de Maupeou address: street: rue Sébastien Mercier house_number: "66" postal_code: "75015" city: Paris country: Frankreich impressum: https://energiemarie.de/impressum outreach: contact_person: Stefanie Schäffer mailbox: [email protected] # plus-addressing per operator: stefanie+<MaStR-Nr>@ phone: "+33 1 86 26 41 26"
Journal
316 entriesRan buildenrichbatchexcl.mjs 45 20260908-0530: 746 pending, 0 pendingdomain. Split shard A (23) / shard B (22), same population-priority ordering as prior runs. Two parallel top-level subagents worked the shards; each of them further split its 20+ companies in…
Ran buildenrichbatchexcl.mjs 45 20260908-0030: 791 pending, 0 pendingdomain this time (all already resolved), split shard A (23, all self-supplying small Bayern/Hessen/BW Gemeinden) / shard B (22, similar profile). Two parallel subagents researched contact fie…
Batch of 45 (buildenrichbatchexcl.mjs 45 20260907-2030: 836 pending after the 6 standing excluded duplicate-mint pairs, 0 pendingdomain — the campaign's pendingdomain backlog is fully cleared, only the standing wv-de-7505 exclusion remains there). Split 23 (sh…
fetchmail.mjs → 4 new messages, all matched. fetchbrowser.mjs: only SNB921080203146 (ew-rohmund.de) attempted, failed again with function 400 — 11th consecutive occurrence on this exact URL (logged every pass 2026-09-04 through now); still not escalating since…
Monthly report
- Google Doc
- Country OS — Germany
- Updated
- 01/09/2026 08:11
Latest runs
all 369| When | Task | What it reported | Cost | |
|---|---|---|---|---|
| 08/09/2026 07:53 | netzbetreiber-replies | This was a quiet pass: 0 new mail, 0 pending documents, nothing to triage. `fetch_browser.mjs` hit its standing `ew-rohmund.de` function-400 failure a… | $0.42 | ok |
| 08/09/2026 07:51 | wasserversorger-enrich | Journal updated. The run is complete: batch of 45 water suppliers researched and merged, coverage stable, evidence trail intact, no forbidden actions… | $37.58 | ok |
| 08/09/2026 02:50 | wasserversorger-enrich | Journal entry appended. Final summary for the report. Enriched 45 more German water suppliers (wasserversorger campaign): 39/45 got a full contact se… | $14.66 | ok |
| 07/09/2026 22:56 | wasserversorger-enrich | Run complete. Enriched 45 water suppliers (23+22 shards, both validated and merged cleanly): 45/45 got address/phone/website, 41/45 email, 15/45 multi… | $19.20 | ok |
| 07/09/2026 19:34 | netzbetreiber-replies | This run processed 4 messages: 3 full §14a extractions (Stadtwerke Konstanz, Talwerk GmbH/Stadtwerke Kaiserslautern, LokalWerke GmbH — all high-confid… | $1.43 | ok |
| 07/09/2026 18:10 | wasserversorger-enrich | Journal updated. Final report: Enriched 45 German water suppliers (batch tag `20260907-1530`, 2 parallel subagents per shard cap) — phone/address/web… | $34.70 | ok |
| 07/09/2026 16:05 | wasserversorger-enrich | Both files clean. Run complete. **Outcome:** Enriched 45 more Wasserversorger (23+22 across two parallel research passes), bringing `enrichment.jsonl… | $28.33 | ok |
| 07/09/2026 12:54 | wasserversorger-enrich | Journal entry appended. The run is complete. **Outcome:** Enriched 45 more German water suppliers (Wasserversorger) via 2 parallel subagents (23 + 22… | $14.79 | ok |
| 07/09/2026 10:30 | netzbetreiber-push-contents | pushed 511 operator rows to contents (0 awaiting normalization) | $0 | ok |
| 07/09/2026 09:00 | netzbetreiber-send-daily | done: 0 sent, 0 failed → /app/countries/de/campaigns/netzbetreiber/sends.jsonl | $0 | ok |
| 07/09/2026 08:05 | netzbetreiber-replies | This run processed 1 new message: Stadtwerke Norderstedt (SNB985965721965) replied with a link to their own Preisblatt, which I fetched and fully extr… | $0.81 | ok |
| 07/09/2026 08:02 | wasserversorger-enrich | This wasserversorger-enrich run completed cleanly: enriched 45 more water suppliers (2379→2424 of 3375 total, 976 pending), population coverage holds… | $46.20 | ok |
| 07/09/2026 03:07 | wasserversorger-enrich | These are historical batch files from past runs (consistent with the campaign's convention of keeping them); today's own batch files were already remo… | $52.12 | ok |
| 06/09/2026 22:53 | wasserversorger-enrich | Run complete. Enriched 45 more water suppliers (2289 → 2334 distinct, `enrichment.jsonl` 2294 → 2339 lines), all verified as genuine active suppliers… | $17.77 | ok |
| 06/09/2026 19:32 | netzbetreiber-replies | Journal entry recorded. Final report: **netzbetreiber-replies run (2026-09-06 17:30) — quiet pass, no mail to triage.** - Fetch: 0 new inbox message… | $0.52 | ok |