Finding real purpose with impact mapping

I’ve always had a soft spot for structured thinking tools, but there’s one that stands out when it comes to bringing real clarity into messy product work: Impact mapping.

Most projects don’t actually fail because teams can’t build things. They fail much earlier, at the point where nobody has really agreed on why they are building anything in the first place. There is often a surface-level purpose, usually something like “replace legacy system X” or “build a new app for customers”. That kind of goal sounds concrete, but it hides a bigger problem: it describes activity, not value. It explains what we are doing, not what we hope will change.

What’s usually missing is the real purpose. The uncomfortable but necessary question: what do we want this investment to give us in return?

That question is where things start to get interesting, and also where many teams accidentally skip ahead. Because the real purpose is rarely about the system itself. It lives one level above it. It could be something like reducing customer effort, increasing conversion, lowering support dependency, improving retention, or enabling faster decisions. And importantly, it has to be something that matters outside the delivery team. If the purpose only makes sense inside IT or product, it tends to drift into output thinking again.

Finding that purpose is less about creativity and more about structured conversation. It usually starts with pulling the right people into the same room, especially those who understand the business side, not just the technical constraints. Then it becomes a process of asking slightly uncomfortable follow-up questions. What changes if this succeeds? Who benefits? Why does that matter? And if we achieved that, what would that enable next?

It helps to keep stripping away layers until you reach something that still feels meaningful without referencing the solution. If the discussion keeps returning to features or systems, it is often a sign that the real goal is still hiding underneath.

Once that purpose starts to feel real, the next challenge is making sure it is actually something the product can influence. A purpose that is too broad, like “increase company profitability”, is not useless, but it is too far removed from day-to-day design decisions. On the other hand, a purpose that is too narrow, like “reduce number of clicks in form X”, risks becoming local optimization without strategic relevance.

The sweet spot is when the purpose is clearly connected to business outcomes, but still reachable through changes in user behavior and experience. It should feel like something the product can genuinely move, even if it does not fully control it.

From there, the thinking naturally shifts into mapping. This is where Impact mapping becomes especially powerful. You take that purpose and start asking who needs to act differently in order for it to happen. Not abstract personas, but real actors with intentions. Customers, internal users, partners, support staff. Then you ask what behaviors from those actors would actually create the desired outcome.

This is where things start to become measurable, because behavior is observable. Not opinions, not intentions, but actions. If customers complete a purchase without contacting support, that is measurable. If users return weekly instead of dropping off after the first session, that is measurable. If employees resolve tasks without escalation, that is measurable.

The important shift is that success stops being defined by whether features were delivered, and starts being defined by whether real-world behavior changed in the direction of the purpose. That question becomes central: if all this happens, will we be considered successful?

And that is where the most useful measures tend to emerge. Not vanity metrics or isolated activity counts, but signals of actual usage over time. Because real value shows up when people interact with the product in their everyday life, not in a demo, not in a workshop, not in a test environment.

It is in the repeated patterns that meaning appears. Do users come back without being pushed? Do they rely on the product to complete tasks they previously did manually? Do they trust it enough to make it part of their routine? These are the kinds of signals that tell you whether the product is actually doing its job in the world, not just whether it works technically.

When teams get this right, design work becomes less about debating features and more about shaping behavior. Every design decision can be traced back to a chain: this supports this behavior, which contributes to this impact, which serves this purpose. It doesn’t remove complexity, but it gives it direction.

And that is why Impact mapping keeps feeling so valuable. It doesn’t pretend to simplify reality. It just makes sure everyone is aiming at the same version of it.

building with hermes agent : hardening the permission approval bridge

Image descr iption

i found a bug that could crash your ai agent and fixed it

i contributed a bug fix to the hermes agent project that makes the permission approval system more robust and reliable. the change handles an edge case where the permission request could return none instead of a valid response, which could cause the agent to crash or behave unexpectedly.

the fix adds proper error handling so that when the permission system gets an unexpected none response, it defaults to denying the request safely. this is a small but important improvement that makes hermes agent more stable in production environments.

why this matters for the community

when you run hermes agent and it needs to ask for permission before running a command, it uses a bridge system to communicate with the approval callback. if something goes wrong in that communication and the system gets a none response, the old code would fail silently or throw an error.

by adding a simple check that returns deny when response is none, we ensure that the agent always has a safe fallback. this prevents crashes and keeps the permission system predictable. users can trust that their commands will either be approved or denied, never left hanging.

this kind of defensive coding is important for an agent that runs on your own infrastructure and handles sensitive operations. every edge case handled well means fewer surprises in production.

code

here are the files i changed:

the main fix in acp_adapter/permissions.py adds a guard clause that checks for none and returns deny safely.

        if response is None:
            return "deny"

the test file tests/acp/test_permissions.py adds a regression test that ensures this behavior is covered going forward.

Image descr iption

you can find the full merged pull request here:




fix(permissions): handle None response from ACP request_permission

#13457

What does this PR do?

This PR hardens the ACP ? Hermes permission-approval bridge by safely handling an unexpected None result from
equest_permission, preventing attribute errors and defaulting to a safe deny.

Related Issue

Fixes #13449

Type of Change

  • [x] ?? Bug fix (non-breaking change that fixes an issue)
  • [ ] ? New feature (non-breaking change that adds functionality)
  • [ ] ?? Security fix
  • [ ] ?? Documentation update
  • [x] ? Tests (adding or improving test coverage)
  • [ ] ?? Refactor (no behavior change)
  • [ ] ?? New skill (bundled or hub)

Changes Made

  • Return “deny” when
    equest_permission resolves to None in the approval callback.
  • Add a unit test covering the None response case to ensure the callback denies safely.

How to Test

  1. Connect via an ACP client that sends an empty response to permission requests.
  2. Verify the permission is denied rather than throwing an exception.

Checklist

Code

  • [x] I’ve read the Contributing Guide
  • [x] My commit messages follow Conventional Commits
  • [x] I searched for existing PRs to make sure this isn’t a duplicate
  • [x] My PR contains only changes related to this fix/feature (no unrelated commits)
  • [x] I’ve run pytest tests/ -q and all tests pass
  • [x] I’ve added tests for my changes (required for bug fixes, strongly encouraged for features)
  • [x] I’ve tested on my platform

Documentation & Housekeeping

  • [x] I’ve updated relevant documentation (README, docs/, docstrings) � or N/A
  • [x] I’ve updated cli-config.yaml.example if I added/changed config keys � or N/A
  • [x] I’ve updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows � or N/A
  • [x] I’ve considered cross-platform impact (Windows, macOS) per the compatibility guide � or N/A
  • [x] I’ve updated tool descriptions/schemas if I changed tool behavior � or N/A

and the repository is here:

GitHub logo

NousResearch
/
hermes-agent

The agent that grows with you

Hermes Agent

Hermes Agent ☤

Documentation
Discord
License: MIT
Built by Nous Research
中文

The self-improving AI agent built by Nous Research. It’s the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions. Run it on a $5 VPS, a GPU cluster, or serverless infrastructure that costs nearly nothing when idle. It’s not tied to your laptop — talk to it from Telegram while it works on a cloud VM.

Use any model you want — Nous Portal, OpenRouter (200+ models), NovitaAI (AI-native cloud for Model API, Agent Sandbox, and GPU Cloud), NVIDIA NIM (Nemotron), Xiaomi MiMo, z.ai/GLM, Kimi/Moonshot, MiniMax, Hugging Face, OpenAI, or your own endpoint. Switch with hermes model — no code changes, no lock-in.

A real terminal interface Full TUI with multiline

…

how it works

the approval callback function handles permission requests from the ACP system. when a command needs approval, it runs a coroutine to get the response from the permission handler.

in the try block, if the coroutine completes successfully, the code checks if the response is none. if it is none, the function immediately returns deny.

this approach is straightforward but effective. it means that even in unusual situations where the permission system does not return a proper response, the callback always produces a valid result. the agent continues running instead of crashing.

the test confirms this behavior by mocking the coroutine to return none and verifying that the callback returns deny.

my tech stack

the changes were made using python with the standard library asyncio module for handling asynchronous permission requests. the test uses pytest with mock objects to verify the behavior without needing a full system setup.

lessons learned

working on this fix taught me a few things about building reliable agent systems.

first, edge cases matter. in a system that handles permissions, every unexpected input should have a clear handling path. crashing is never the right answer.

second, testing edge cases is just as important as testing happy paths. the regression test ensures that this none response case keeps working correctly as the codebase evolves.

third, the hermes agent codebase is well structured and welcoming to contributions. the team uses copilot for code review, which helps catch potential improvements early.

i am glad to have contributed to an open source project that gives developers control over their own AI agents. the hermes agent challenge is a great way to get involved and learn about agentic systems.

thanks for reading.

if you want to explore hermes agent or contribute to it, check out the github repository linked above.

and thanks again …

How I Automated Away My Weekly Status Reports with Next.js and Llama 3.1 🐈

Fabric main page

Context

As a founding engineer, I was spending too much time explaining what I was doing instead of building features. My clients are non-technical, and status meetings were distracting for my deep work.

Instead of just complaining about the micromanagement, I built Fabric 🐈.

Fabric main page

What it does

Fabric connects to your GitHub account (via Octokit) and uses Next.js, Groq, and Llama 3.1 to transform raw commits from any branch/date range into clean progress reports ready for your clients or managers.

Built for the security-paranoid:

  • It never touches your code, only commit metadata. All you need is a GitHub token with strict read permissions.

  • This is a self hosted tool, not a SaaS. I didn’t want to deal with third party compliance or store your repository data.

  • Best suited for freelancers, indie consultants, and small teams (2-10 people) without a dedicated product manager.

Fabric report generation

Limitations:

  • It only supports GitHub at the moment (GitLab/BitBucket are on the roadmap).
  • The report quality depends heavily on your commit messages. It gives you a solid 90% starting point to tweak before sending.

I used to build these weekly summaries manually by hunting down commits and copying them into Notion and ChatGPT. Now, I just run Fabric and get the clean markdown.

Get Started

The project is fully open source. Check out the repo to host it locally or contribute:

GitHub: https://github.com/franciscoluna-28/fabric-ai

Thanks! I would love to hear your thoughts or feedback on the idea.

Why Developers Should Learn How Systems Fail

Most developers spend years learning how to build software, but far fewer spend time studying how software breaks. Yet some of the most valuable engineering lessons come from failure rather than success.

Modern applications are incredibly complex. A simple user action can trigger frontend rendering, backend services, APIs, databases, cloud infrastructure, caching systems, authentication layers, and third-party integrations all within seconds. When one small component fails, the effects can spread quickly across the entire system.

Understanding failure is what separates someone who can write code from someone who can build reliable systems.

Failure Is a Normal Part of Software

One of the biggest misconceptions in software development is the idea that stable systems are systems without errors. In reality, even the largest technology companies experience outages, deployment failures, database corruption, memory leaks, and scaling problems.

The difference is not whether failures happen. The difference is:

  • How quickly teams detect problems
  • How effectively systems recover
  • How much damage failures cause
  • How well developers learn from incidents

Experienced engineers expect failure and design systems accordingly.

Debugging Builds Deep Technical Knowledge

Many developers improve rapidly when they are forced to debug difficult production issues. A broken system exposes hidden details that are easy to ignore during normal development.

For example, debugging may teach:

  • How HTTP requests actually move through infrastructure
  • Why database indexes matter
  • How memory management affects performance
  • What race conditions look like in real systems
  • Why caching creates unexpected bugs
  • How distributed systems behave under stress

These lessons often stay with developers far longer than theoretical explanations.

Logs Are One of the Most Valuable Engineering Tools

Beginners sometimes underestimate logging because it feels secondary to writing features. In production environments, logs often become the primary source of truth during incidents.

Good logging can answer critical questions:

  • What failed?
  • When did it fail?
  • Which users were affected?
  • Did another service trigger the issue?
  • Was the failure gradual or immediate?

Poor logging turns debugging into guesswork.

Strong engineering teams treat observability as part of product quality rather than an afterthought.

Small Mistakes Can Create Massive Problems

Some of the largest outages in tech history started with surprisingly small issues:

  • A missing database index
  • Incorrect cache invalidation
  • Expired certificates
  • Infinite retry loops
  • Misconfigured DNS settings
  • Faulty deployment scripts

This unpredictability is why careful testing and monitoring matter so much. Software systems often fail in ways developers never originally imagined.

Resilient Systems Are Designed Differently

Developers who understand failure begin designing applications with resilience in mind.

Instead of assuming everything will always work, they ask:

  • What happens if this API becomes slow?
  • What if the database temporarily disconnects?
  • Can this queue handle traffic spikes?
  • What happens during partial outages?
  • Is there a rollback plan?

This mindset changes architecture decisions completely.

Features become more reliable because developers stop designing only for ideal conditions.

Failure Improves Team Culture

Teams that openly analyze incidents often become stronger over time. Blameless postmortems help developers focus on improving systems rather than attacking individuals.

Healthy engineering cultures encourage discussions like:

  • Which warning signs were missed?
  • Which monitoring tools failed?
  • Could recovery steps be automated?
  • Were alerts useful or noisy?
  • How can similar issues be prevented?

This process gradually improves both technical systems and team communication.

The Best Engineers Stay Curious About Problems

Some developers avoid difficult bugs because they are frustrating or time-consuming. Others become deeply curious about why failures happen.

That curiosity usually leads to growth.

Understanding system failures teaches developers about architecture, scalability, infrastructure, networking, security, and performance all at once. It transforms debugging from a stressful task into an opportunity to understand technology more deeply.

Final Thoughts

Software development is not only about creating features. It is also about building systems that survive real-world conditions.

The developers who grow the fastest are often the ones willing to investigate crashes, analyze outages, and study failures carefully. Every broken deployment, unexpected bug, or production incident contains lessons that improve engineering judgment over time.

Reliable software is rarely built by developers who never encounter failure. It is usually built by developers who learned from it repeatedly.

How Misconfigured Docker Ports Bypass Every Firewall You Set Up – Stealthy vulnerability

Misconfigured Docker Ports Bypass Every Firewall

Originally published at fatihsennik.com

You were running your own servers. You thought you understood firewalls such as UFW rules, iptables, Cloudflare in front of everything. You’d done the reading. You knew what you were doing.

Then you set up Docker on a new server and watched it silently undo all of it.

This isn’t a hypothetical. I found this the hard way, on a friend’s server, after something was already listening on a port that had no business being open.

How It Started

The server was a fresh Ubuntu VPS. My friend had a Laravel application, a MySQL instance, a Redis cache, and a private admin panel they definitely did not want public. Standard setup. He had configured UFW before installing anything:

ufw default deny incoming
ufw allow ssh
ufw allow 80
ufw allow 443
ufw enable

Clean. Only ports 80, 443, and SSH open. He checked with ufw status and it looked exactly right. He ran nmap from my local machine against the server IP and the output confirmed it, three ports, nothing else.

Then He installed Docker, deployed the stack with Docker Compose, and moved on.

Three weeks later, a routine audit on their side flagged something in network logs. Traffic was hitting their server on port 6379. From an IP address in Romania. ⚠️

Redis. The cache that’s supposed to be internal-only. Completely exposed to the internet. 😱

Misconfigured Docker Ports Bypass Every Firewall

What Docker Does to Your Firewall

This is the thing that trips up a lot of people who come from a traditional server background. Docker doesn’t treat nicely with UFW. It doesn’t go through UFW. It goes around it.

When you map a port in Docker such as ports: 6379:6379 in your Compose file. Docker modifies iptables directly. It inserts its own rules into the DOCKER chain, which gets evaluated before the INPUT chain where UFW rules live. Your UFW deny incoming rule never gets a chance to fire.

The result: UFW says the port is blocked. ufw status says the port is blocked. But the port is actually open and accepting connections from anywhere.

You can verify this by running:

# UFW thinks everything is fine
ufw status verbose

# But iptables tells the real story
iptables -L DOCKER -n

You’ll see entries like:

ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:6379

That’s Docker accepting connections from anywhere (0.0.0.0/0) and forwarding them to the Redis container. UFW knows nothing about it.

This behavior is documented. Docker’s official docs mention it. But it’s buried, and most tutorials don’t bring it up at all. So you follow a guide, set up UFW, feel secure, and then publish Redis or MySQL or your admin panel to the entire internet without realizing it.

What an Attacker Finds

Redis with no authentication which was the default for a long time, and still is if you don’t configure it is one of the most exploited services on the internet. Shodan lists hundreds of thousands of exposed Redis instances at any given moment.

What can someone do with access to an unauthenticated Redis? More than you’d expect.

If it’s a Laravel app, the session data is probably in Redis. Someone with read access to Redis can steal sessions and log in as any authenticated user by including your admin accounts without knowing a single password.

Worse: if the Redis instance has write access and the server is running as root or a privileged user, there are known techniques to write SSH keys into the authorized_keys file via Redis. At that point it’s full server compromise, not just a data breach.

In my friend’s case, the logs showed connection attempts and some key enumeration. We couldn’t confirm with certainty whether sessions were read. We had to assume they were, force-logout every active user, rotate all credentials, and notify the affected accounts. Not a fun conversation to have.

The Fix: Bind to localhost, Not the World

The cleanest solution is to stop publishing internal services to 0.0.0.0 in the first place.

In your docker-compose.yml, any service that doesn’t need to be publicly accessible should bind only to localhost:

services:
redis:
image: redis:7-alpine
ports:
- "127.0.0.1:6379:6379" # localhost only — not 0.0.0.0
command: redis-server --requirepass your_strong_password_here

mysql:
image: mysql:8
ports:
- "127.0.0.1:3306:3306" # same principle
environment:
MYSQL_ROOT_PASSWORD: your_root_password

The 127.0.0.1: prefix tells Docker to bind the host-side port only to the loopback interface. Docker still creates its iptables rules, but they only accept connections from localhost not from external IPs.

For services that don’t need to be exposed on the host at all (Redis accessed only by other containers in the same Compose stack), just remove the ports mapping entirely and use Docker’s internal networking:

services:
app:
image: your-app
depends_on:
- redis
- mysql
# No ports needed here for internal communication

redis:
image: redis:7-alpine
# No ports: mapping — only accessible within Docker network
command: redis-server --requirepass your_strong_password_here

mysql:
image: mysql:8
# No ports: mapping

Containers in the same Compose project can reach each other by service name (redis:6379, mysql:3306) without any host port mapping. There’s nothing to expose.

Fixing Docker’s iptables Behavior at the Engine Level

If you want UFW to actually control Docker traffic, there’s a configuration option in the Docker daemon that stops it from touching iptables directly:

Create or edit /etc/docker/daemon.json:

{
"iptables": false
}

Then restart Docker:

systemctl restart docker

The problem with this approach: now Docker’s internal container-to-container networking can break too, because Docker relies on its own iptables rules for routing between containers and for NAT. You’d need to manage that routing manually with your own iptables or nftables rules. For most people this creates more problems than it solves. So please ingore it.

The Audit You Should Run Right Now

If you have Docker running on any server, check what’s actually exposed at the iptables level, not just what UFW thinks is exposed:

# See all open ports on the host (Docker and non-Docker)
ss -tlnp

# See specifically what Docker has added to iptables
iptables -L DOCKER -n --line-numbers

# Or just scan yourself from outside
nmap -p 1-65535 your.server.ip

That last one is the most honest. Run it from a machine outside your network and see what actually responds. If you see ports you didn’t intentionally open, you have the same problem I found on that my friend’s server.

Also check your Compose files for any service with a ports: mapping that uses only a port number or 0.0.0.0:


`# Find potentially dangerous port mappings in your Compose files
grep -r '"[0-9]*:[0-9]*"' /path/to/your/compose/files
grep -r "'[0-9]*:[0-9]*'" /path/to/your/compose/files`

One More Thing That Makes This Worse

The default Docker network (172.17.0.0/16) is also reachable from other containers, even across different Compose projects on the same host. If you’re running multiple client applications on one server, a compromised container in one project can potentially reach services in another project if they’re all on the default bridge network.

Use named networks and keep projects isolated:

networks:
internal:
driver: bridge

services:
app:
networks:
- internal
redis:
networks:
- internal

This won’t stop everything, but it reduces the blast radius significantly if something does get compromised.

What I Changed After This

On every server where I deploy Docker now, my checklist before going live:

  1. Run nmap -p 1-65535 against the public IP from an external machine
  2. Verify every ports: mapping in every Compose file has 127.0.0.1: prefix or is removed entirely
  3. Redis and MySQL never have host port mappings unless there’s an explicit reason
  4. All services that only talk to each other get put on a named internal network with no host exposure
  5. Redis always runs with --requirepass even on internal networks

Most of this takes five minutes to get right. The problem is that most Docker tutorials skip it entirely, they show you ports: "6379:6379" and move on, and nobody mentions that you just published your cache server to the planet.

Run the nmap scan. Not later, now. It takes thirty seconds and you might not like what you find.

Per-Section Briefs: How to Stop AI Agents Losing the Plot at 2000 Words

In March 2025 I wired up my first long-form content agent. One prompt. A 2000 word target. A list of seven H2s pasted in at the top. The first 600 words read fine. By word 1200 the model had quietly forgotten the original thesis. By word 1800 it was paraphrasing the introduction back at me, just with worse vocabulary.

That article never shipped.

TLDR

Instead of asking an LLM to write a 2000 word article in one shot, give each H2 section its own brief, generate the sections in parallel, then stitch the result together. The model only has to hold one section at a time. You can regenerate any failing section without burning the entire piece.

I have shipped over 50,000 long-form articles through this pattern in production for Articfly. Every time I have tried to skip a step and collapse back to a single prompt, output quality dropped. Here is the full pattern and the failure modes it actually solves.

Why single-prompt long-form fails

What goes wrong when you push past 1000 to 1500 words in a single completion:

  • The model loses recency. Anything in the first 30 percent of the system prompt drifts out of working attention.
  • Internal repetition explodes. Without a section-level scope, the model rehashes earlier points to fill the word budget.
  • Transitions degrade. The model strings paragraphs together with generic connectors because it has no scoped goal for the current section.
  • Errors compound. If section three is weak, section four often inherits the same weakness because the model treats its own earlier text as ground truth.

This is not a model-size problem. I tested the same single-prompt setup with GPT-4o, Claude Sonnet, and Gemini 1.5 Pro. All three drifted past the 1200 word mark. The mechanism is structural, not capability based. You can throw more parameters at it and the failure mode moves from word 1200 to word 1500. It does not go away.

The pattern

Three moving parts:

  1. An outline pass that produces a list of H2 sections, each with a target claim, supporting evidence, and a one-line transition
  2. A section pass that writes one H2 at a time using only the brief and the article-level voice profile
  3. A stitch pass that joins the sections, fixes paragraph-level transitions, and runs a final consistency check

Three calls per article, not one. Sometimes four if you add a fact-check loop. The token cost is only about 50 percent higher than the single-prompt approach because each call is shorter and tighter. You pay for the structure, not for raw token volume.

Step 1: The outline

The outline is the most important call in the pipeline. Get this right and the rest is mechanical.

Outline prompt template:

Topic: <topic>
Primary keyword: <keyword>
Target length: 2000 words
Voice profile: <attached>

Produce an outline with 6 to 8 H2 sections.
For each section provide:
- title (under 60 chars)
- claim (one sentence, what this section argues)
- evidence (2 to 3 bullets, what supports the claim)
- transition (one sentence, how this section leads into the next)
- target_words (integer, must sum to 1800 to 2100)

Output strict JSON.

The key constraints are the claim per section and the target_words sum. Without a per-section claim, the model invents one mid-write. Without a word budget, sections drift to 400 words each and the article overshoots to 3200.

Use a strict JSON output and validate it with Zod or Pydantic. Reject and retry if the word sum is off by more than 10 percent, or if any section is missing a claim. I do not retry the outline silently. If it fails validation twice in a row, the pipeline hard-fails and I see it in logs. A bad outline poisons everything downstream.

Step 2: Write the sections

For each H2 in the outline, fire a section call. These can run in parallel because they do not depend on each other.

js
async function writeSection(brief, voiceProfile) {
  const prompt = buildSectionPrompt(brief, voiceProfile);
  return llm.complete({
    model: "claude-sonnet-4-6",
    system: prompt,
    max_tokens: 1500,
    temperature: 0.7,
  });
}

const sections = await Promise.all(
  outline.sections.map(s => writeSection(s, voiceProfile))
);

The section prompt:

You are writing H2 section "<title>" of a longer article on <topic>.
The full article will be 2000 words. This section targets <target_words> words.

Section claim: <claim>
Supporting evidence: <evidence>
Transition out: <transition>

Voice profile:
<voice>

Constraints:
- Do not restate the article introduction
- Do not preview future sections
- Open with the claim or a concrete example, not a generic setup line
- End with the transition or a sentence that sets it up
- Use first person if the voice profile uses first person
- No headers inside the section (the H2 is handled at stitch time)

Write the section now.

The “do not restate” and “do not preview” lines are load-bearing. Without them, every section starts with “In this article we will explore” and ends with “Next we will look at”. The model wants to be helpful in a way that breaks long-form structure. You have to say no.

Step 3: Stitch

The stitch pass is short. It takes all sections, joins them, and runs one more LLM call to fix paragraph-level transitions and remove duplicate phrases.

js
async function stitch(sections, outline) {
  const joined = sections.map((s, i) =>
    `## ${outline.sections[i].title}nn${s}`
  ).join("nn");

  return llm.complete({
    model: "claude-sonnet-4-6",
    system: `Fix paragraph transitions between sections.
      Do not rewrite. Do not change facts.
      Remove duplicate phrases that appear across sections.
      Replace generic connector words with concrete prose.`,
    user: joined,
    max_tokens: 4000,
    temperature: 0.3,
  });
}

Low temperature here on purpose. The stitch call is an editor, not a writer. If you let it ride at 0.7 it will start adding new content and the article inflates past target length.

The voice profile

The voice profile is what stops per-section calls from sounding generic. I generate it once per customer by scraping 10 to 20 of their published articles and extracting patterns:

  • Average sentence length
  • Vocabulary level (Flesch reading ease)
  • First person versus third person
  • Common rhetorical moves (anecdote opener, contrarian claim, structured list, war story)
  • Banned words specific to the brand

The voice profile gets injected into every section call. Without it, each section sounds like a different writer wrote it. The stitch pass cannot fix that drift, only prevent it upstream. You cannot edit voice into existence at the end.

Voice extraction warrants its own article. I will not cover the extraction prompt here, only flag that it is required for any pipeline that wants outputs to read like one writer.

Tips that matter in production

  1. Cache the outline. If the user regenerates one section, do not redo the outline. Hash the topic plus keyword plus voice profile and reuse the cached outline.
  2. Run section generations in parallel. Latency drops from 90 seconds (sequential) to under 30 (parallel).
  3. Validate section word counts. If a section comes back at 50 percent of its target, regenerate only that section.
  4. Keep the stitch model the same as the section model. Mixing models at stitch time changes the voice subtly and readers notice.
  5. Log each section call separately. When a customer complains about an article, you can trace exactly which section drifted and why.
  6. Token budget the section call hard. If you leave max_tokens unbounded, the model fills the context with one runaway section.
  7. Strip generic closers like “in summary” with a regex after stitch. The model still inserts them about 15 percent of the time even when told not to.

When NOT to use this pattern

Per-section briefs add real complexity. Three LLM calls instead of one. Outline validation logic. Section retry logic. Stitch pass. Section storage so you can recover from partial failure. For some content types this is wasted engineering.

Skip this pattern when:

  • You are generating under 800 words. Single-prompt handles short-form fine.
  • The content is templated (job posts, product descriptions, FAQ entries). A template plus variable injection beats both single-prompt and per-section.
  • You need sub-five-second latency. Three sequential calls plus stitch will not hit that, even with parallelization in the middle step.
  • A human editor reviews every output anyway. The marginal quality gain does not survive a full editor pass.
  • The output is low-quality-bar SEO filler. Per-section briefs do not save bad input topics.

I run a separate pipeline for short-form (under 600 words) that is one tight call with a tight brief. The cost of the agentic pattern is not free. Treating every output the same is its own kind of one-size-fits-all problem.

Stack notes

For anyone wiring this up from scratch:

  • Outline pass: any frontier model works. I use Claude Sonnet because the structured JSON output is reliable across temperature settings
  • Section pass: parallel calls, Claude Sonnet or GPT-4o, temperature 0.6 to 0.8
  • Stitch pass: same model as the section pass, temperature 0.2 to 0.3
  • Validation: Zod (TypeScript) or Pydantic (Python) on the outline JSON
  • Retry policy: 2 attempts per section with exponential backoff. Fail-fast on the outline pass with no retry, just hard fail
  • Storage: I write each section to Postgres as it completes. If the stitch step fails, I can resume without re-running section calls

Total cost per 2000 word article runs about $0.04 to $0.08 depending on model. The single-prompt version cost $0.03 to $0.05. The per-section pattern costs roughly 50 percent more in tokens. The quality difference shipped 50,000 articles. I will eat the 50 percent.

Conclusion

The article that drifted back in March 2025 did eventually ship. After I rewrote the pipeline around per-section briefs, I ran the same topic through. Same target length. Same H2 list. Same voice profile. The result read like one writer wrote it from start to finish.

The single-prompt version is still in my git history. About once a quarter I dig it up to remind myself why three calls beat one.

What pattern do you use to keep long-form AI output coherent past the 1000 word mark?

Bonus : Règles du standard ERC-20

Un token ERC-20 est une valeur gérée par un smart contract qui doit respecter un ensemble précis de règles fonctionnelles afin d’être reconnu et utilisable par l’écosystème Ethereum.

Liste des Règles :

  • Identifier le token : Permet d’identifier clairement le token.

    • Rôle

      • Afficher correctement le token dans les wallets et interfaces.
      • Distinguer un token d’un autre.
    • Fonctions associées

      • name() → nom lisible du token
      • symbol() → version courte du nom
      • decimals() → nombre de décimales utilisées

      Aucune logique métier, uniquement de l’information.

  • Connaître la quantité totale de tokens (total supply) : Indique le nombre total de tokens qui existent pour un token donné.

    • Rôle

      • Garantir la cohérence globale du token.
      • Permettre l’audit et la transparence.
    • Fonction associée

      • totalSupply()
  • Gérer les soldes : Associe chaque adresse à un solde et permet de le consulter

    • Rôle

      • Répondre à la question : « Qui possède combien de tokens ? »
    • Fonction associée

      • balanceOf(address)

      Fonction de lecture uniquement, aucun changement d’état.

  • Transfert direct de tokens : Permet d’envoyer des tokens d’une adresse vers une autre.

    • Rôle

      • Assurer la circulation des tokens.
      • Empêcher les soldes négatifs.
    • Fonction associée

      • transfer(address to, uint256 amount)

        Ici le smart contract :

        • vérifie que l’expéditeur a un solde suffisant,
        • met à jour les soldes dans l’état global.
  • Autoriser un tiers à utiliser des tokens (allowance) : permettre à un propriétaire de tokens d’autoriser un tiers (souvent un smart contract) à utiliser une quantité limitée de ses tokens

    • Rôle

      • Permettre aux smart contracts, exchanges et applications DeFi d’agir pour l’utilisateur.

        💡Les exchanges et les applications DeFi sont des smart contracts qui automatisent des opérations financières, et ils ont besoin d’autorisations pour utiliser les tokens des utilisateurs à leur place.

        • Exchange = échanger un token contre un autre
        • DeFi = faire de la finance avec des smart contracts
      • Sans donner accès à la clé privée.

    • Fonction associée

      • approve(address spender, uint256 amount)
  • Lire une autorisation : Permet de consulter le montant qu’un tiers est autorisé à utiliser.

    • Rôle

      • Vérifier les autorisations accordées.
      • Permettre aux wallets, explorateurs et applications décentralisées de lire et afficher les données du token.
    • Fonction associée

      • allowance(address owner, address spender)

      Fonction de lecture.

  • Transfert par un tiers autorisé : Permet à un tiers autorisé de transférer des tokens pour le compte d’un autre utilisateur.

    • Rôle

      • Rendre possibles les échanges automatisés.
      • Cœur du fonctionnement des exchanges et de la DeFi.
    • Fonction associée

      • transferFrom(address from, address to, uint256 amount)

        Ici le smart contract :

        • vérifie l’autorisation,
        • met à jour les soldes,
        • réduit l’autorisation restante.
  • Traçabilité des actions : Signale les actions importantes liées au token.

    • Rôle

      • Permettre aux wallets et explorers de suivre l’activité.

        💡Un explorer est un site web qui permet de voir et vérifier tout ce qui est écrit sur une blockchain.

      • Rendre le token observable et transparent.

    • Événements associés

      • Transfer
      • Approval
  • Cohérence et sécurité globale : Garantit que toutes les règles précédentes sont respectées en permanence.

    • Rôle

      • Empêcher les incohérences.

        exemple : Alice possède 5 tokens, mais essaie d’en envoyer 10 à Bob.

      • Garantir l’intégrité de l’état global.

    • Implémentation

      • Vérifications internes dans toutes les fonctions du smart contract.

En résumé, le standard BEP-20 définit comment un token est identifié, les soldes sont gérés, les tokens sont transférés, des tiers peuvent être autorisés, et comment ces actions sont rendues visibles à l’écosystème.

How to keep your OTA updates lean and fast

OTA Update optimization

Originally published on expo.dev/blog by Jacob Clausen.

Have you ever shipped a critical fix to your entire user base within minutes of merging the PR – a straight highway from your fix to your users’ fingertips, no bureaucracy, no interruption to the user experience? That feeling never gets old.

EAS Update makes this possible, and once you’ve experienced it, it’s hard to imagine working without it. But publishing the fix is only half the story. Your users don’t benefit until it’s running on their devices. The faster that download completes, the sooner your fix is in their hands. And the leaner your updates are, the better the experience gets for everyone. Faster downloads, lower costs, and a smoother ride as your team and user base grow. In this guide, we’ll walk through how to keep your updates small, ship them with confidence, and get the most out of every push.

EAS Update pricing recap

To get the most out of EAS Update, it helps to understand two concepts that shape how your updates are delivered and billed: Monthly Active Users (MAUs) and bandwidth. Knowing how they work, and the things you can do to keep your updates lean, means faster downloads for your users and lower costs for you. This blog post assumes you’re already familiar with what EAS Update is and what scenarios it’s useful for.

Monthly Active Users (MAUs)

A Monthly Active User (MAU) is a unique user who downloads at least one update via EAS Update within a single billing period. No matter how many updates they download that month, they still count as one MAU. And if a user checks for updates but nothing new is available (so no download happens), they’re not counted at all.

Uninstalling and reinstalling the app will count as a separate MAU the next time they download an update. Similarly, the same person on two different devices would count as two MAUs. These edge cases probably won’t dramatically change your numbers, but they’re good to be aware of if your actual MAU count doesn’t exactly match Play and App Store counts or metrics that consider more context about the unique identities of your users.

Bandwidth

While every update a user downloads consumes bandwidth, the quotas on each plan are generous, and most teams stay comfortably within their plan’s allocation. Actual bandwidth usage tends to be much lower than you might expect, thanks to factors like compression.

The size of that download depends on what changed. For example, if only your JavaScript was updated, that’s all the user downloads. Assets already on the device from the original build (or from a previous update) aren’t re-downloaded.

That number is often smaller than you’d think: if you publish three updates in a week and a user only opens your app once that week, they download one update – not three. Only the latest update is downloaded, and the rest are skipped. From both a speed and bandwidth perspective, this is great – your users aren’t downloading more than they need to, which means updates are delivered faster and you’re only paying for what’s actually being used.

That said, keeping your updates lean is still worth doing for a bunch of reasons beyond billing: faster downloads, less data usage for your users, and a snappier experience all around. More about this later.

One thing we’re really excited about: with SDK 55, you can opt in to Hermes bytecode diffing (currently in beta, please check it out!). Instead of downloading a full JavaScript bundle, the client applies a binary diff to what’s already installed. This can reduce download sizes significantly, meaning your users get updates faster and you can ship more frequently. It’s one of the ways we’re actively working to make updates both cheaper to deliver and better for your users.

Why smaller updates matter

The smaller your updates are, the faster they download for your users – and the less bandwidth each update consumes, both for your EAS Update usage and for your users on cellular or limited data plans. Smaller bundles also mean faster builds, and when your app loads an update on launch, a leaner bundle means a snappier startup. And if your bundle gets large enough, you might even notice it during development – live reload can start to feel slow when it has to process a big bundle on every change. Here are some techniques worth knowing about.

Optimize your assets

Images can add up quickly in your update size, so it’s worth making sure they’re optimized before you ship. Compressing your images where possible can make a meaningful difference – often with little to no visible quality loss. The smaller your images, the faster your updates download and the less data your users consume.

Let your assets be assets

When an image is a proper asset file, it gets downloaded once (either through a build or an update) and stays on the device. Future updates skip it entirely if it hasn’t changed. But when assets are inlined in your JavaScript (like for example base64-encoded images), they’re part of the bundle itself, so they come along for the ride on every update.

For images that change frequently or live outside your bundle entirely, serving them from a CDN paired with expo-image is worth considering. Its default caching policy persists images to disk on first load, so they behave just like bundled assets after that (downloaded once, reused across app restarts) without adding any weight to your updates.

Choose which assets go into your updates

Not every asset in your project needs to be delivered through updates. With asset selection, you can specify exactly which assets should be included using file patterns in your app config. Everything else stays in the native build and never gets downloaded from the update server.

For example, if only the images in your app/images directory change between releases, you can limit your updates to just those:

{
  "expo": {
    "updates": {
      "assetPatternsToBeBundled": [
        "app/images/**/*.png"
      ]
    }
  }
}

Once you’ve configured your patterns, run npx expo-updates assets:verify before publishing. This ensures that all required assets will be included when you publish an update – assets that fall through the cracks won’t be available, which can cause unexpected behavior or crashes. Check out the docs for more details.

Submit to the app stores regularly

When you submit a new build to the app stores, it includes all your latest assets. That means any OTA updates you publish after that only need to include what’s changed since the build, so users download less. If you’ve recently added large or multiple new assets, shipping a new build is a good way to keep your next updates lean.

One approach that works well is pairing a regular binary release cadence (say, monthly) with OTA updates for changes in between. Your users get improvements more frequently, and each new binary resets the asset baseline for your subsequent updates.

Keep your JavaScript bundle in check

OTA Update optimization

Your JavaScript bundle is part of every update you publish. Tools like Expo Atlas give you a visual breakdown of your bundle, showing exactly how much space each dependency takes up. It can be a real eye-opener – you might discover a library you imported for a single utility function is pulling in way more than you expected, or spot a dependency you stopped using but never removed.

You can try it right now with your local dev server. Start your app with Atlas enabled:

EXPO_ATLAS=true npx expo start

Then press shift + m in the terminal running the dev server to open the dev tools plugin menu and launch Atlas.

Shipping updates efficiently

So far we’ve covered how to keep your updates lean. But the way you ship updates matters too. Rollouts give you more control over how updates reach your users, and using updates for internal testing can save you time and build credits.

Use rollouts for production updates

When you publish an update, it’s available to your entire user base by default. Every update your users download counts toward your bandwidth usage, so rollouts are a natural way to be efficient: start with a small group, make sure everything looks good, and then roll it out to everyone.

Use updates for internal testing and PR previews

During development, getting changes in front of your team for review often means creating a new build. But for changes that don’t involve native code, an OTA update does the same job – and it takes only seconds. Your team can preview changes directly on a development build they already have installed, or even from a pull request preview that’s generated automatically.

The more of your review and iteration cycle that happens through updates rather than builds, the less time you spend waiting around before anyone can look at a change. Your team can have the latest version on their device in seconds, which means the feedback loop opens up much earlier in the process. That adds up fast when you’re iterating on a feature, and you’re saving build credits along the way. And since it’s just your team downloading these updates, the MAU and bandwidth cost is practically zero, making it one of the best value-for-effort wins on your plan.

Keeping track of your usage

Everything we’ve covered so far (keeping updates lean, using rollouts, leveraging updates for internal testing) all contributes to getting more value out of your EAS Update plan. But it’s also nice to know where you can see how things are going.

Monitor your usage in the dashboard

Expo provides a usage overview on your account’s Billing and Usage pages. There you’ll find a summary of your EAS Update usage: how many MAUs have downloaded updates and how much bandwidth has been consumed during the current billing cycle (or given time period). It’s a handy way to stay informed, especially if you’re curious how your numbers look after a big release.

Use the pricing calculator to find the right plan

If you’re trying to figure out which subscription plan fits your needs, the pricing calculator on the Expo website can help. Adjust the sliders for your expected MAU count, builds, and CI/CD minutes, and it’ll recommend a plan – and if your numbers go beyond what’s included in that plan, it’ll show you the estimated cost of that extra usage too.

Pick the plan that fits today

You don’t need to have everything figured out before choosing a plan. Start with what fits your current needs – you won’t hit a wall if your app grows faster than expected. Every plan scales naturally with your usage, and you can move between plans as your needs change. Expo’s pricing is designed to grow with you, not get in the way. EAS Update is available on every plan, including the free plan, so you can try it out without any commitment.

Wrap-up

At the end of the day, EAS Update gives you a pretty sweet deal: ship updates to your users in seconds, iterate faster with your team, and scale at your own pace. Leaner updates, rollouts, and a few of the techniques we covered are all it takes to deliver a faster experience for your users while getting more value out of your plan.

Ready to get started? Set up EAS Update in your project – it only takes a few minutes to set up, and you can start shipping updates right away. If you want to dive deeper into any of the topics we covered, the EAS Update docs are a great next step. We’d love to hear about your success story with EAS Update too!

Social Features

Spent today working on a new leaderboard feature for my coding challenge app. A senior dev mentioned that his team tends to engage more when there are social features involved, so I wanted to try adding some.

I started by implementing a review system that lets users rate the multiple choice joke options included in the challenges. It feels like a small but fun way to get some interaction going.

Next up is the leaderboard itself, with settings so users can opt out or choose to display only their initials. That way, people can decide how competitive or anonymous they want to be.

Still figuring out how these social features will affect engagement, but it seems like a good direction to explore. Curious if others have tried similar social tweaks in coding projects and what worked for them.

Ethereum VS BNB Smart Chain (BSC)

Blockchain Ethereum

Ethereum est une blockchain publique conçue pour exécuter des smart contracts et des applications décentralisées.

dApp = application qui fonctionne grâce à des smart contracts sur une blockchain, au lieu de fonctionner sur un serveur central.

Ethereum est la blockchain de référence sur laquelle le standard ERC-20 a été défini.

Le standard ERC-20 (sans code)

ERC-20 est un ensemble de règles que doit respecter un smart contract pour être reconnu comme un token sur Ethereum.

ERC-20 ≠ token ERC-20

ERC-20 = un standard

ERC-20 est une liste de règles.

Il ne contient :

  • aucun token
  • aucun solde
  • aucun contrat déployé

Smart contract ERC-20

Le smart contract :

  • stocke les valeurs
  • applique les règles
  • permet les transferts

Le smart contract du token applique les règles via des fonctions.

Token ERC-20

Par abus de langage, on parle souvent de “token ERC-20”.

💡Distinction claire

  • Standard ERC-20 → liste de règles
  • Smart contract ERC-20 → smart contract respectant le standard
  • Fonctions ERC-20 → implémentation technique des règles
  • Token → valeur numérique associée à une adresse
  • Token ERC-20 → valeur gérée par un smart contract ERC-20

Où sont stockés les soldes ?

Dans un token ERC-20, les soldes sont stockés dans le storage (mémoire permanente) du smart contract, c’est-à-dire dans l’état global de la blockchain, à l’intérieur du compte smart contract.

Techniquement, c’est une structure du type :

balances[address] = nombre

Quand un wallet “affiche” tes tokens, il lit le storage du contrat (via des appels de lecture) et récupère le solde associé à ton adresse.

BNB Smart Chain

BNB Smart Chain est une blockchain publique compatible Ethereum, conçue pour exécuter des smart contracts avec des frais faibles et des transactions rapides.

Elle fait partie de l’écosystème BNB Chain.

BNB Chain ≠ BNB Beacon Chain ≠ BNB Smart Chain

BNB Chain

Le nom de l’écosystème qui regroupe plusieurs blockchains.

BNB Beacon Chain

Blockchain dédiée à la gouvernance et au staking de BNB Chain, sans smart contracts complexes.

💡 Le staking consiste à bloquer des tokens pour participer au fonctionnement et à la sécurité d’une blockchain.

BNB Smart Chain (BSC)

BNB Smart Chain (BSC) est une blockchain publique compatible Ethereum, conçue pour exécuter des smart contracts et des tokens BEP-20, avec des transactions rapides et des frais faibles.

Compatible Ethereum car :

  • elle utilise la même machine virtuelle (EVM = Ethereum Virtual Machine)
  • elle permet d’exécuter les mêmes smart contracts
  • elle utilise le même langage (Solidity)
  • elle utilise les mêmes outils :
    • Metamask
    • Remix
    • Hardhat

Elle reprend également le même modèle de comptes qu’Ethereum.

Le standard BEP-20 (sans code)

BEP-20 est un ensemble de règles que doit respecter un smart contract pour être reconnu comme un token sur BNB Smart Chain.

BEP-20 ≠ token BEP-20

BEP-20 = un standard

BEP-20 définit un ensemble de règles sur la manière dont un token doit fonctionner.

Il ne contient :

  • aucun token
  • aucun solde
  • aucun contrat déployé

Smart contract BEP-20

Le programme qui :

  • stocke les valeurs
  • applique les règles
  • permet les transferts

Le smart contract du token applique les règles via des fonctions.

💡 Par abus de langage, on parle souvent de “token BEP-20”.

Token BEP-20

Un token est une valeur numérique associée à une adresse.

Un token BEP-20 n’est donc pas le smart contract lui-même.

C’est une valeur dont les règles sont implémentées par un smart contract BEP-20 sur BNB Smart Chain.

💡 Distinction claire

  • Standard BEP-20 → liste de règles
  • Smart contract BEP-20 → smart contract respectant le standard BEP-20
  • Fonctions BEP-20 → implémentation technique des règles définies dans le standard BEP-20
  • Token → valeur numérique associée à une adresse
  • Token BEP-20 → valeur dont les règles sont implémentées par un smart contract BEP-20

Règles du standard BEP-20

Un token BEP-20 est une valeur gérée par un smart contract qui doit respecter un ensemble précis de règles fonctionnelles afin d’être reconnu et utilisable par l’écosystème BNB Smart Chain.

Ces règles sont héritées du standard ERC-20 (Ethereum).