How to Auto-Post on Nostr Without Reading Like a Bot
Three Nostr accounts opened on the same day cannot all post the same shape of content at the same time and expect to land as anything but a coordinated bot push. The technical work of running a self-hosted blog is the easy part. The harder problem is distribution without the marketing-spam fingerprint that most autoposters carry by default.
This article documents the cadence I built for the sovgrid brand account, the deliberate anti-pattern choices, and why three separate accounts (operator, AI co-host, brand) feed Nostr rather than one combined firehose.
Quick Take
- Five posts a week, Monday to Friday, around 20:00 CEST. Skip 8% of scheduled days at random. The point is that the cadence reads as built-by-a-human-with-a-cron, not built-by-a-marketing-team.
- Opener logic is per-article hook cache, not template substitution. Each article gets 3-5 Mistral-generated hooks that lead with a concrete number or fact from the actual content. Templates are bot-fingerprinted within two weeks.
- Pre-publish tone-guard rejects em-dashes, AI-slop adjectives (essentially, fundamentally, fascinating), aphorism couplets, and posts over 500 characters. Same gate as the blog’s quality scoring.
- Three accounts with distinct voice: cipherfox@sovgrid.org (skeptical operator, war stories), hexabella@sovgrid.org (AI co-host, warm translator), blog@sovgrid.org (brand, engineering log). Cross-account replies on launch posts to seed the network graph.
The four bot-fingerprints I am trying to avoid
Most autoposters look like autoposters within a week of operation. Four patterns give them away.
One: same opener-template every post. “If you want one place to start with X…” or “Today’s read: X.” When a reader sees three of these in a row, the autoposter is exposed. Fixed templates substituting article titles is the laziest possible cadence and the easiest to detect.
Two: hashtag-stuffing. Real human posts on Nostr typically carry 1-3 hashtags. Bots stack five to ten because the bot was optimized for SEO without understanding the social cost. The hashtag column at the end of a post is the most visible bot tell.
Three: posting at exact cron-tick times. A post at exactly 18:00:00 UTC every weekday is a cron job. A post at 19:53 one day, 20:11 the next, 19:47 the day after, with the occasional skipped day, is plausibly a human running a cron job. The randomness is the disguise.
Four: marketing speech. “Don’t miss this.” “Must-read.” “Essential breakdown.” This is corporate-tone language a self-hosted engineering log should never use. The reader can hear the marketing department typing.
Each of these is solvable individually. Solving them together is the build.
Architecture: where the logic lives
The cadence script runs from /data/scripts/blog/sovgrid-nostr-daily.py on the Sovereign Grid host. It is triggered by cron Monday through Friday at 17:50 UTC and sleeps a random 0 to 30 minutes before publishing. Effective post window: 17:50 to 18:20 UTC, which lands as 19:50 to 20:20 CEST.
The script reads src/content/blog/*.md, filters by quality score (excludes anything below the published threshold) and by recency (60-day cooldown after a slug was last posted). The eligible pool of ~60 articles never runs out. The selection algorithm is weighted: 30 percent hub articles, 40 percent strategy articles, 30 percent research and fix postmortems.
The 60-day cooldown means each article repeats roughly every 3 to 4 months. By that time the hook the script picks should differ from the last one used, which is where the per-article hook cache matters.
Per-article hook cache: not templates
The naive approach is six opener templates with {topic} substitution. Render "If you want one place to start with {topic}, this is it." with topic = "self-hosted AI on the DGX Spark", and you have a post. Do this 30 times across rotating templates and a reader watching the brand account can see the loop.
The refined approach is per-article hook cache. When the script first selects an article, it sends the article body to the local Mistral instance and asks for 3 to 5 candidate hooks, each 1-2 sentences, each leading with a concrete fact (a number, a named entity, a specific failure). These hooks are cached to config/nostr-hook-cache.json keyed by slug.
Examples from real article hooks generated this way:
- For the Voxtral encoder article:
"Encoder weights stayed gated in Mistral's hosted product. ref_audio crashed the engine. Voice cloning was always closed-source, just labeled otherwise." - For the Qwen3.6 model swap article:
"95.11 tokens per second on a single Spark, Apache 2.0, 22 GB on disk. The math on the model swap." - For the backup post-mortem:
"Six months of automated backups, all of them fake. The script was running. The data wasn't moving."
Per post, the script picks an unused hook for that slug. When all hooks are used, it regenerates a fresh batch. After the 60-day cooldown ends and the article comes back into rotation, the second wave of hooks differs from the first. A reader following the brand account never sees the same opener twice within months.
The Mistral round-trip is cheap because it runs once per new article (not once per post), and is gated behind the quality threshold so the same script that decides whether to post also decides whether to invest the prompt budget.
Multimedia variety: breaking the link-only rhythm
Five link-posts a week, all pointing back to sovgrid.org, is rhythm. Rhythm is exactly what gives an autoposter away to a reader after the third week. The script needs at least one non-link post per week to look human.
The solution is a pre-rendered fun-image pool. A second script, generate_fun_images.py, renders 10 image variations via the same FLUX-schnell ComfyUI pipeline that produces blog hero images. The prompts are character-driven, not article-driven: CIPHERFOX in his basement lab, HEXABELLA walking through an alpine meadow at dawn with source-code fireflies, an exploded isometric of the sovereign-grid stack. Style-consistent with the blog visual vocabulary (neon-green #76b900 accents, alpine and data-center motifs, 35mm cinematic), but the content is mood, not information.
These render in roughly 20 seconds each at 4 steps, so refreshing the pool is cheap. Once a quarter the script reruns with new prompt variations to keep the pool from going stale. Images get uploaded to a Blossom mediaserver (blossom.primal.net) and the URLs live in the daily-pipeline’s image cache.
The integration into the cadence: roughly one in every 5 to 6 posts is image-only. No link, no hashtags beyond #SovereignAI, just an image and a 1-2 sentence caption that picks up the visual. The pool can be hand-pruned (delete the ones that came out boring) without breaking the pipeline. Pure quality control by the operator.
The image-post days do not count against the article-cooldown rotation: they sit alongside the article-link rhythm as deliberate texture-variation. The reader scrolling sovgrid’s feed sees: link, link, image, link, skipped day, link, link. That sequence reads as a person posting from a project, not a bot dispatching a queue.
Tone-guard: same gate as the blog
Before any post leaves the script, a regex pass scans for the anti-patterns from sovereign-kb/mistral-overuse-phrases.md:
- Em-dashes
- “essentially”, “fundamentally”, “ultimately”, “notably”, “interestingly”, “crucially”, “remarkably”
- “fascinating”, “absolutely”, “great question”, “let’s dive in”
- Aphorism-couplets (“X is paid in time. Y is paid in money.”)
- Hashtag count > 3
- Total content length > 500 characters
If a hook fails the gate, the script regenerates from cache (pick a different one) or asks Mistral for a fresh batch. Up to 3 retries. If still failing, the script aborts that day’s post and notifies Matrix. Missing a day is fine; publishing slop is not.
The blog’s quality gate gives the brand the same standards as the long-form content. Distribution that contradicts the content’s discipline is worse than no distribution at all.
Cadence-jitter and skip-probability
The cron entry is 50 17 * * 1-5 (17:50 UTC, Monday through Friday). The script sleeps random.randint(0, 1800) seconds before doing anything. This produces post timestamps between 17:50 and 18:20 UTC. Reader-side this looks like “around eight in the evening”, not “exactly 20:00:00 to the second”.
Within the script, an 8 percent random check skips the day entirely (with a log entry and a Matrix notification). On average, four scheduled posts per year get randomly skipped. The brand reads as “a human running this who sometimes forgets” rather than “a relentless 5-day-a-week machine”.
The randomness is the disguise. The cron is still the cron. The reader’s perception is what matters.
Three accounts, three voices, one ecosystem
Most blog brands run one Nostr account. The sovgrid stack runs three. The reason is voice-coherence, not reach-multiplication.
- cipherfox@sovgrid.org is the operator. Skeptical-engineer voice, war stories, declarative, drops articles (“Pipeline started running”), F-bombs allowed in moderation. Picks the failure stories.
- hexabella@sovgrid.org is the AI co-host on the upcoming podcast. Warm builder, analogy-driven, audience-anchor. Translates the dense engineering log into something a listener can hold without a CUDA driver in their basement.
- blog@sovgrid.org is the brand. Engineering-log voice, deklarative, official-but-not-corporate. Distribution layer for the long-form content.
The three voices stay separate because conflating them collapses the brand into mush. When CIPHERFOX publishes a 0/10 verdict on Voxtral, that lands harder coming from the operator than from the brand. When HEXABELLA introduces herself with “I am not human”, that lands harder from her account than as a paragraph inside a blog post.
For launch posts (first post from each account), the other two accounts leave a reply within an hour. This seeds the network graph: Nostr clients see that the three pubkeys interact, and treat them as a related cluster. Reader-side this looks like “three people who know each other talking about the same project”, not “one publisher pretending to be three voices”. The relationship is real, the staging is timing.
Bootstrap: day 1 through day 7
The first week is curated, not algorithmic. The three hub articles get the first three slots so a new reader following the brand account hits the entry-point content first:
| Day | Article |
|---|---|
| Mon | setup-self-hosted-ai-start-here |
| Tue | strategy-roadmap |
| Wed | strategy-hub-articles-protocol |
| Thu | strategy-tts-pivot-voxtral-ceiling (this week’s frontline story) |
| Fri | strategy-next-model-choices-dgx-spark (yesterday’s main piece) |
From week two, the weighted-bucket algorithm picks freely. The curated bootstrap is just to make sure the first week’s followers see the durable content before the rotation kicks in.
What this is not, and what it might become
This is not engagement-bait infrastructure. There is no “tell me which model YOU use” comment-prompt at the end of each post. There is no “thread incoming” multi-part split when a single post would do. The cadence exists to publish links to durable content with self-aware framing, not to harvest replies.
What it might become, once the daily cadence has accumulated 60 to 90 days of post-history: a feedback loop where the script tracks which articles drew zaps, which drew replies, which drew nothing, and feeds those signals back into the weighted-bucket selection. A bucket-weight tuning loop that learns what reaches readers and what does not.
That is the next stage. For now, the goal is the boring one: post five times a week, not embarrass the brand, and document the build openly enough that a reader can audit whether the cadence-design matches the cadence-output.
Source link and accounts
The script will live at /data/scripts/blog/sovgrid-nostr-daily.py in the cipherfox/sovereign-ops repo on Gitea once shipped. The mirror to GitHub follows on the next deploy cycle. State file is at /data/scripts/blog/.sovgrid-nostr-state.json. The cron entry lives in /etc/cron.d/sovgrid-nostr with a one-line schedule.
The three Nostr profiles, as of 2026-05-12:
- cipherfox: njump.me/cipherfox@sovgrid.org
- hexabella: njump.me/hexabella@sovgrid.org
- sovgrid: njump.me/blog@sovgrid.org
If the cadence drifts into anything resembling the four bot-fingerprints listed above, that is a build failure and a tell that the implementation is not matching the design. The right response is to fix the script, not to lower the bar.
What I Am Building
- Cron-triggered Python script with random 0-30 min jitter, 8% random skip
- Per-article hook cache, 3-5 hooks per slug, Mistral-generated, regenerated after exhaustion
- Tone-guard regex pass identical to blog quality_gate (em-dash, AI-slop adjectives, aphorism couplets, length cap)
- Weighted-bucket selection (Hub 30 / Strategy 40 / Research+Fixes 30) with 60-day cooldown per slug
- Matrix notification on success, Matrix notification on tone-guard failure, Matrix notification on aborted day
- State file as authoritative source for post-history, last-opener tracking, bootstrap-pointer