How did thousands of notes get shared in just 10 days?

Recently I launched ShareText.net on Product Huntand honestly, I was shocked by the response.

Within the first 10 days, thousands of private links were created and shared by users.

**The idea behind ShareText was simple:

  • Instantly share text or notes with a temporary private link
  • No account required
  • Optional password protection
  • Expiration dates
  • View limits
  • Clean and distraction-free experience

**I originally built it because I felt most sharing tools were either:

  • too heavy,
  • too slow,
  • or required too many steps just to send something quickly.

**What surprised me most was seeing how people actually used it:

  • developers sharing logs/snippets,
  • teams sharing temporary notes,
  • people sending sensitive information securely,
  • quick one-time communication without email threads or cloud drives.

This made me realize that sometimes users do not want another “workspace” or “platform.”
They just want a fast tool that solves one problem really well.

Still early, but seeing real adoption from the community has been incredibly motivating.

What do you think made a simple tool like this gain traction so quickly?

Multi-Agent Development Workflows with Claude Code

Single-agent Claude Code is pair programming. One developer, one task, full attention.

I’ve been running three or four agents against a project backlog simultaneously. Not because single-agent broke, but because groomed cards were sitting idle.

Here’s what that looks like in practice.

The shift: from writing code to shaping work

When you use Claude Code as a single agent, you’re pair programming. That’s powerful when you’re exploring a problem or designing an approach. But if you have independent cards groomed and ready, you’re leaving throughput on the table.

Your role shifts. Instead of writing code alongside one agent, you shape the work before it starts and judge it when it’s done. You groom cards, make design decisions, dispatch work, and review output. The agents write the code. Addy Osmani calls this the factory model: you’re no longer building software, you’re building the factory that builds your software. The spec becomes the primary deliverable, and the harness (task tracking, isolation, quality gates, review) is the factory floor.

Steve Yegge’s Gas Town post maps this journey in eight stages, from IDE copilot to building your own orchestrator. I started multi-agent work at stage 6: three or four terminal windows, each running Claude Code on a different card. You realise quickly that you’re the bottleneck. The agents can move faster than you can review, approve, and redirect. The answer isn’t more attention from you. It’s giving the agents more autonomy with safety nets: quality gates that catch problems automatically, structured dispatch so agents find their own work, and a review workflow for when they’re done.

This post is my version of stage 8. The tooling is still maturing, and this harness will look different in six months. This is the April 2026 version.

Anthropic’s 2026 Agentic Coding Trends Report says multi-agent “doesn’t make sense for 95% of agent-assisted development tasks.” That’s probably true for ad-hoc coding. But if you have a groomed backlog of independent cards, running them in parallel is the logical next step to move through the backlog quicker.

Two modes, not a progression

These aren’t stages you graduate through. They’re modes you switch between based on what you’re doing right now.

Thinking mode (single agent)

When you’re exploring, designing, or working through a single problem. Grooming cards, writing acceptance criteria, debugging something complex. The value is in the conversation, not the throughput.

This is pair programming. Full attention on one thing.

Throughput mode (parallel workers)

When you have multiple cards ready to go. Each worker gets a card, a worktree, and runs independently. You review their output when they’re done.

Choose based on card complexity and dependencies:

Sub-agents for small, independent cards (roughly 15-minute tasks):

  • Quick fixes, config changes, bounded features
  • Research running in background
  • Automated code review of completed work
  • Short-lived: no auto-compaction, so longer tasks can exhaust the context window
  • Cheaper: minimal context startup, returns summaries only

Agent teams for substantial cards or cards with cross-card dependencies:

  • Multi-file features that need to read large parts of the codebase
  • Cards where the agent needs sustained autonomy and may hit context limits
  • Each teammate is a full Claude Code session with auto-compaction, so they can sustain longer work
  • More expensive: each teammate loads full project context independently

Agent teams also handle coordination. When cards genuinely depend on each other, teammates can communicate directly via peer-to-peer messaging (SendMessage), shared task lists with dependency tracking, and auto-unblocking. Vertically-sliced stories following the INVEST principle produce fewer cross-card dependencies than horizontal slicing, but they don’t eliminate them. Real dependencies exist even in well-groomed backlogs.

Real coordination cases:

  • One card updates a shared schema; other in-flight cards need to know before they merge
  • Large refactors that can’t be one card, where agents need to agree on new interfaces
  • Adversarial debugging: competing hypotheses where agents share findings

Agent Teams require CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (v2.1.32+) and are still experimental. No session resumption, task status can lag, one team per session.

How to choose

Situation Mode Why
Exploring, grooming, designing Thinking You need the dialogue
One thing that needs full attention Thinking Conversation > throughput
Multiple small, bounded cards ready Throughput (sub-agents) Fast, cheap, parallel
Multiple substantial cards Throughput (agent teams) Full context, sustained autonomy
Cards with cross-card dependencies Throughput (agent teams) Agents need to communicate
Research while you work Throughput (sub-agents) Background tasks
Review of completed work Throughput (sub-agents) Fresh context, separate reviewer

Rate limits are the real parallelism ceiling. They’re pooled across all sessions on your account. Opus has the strictest limits. Plan for this when dispatching multiple workers.

The harness: making throughput mode reliable

Dispatching multiple agents is easy. Getting reliable output is hard. The harness (task tracking, isolation, quality gates, review) is what makes multi-agent development repeatable.

Layer 0: the upstream gate

The most important quality gate happens before any code is written.

Careful grooming is what makes the whole pipeline work. Clear description, specific acceptance criteria, explicit non-goals. As Ankit Jain puts it, “the most valuable human judgment is exercised before the first line of code is generated, not after.”

I spend more time grooming cards than I do reviewing agent output. That ratio feels right. Groom in your main Claude Code session, use the conversation to think through edge cases, and write precise acceptance criteria. The card is the spec.

Layer 1: task tracking

Agents need to discover available work, claim it atomically, and track what’s been tried. A TODO list isn’t enough.

I’m using Beads for this. It stores data locally via Dolt, gives agents programmatic access, and handles dependencies between tasks. The key commands:

  • bd ready lists tasks with no open blockers
  • bd update <id> --claim atomically claims a task
  • bd show <id> gets full card details including previous notes and rejection feedback

A /dispatch skill wraps this into a workflow: find available cards via bd ready, present them for selection, claim each one, and spawn a worker per card with worktree isolation.

For multi-developer setups, a centralized tool (GitHub Issues, Linear) may be more practical. Beads’ strength is agent-native programmatic access. See also The Claude Protocol and Metaswarm for existing harness implementations.

Layer 2: isolation

Without worktree isolation, parallel agents can’t write to the same files. With it, each agent gets its own branch and working directory.

A worker agent definition (.claude/agents/worker.md):

---
model: sonnet
isolation: worktree
background: true
tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
permissionMode: acceptEdits
---

isolation: worktree gives each worker its own git worktree. background: true means the dispatch doesn’t block waiting for workers to finish. model: sonnet keeps costs down for development work (swap to opus for complex cards).

Supporting config:

  • .worktreeinclude copies gitignored files (like .env) into new worktrees
  • WorktreeCreate hooks handle dependency installation
  • Scope each agent via CLAUDE.md to prevent merge conflicts across worktrees

Anthropic’s C compiler case study used this pattern with 16 parallel agents. They hit duplicate work and merge conflicts. Tighter scoping and atomic task claiming address both.

Layer 3: quality gates

Two categories: automated (hooks that block the agent) and manual (human judgment during review). I underestimated how large agent-generated diffs get when the card isn’t tightly scoped. The diff size guard was an afterthought; it’s now one of the more useful gates.

Automated gates (fail-fast pyramid)

Run fastest and cheapest first, most expensive last:

  1. Formatting (PostToolUse on Write/Edit, instant). Auto-fix, not a gate.
  2. Linting / static analysis (seconds). Fast, deterministic.
  3. Type checking (seconds). Catches interface mismatches.
  4. Secret detection (PreToolUse on Edit/Write). Blocks before secrets hit disk.
  5. Unit tests (minutes). The foundation.
  6. Diff size guard (instant). Reject if change exceeds threshold. Prevents comprehension debt.
  7. Automated code review (subagent, 30-90s). Separate agent reviews the diff.

The code review subagent must be a separate agent with its own context window. As Nick Tune writes, “asking the main agent to mark its own homework is obviously not a good approach.” Hamilton Greene’s 9-agent approach achieves roughly 75% useful suggestions versus less than 50% from single-agent review.

Hook implementation:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "./scripts/detect-secrets.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "npx prettier --write "$FILE_PATH"" }]
      }
    ],
    "TaskCompleted": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "./scripts/quality-gate.sh" }]
      }
    ]
  }
}

Exit code 0 proceeds. Exit code 2 blocks with feedback (the agent gets the stderr message and iterates). Lint, tests, and code review fire on TaskCompleted (runs once when the agent says “done”). Secret detection fires on PreToolUse (blocks before the write). See hooks reference.

Manual gates

What automated checks can’t catch:

Layer 4: review gates

For trunk-based development without PRs, the worktree branch is the review surface. git diff main from the worktree shows exactly what would change on merge.

A /review-worktree skill handles this:

  1. Cross-references bd list --label review:pending with git worktree list
  2. Shows commit history and diff summary for the selected worktree
  3. Options: view full diff, view specific file, run tests, run review agent, approve, reject
  4. Approve: merge to main, close card, clean up worktree
  5. Reject: reopen card with feedback comment visible to the next worker via bd show

For PR-based teams, the agent creates a PR from the worktree branch. Standard review process.

Layer 5: the feedback loop

When work is rejected, the card reopens with a comment explaining why. On the next dispatch, the worker agent sees the rejection feedback via bd show. It has context on what was tried and why it failed.

When an agent is stuck, the card goes to blocked with a needs-help label and a note explaining what was tried. The human reviews and either re-grooms the card or splits it.

What it looks like in practice

Here’s a walkthrough on credit-card-lending using Agent Teams. Three cards groomed and ready. The session starts with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 enabled.

Check the backlog and create the team

> bd ready --plain

bd-c4a1  P1  Add payment grace period calculation     customer
bd-e2f7  P1  Fix SSN validation accepting 000 prefix  customer
bd-b8d3  P2  Add CSV export to transaction history     reporting

> Create a team with three teammates, one for each card. Each teammate should use the worker agent definition and get its own worktree.

Creating team "sprint-work"...

Spawning teammate "grace-period" for bd-c4a1...
  Worktree: work/bd-c4a1-payment-grace-period
  Card claimed.

Spawning teammate "ssn-fix" for bd-e2f7...
  Worktree: work/bd-e2f7-fix-ssn-validation
  Card claimed.

Spawning teammate "csv-export" for bd-b8d3...
  Worktree: work/bd-b8d3-csv-export-transactions
  Card claimed.

Team "sprint-work" running. Ctrl+T to toggle task list. Shift+Down to
cycle between teammates.

Monitor progress

The shared task list shows what each teammate is working on. Ctrl+T toggles it:

Tasks:
  [in_progress] bd-c4a1: Add payment grace period calculation (grace-period)
  [completed]   bd-e2f7: Fix SSN validation accepting 000 prefix (ssn-fix)
  [in_progress] bd-b8d3: Add CSV export to transaction history (csv-export)

While teammates work, I stay in the lead session. Groom next sprint’s cards, explore a design problem, whatever needs thinking. Teammates message the lead if they’re stuck or need clarification.

Review completed work

ssn-fix and csv-export have finished. I review each worktree diff from the lead session.

> Show me the diff for ssn-fix's worktree

Commits (main..HEAD):
  a3f8c21 Fix SSN validation to reject 000 and 999 prefixes
  e7b2d14 Add test cases for invalid SSN prefixes

Changed files:
  src/.../customer/validation/SsnValidator.java      | 12 ++++++--
  src/.../customer/validation/SsnValidatorTest.java  | 28 ++++++++++++++++

Small, focused fix. Two files, clear test coverage. Merge it.

> Merge ssn-fix's worktree to main

Merging work/bd-e2f7-fix-ssn-validation into main... done
Closing bd-e2f7... done
Removing worktree... done

Now the CSV export:

> Show me the diff for csv-export's worktree

Commits (main..HEAD):
  b1c4e89 Add CSV export endpoint for transaction history
  d5a7f23 Add PDF export endpoint for transaction history
  f9e1b34 Add export format selection dropdown to UI

Changed files:
  12 files changed, 847 insertions(+), 23 deletions(-)

Scope creep. The card said CSV export. The teammate added PDF export and a UI component.

> Reject. Send csv-export a message: "Card asked for CSV export only.
  PDF export and UI dropdown are out of scope. Revert those changes
  and keep only the CSV export."

Message sent to csv-export. Reopening bd-b8d3...

With Agent Teams, the rejection goes directly to the teammate via SendMessage. The teammate receives the feedback, reverts the out-of-scope work, and resubmits. No re-dispatch needed.

This is a common failure mode: agents are eager to build adjacent features. The tighter the acceptance criteria in the card, the less often this happens.

Where it falls apart

Compound reliability

Each agent at 95% success. Five agents chained: roughly 77%. Multi-agent trades reliability for parallelism. The benefit must justify the overhead.

Context loss between agents

Every handoff is lossy compression. Google Research found 39-70% degradation in sequential multi-agent tasks. Subagents summarize results back to the caller; teammates don’t get the lead’s conversation history. Isolation prevents context pollution but loses nuance.

Token cost

Multi-agent consumes 2-5x more tokens for equivalent work. No published harness has budget limits per task. /usage monitoring is the best we have. This is an unsolved problem.

Time blindness

From the C compiler case study: Claude can’t tell time and will spend hours running tests instead of making progress. The harness needs to print progress infrequently and offer fast-test options.

Duplicate work

Without task claiming, multiple agents fix the same bug independently and overwrite each other. I’ve seen this even with bd’s --claim, when two cards touch overlapping files. The C compiler case study hit it at scale with 16 agents targeting the same bug.

The 18-month wall

Without quality gates, the pattern is: early velocity (months 1-3), plateau (4-9), decline (10-15), stall (16-18) as comprehension debt accumulates. CodeRabbit’s research found AI-generated code produces 1.7x more issues and performance inefficiencies 8x more often than human code. This is why quality gates matter. Without them, the velocity gains are temporary.

The honest tradeoffs

Model lock-in

Claude Code is locked to Claude models. The orchestration layer (sub-agents, agent teams, skills, hooks, worktrees) doesn’t exist in other tools. Your model choice is portable (use Claude API keys with aider, opencode, etc.) but the harness is not. No open-source tool today gives you model flexibility and Claude Code’s agent stack. If you’re invested in this workflow, you’re invested in Claude Code.

When to stay in thinking mode

  • You’re exploring, designing, or grooming. The value is in the conversation.
  • One task that needs your full attention and steering.
  • Cost constraint. Throughput mode is 2-5x more expensive per equivalent output.
  • The work isn’t decomposed into independent cards yet. Dispatch without grooming is waste.

The real cost

Anthropic’s C compiler project: $20K in API costs for 16 agents producing 100K lines of code. That excludes significant human effort for workflow design, task decomposition, agent management, output review, and integration. Budget for both.

What’s next

Today’s harness is human-triggered. You run /dispatch when you’re ready. The next step is agents that continuously pull from the backlog as cards become ready, with the human as reviewer rather than dispatcher.

The pieces exist: bd ready for discovery, worktrees for isolation, hooks for quality, agent teams for coordination. The missing piece is the continuous loop, and the trust to let it run.

Companies with agentic coding infrastructure report 30-50% acceleration in development cycles. But a February 2026 NBER study of nearly 6,000 executives found 89% of firms report zero productivity change from AI. The gap between those groups isn’t model quality. It’s the infrastructure around the model.

That’s been the consistent lesson: harness design matters as much as prompt design.

I Built a DAW and a DJ Tool in the Browser Because My English Teacher Asked

A music production studio with warm lighting and mixing equipment

My English teacher is a musician. The other day he was showing me some of his compositions and said something like “I wish I could just sketch a beat quickly without opening a massive app.” He didn’t want Ableton. He didn’t want to install anything. He wanted to open a tab and start making music.

That stuck with me.

My mother studied at the Escola de Música de Barcelona back in the day. My neighbor produces electronic music in his spare room. Music is everywhere around me, and I kept hearing the same frustration: production tools are either expensive, complex, or require you to upload audio to some cloud service that may or may not respect your privacy.

So I built two tools. Both run entirely in your browser. No accounts, no uploads, no subscriptions.

The Loop Music Creator: a browser-based DAW

The first tool is a full loop-based DAW that runs on the Web Audio API. You get:

  • 7 track types: drums, synth, bass, lead, pad, percussion, and sampler
  • 14 synth presets: from FM piano to Karplus-Strong plucked strings to 8-bit chiptune
  • Step sequencer for drum patterns and a piano roll for melodic tracks
  • Per-track effects: 3-band EQ, compressor, reverb/delay sends
  • Master effects chain: reverb, delay, compressor, master EQ
  • Multi-scene arrangement: organize verses, choruses, bridges
  • Tap tempo and per-track swing for groove
  • WAV export and project save/load (.loop.json format)

Everything is synthesized in real-time. No samples are loaded from a server. The drum sounds are generated using noise synthesis and oscillator shaping, the synths use FM, additive, and subtractive methods depending on the preset.

The architecture

The audio engine is decoupled from React state. A useRef holds the engine instance while a useReducer manages the DAW state. This avoids the classic problem of React re-renders causing audio glitches. The engine schedules notes ahead using AudioContext.currentTime with a lookahead buffer; a technique well-documented in Chris Wilson’s “A Tale of Two Clocks”.

Each track gets its own gain node, panner, and effects chain routed to a master bus. The master bus has reverb (convolution-free algorithmic approach), delay with feedback, a dynamics compressor, and a 3-band EQ.

The project serializer handles AudioBuffer encoding/decoding to base64 for the sampler track; so you can save and share complete projects including imported audio.

A music production studio with warm lighting and mixing equipment

YouTube Loop Mix: a DJ tool in your browser

The second tool is a YouTube Loop Mix & DJ Tool; dual decks with crossfader, just like a real DJ setup.

My neighbor kept asking me “can I just loop a section of a YouTube video and mix it with another track?” He’d find a killer drum break on YouTube, want to loop 4 bars, and blend it with a synth pad from a local file. Every existing tool either required downloading the video first or didn’t support mixing two sources.

The YouTube Loop Mix gives you:

  • Dual decks for loading YouTube videos or local audio files
  • A-B loop markers to loop any section of a track with precision
  • Crossfader for smooth transitions between decks
  • 3-band EQ (low/mid/high) per deck for live mixing
  • Cue points to mark and jump to specific moments
  • BPM detection to help you beat-match tracks
  • WAV export of your final mix

It’s the kind of tool that turns a YouTube rabbit hole into a creative session. Find a breakbeat, loop it, EQ out the highs, crossfade into a melody from your local library; all without leaving the browser.

Sheet music and musical theory notation on a wooden surface

Why browser-based?

Three reasons.

1. Zero friction. My teacher opened the link on his school laptop and was sketching beats within 30 seconds. No “download our 2GB installer” wall. No “sign up with Google” interstitial.

2. Privacy. The audio never leaves your machine. Compare that to online DAWs like BandLab or Soundtrap that upload your recordings to their servers. For someone sketching ideas they might not want public, this matters.

3. Portability. The .loop.json project format is human-readable JSON. You can version control it, diff it, share it on Discord, or even generate it programmatically. I’ve already written a script that creates projects from MIDI data.

The Web Audio API is underrated

Most developers don’t realize how powerful the Web Audio API has become. You get:

  • Sub-millisecond scheduling precision
  • Hardware-accelerated DSP (compressors, filters, convolution)
  • Oscillators with all standard waveforms
  • AnalyserNode for real-time FFT visualization
  • Offline rendering for export (no real-time constraint)

The main limitation is latency on first interaction (browsers require user gesture to start AudioContext), but once running, it’s surprisingly solid for production work. Chrome’s audio worklet even lets you write custom DSP in JavaScript running on a dedicated thread.

Try them

Both tools are free, open to anyone, and process everything locally. If you make something cool with the Loop Creator, you can export the .loop.json and share it; the format is designed to be portable.

How to deploy Next.js app to Hostinger shared hosting and alsoadd a github workflow (ci/cd)

How to deploy Next.js app to Hostinger shared hosting and alsoadd a github workflow (ci/cd)

You can bypass Hostinger’s limitations entirely by building your application on GitHub Actions and automatically deploying the static files via FTP/SFTP. This means you do not need Node.js installed on your Hostinger account, and your local machine does not need to handle the build.

You can bypass Hostinger’s limitations entirely by building your application on GitHub Actions and automatically deploying the static files via FTP/SFTP. This means you do not need Node.js installed on your Hostinger account, and your local machine does not need to handle the build.

📋 Prerequisites Before Setting Up

You must first change your Next.js project to static export mode so GitHub can build it into flat HTML/CSS/JS files.

  1. Open your next.config.mjs (or next.config.js) and add the export configuration:
   /** @type {import('next').NextConfig} */const nextConfig = {
     output: 'export',
     images: {
       unoptimized: true, // Required for static export
     },
   };
   export default nextConfig;

Commit and push this change to your repository.

🔐 Step 1: Get FTP Credentials & Add to GitHub

GitHub needs permission to upload files to your Hostinger server.

  1. Log into your Hostinger hPanel.
  2. Navigate to Websites > Manage > Files > FTP Accounts.
  3. Note your FTP Host, FTP Username, and FTP Password.
  4. Go to your repository on GitHub.
  5. Click Settings > Secrets and variables > Actions > New repository secret.
  6. Create the following three secrets:
    • FTP_SERVER (Your Hostinger FTP Host)
    • FTP_USERNAME (Your Hostinger FTP Username)
    • FTP_PASSWORD (Your Hostinger FTP Password)

🚀 Step 2: Create the GitHub Actions Workflow

This workflow will trigger every time you push to your main branch. It spins up a temporary virtual environment, installs Node.js, builds your Next.js app, and uploads the out folder to Hostinger.

  1. In the root of your local project, create a folder structure named .github/workflows/.
  2. Inside that folder, create a file named deploy.yml.
  3. Paste the following configuration into deploy.yml:
name: Deploy Next.js to Hostinger
on:
  push:
    branches:
      - main # Change this to 'master' if your default branch is named master
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-level: 20
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Build Next.js Static Site
        run: npm run build

      - name: Deploy via FTP to Hostinger
        uses: SamKirkland/[email protected]
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          local-dir: ./out/
          server-dir: ./public_html/ 
          # Use ./public_html/subfolder/ if hosting on a subdomain

🛠️ Step 3: Trigger Your First Deployment

  1. Save the deploy.yml file.
  2. Commit and push the new workflow to GitHub:
   git add .github/workflows/deploy.yml next.config.mjs
   git commit -m "Add GitHub Actions CI/CD deployment workflow"
   git push origin main

Go to the Actions tab on your GitHub repository page to watch the live build progress. Once it finishes green, your site will be updated live on Hostinger.

If you hit any errors during the automated GitHub build or face issues with asset loading, let me know the error message or if you are hosting this on a subdomain so we can tweak the routing configuration.

The WordPress.org freemium trap: how to ship a Pro plugin without getting suspended

If you’ve ever thought about building a freemium WordPress plugin and
distributing the free version through WordPress.org, there’s a compliance
rule that will catch you off guard.

You cannot ship locked features.

Not locked behind a license check. Not hidden behind an if (is_pro()).
Not greyed out with a tooltip saying “upgrade to unlock.” The WordPress.org
guidelines call this trialware, and plugins that do it get suspended.

This is the architecture I built to solve it — for
Laterreta Migrator for Shopify,
a plugin that migrates Shopify stores to WooCommerce.

What the guidelines actually say

From the WordPress.org plugin guidelines:

Plugins may not contain functionality that is hidden or disabled and
waiting to be unlocked by a purchase or license.

The key word is contain. The code cannot be in the plugin at all — not
even dormant, not even behind a flag.

This rules out the most common freemium patterns:

// ❌ Not allowed on WP.org — code is present but locked
if ( has_valid_license() ) {
    download_images( $product );
}

// ❌ Also not allowed — feature exists but is gated
function download_images( $product ) {
    if ( ! is_pro() ) {
        show_upgrade_notice();
        return;
    }
    // actual code...
}

Both of these put Pro code inside the free ZIP. That’s trialware.

The solution: hooks as a bridge
WordPress’s hook system (add_filter / add_action / apply_filters /
do_action) gives you a clean way to make the free plugin extensible
without including the extension code.

The idea: the main plugin fires hooks at every point where Pro behaviour
could happen. In the free build, those hooks have no callbacks — they do
nothing. The Pro build ships extra files that register callbacks on those
hooks.

// In the main plugin (ships in BOTH free and Pro)
// Pro: add image fields to the GraphQL query
$image_fields = apply_filters( 'lmsf_graphql_image_fields', '' );

// Pro: do something after a product is imported
do_action( 'lmsf_after_product_imported', $postId, $product );
In the free build, apply_filters() returns the empty string default
and do_action() fires with zero listeners. No Pro code runs because
there's no Pro code present.

In the Pro build, extra files are included that register callbacks:

// Pro only: includes/pro/images.php  (absent from free ZIP)
add_filter( 'lmsf_graphql_image_fields', function( string $extra ): string {
    return $extra . "nimages(first:10){ edges{ node{ url altText } } }";
});

add_action( 'lmsf_after_product_imported', function( int $postId, array $product ): void {
    lmsf_download_and_attach_images( $postId, $product['images']['edges'] ?? [] );
}, 10, 2 );

The Pro file is physically absent from the free ZIP. There’s nothing
to unlock — the feature simply doesn’t exist in that build.

Two ZIPs from the same source
The build script produces two completely different ZIPs from the same
codebase:

./build.sh # Pro ZIP — includes/pro/ copied in
./build.sh –free # Free ZIP — includes/pro/ deleted before packaging
The free ZIP goes to WordPress.org SVN. The Pro ZIP goes to the product
download on our site. Same source, different outputs.

# Simplified build logic
if $FREE_BUILD; then
    rm -rf "$STAGE_DIR/includes/pro"
    cp "$LICENSE_STUB" "$STAGE_DIR/includes/license.php"
else
    cp -r "$PLUGIN_DIR/includes/pro" "$STAGE_DIR/includes/pro"
    cp "$LICENSE_FILE" "$STAGE_DIR/includes/license.php"
fi

Every Pro feature follows the same pattern
Once the hook bridge is in place, adding Pro features is always the same
three steps:

  1. Add a hook in the main plugin at the right extension point
// Main plugin: migration query builder
$extra = apply_filters( 'lmsf_graphql_product_extra_fields', '' );
$gql   = "query P { products { edges { node { id title {$extra} } } } }";
´´´
2. Create a file in includes/pro/ that registers a callback

php
// includes/pro/tags.php
add_filter( ‘lmsf_graphql_product_extra_fields’, function( string $extra ): string {
return $extra . “ntags”;
});

add_action( ‘lmsf_after_product_imported’, function( int $postId, array $product ): void {
lmsf_tags_apply( $postId, $product );
}, 10, 2 );

3. Build — the Pro file is included, the free file is not

Current Pro features and their hooks:

Feature Filter (adds fields)    Action (saves data)
Image download  lmsf_graphql_image_fields   lmsf_after_product_imported
Custom metafields   lmsf_graphql_product_extra_fields   lmsf_after_product_imported
Tag mapping lmsf_graphql_product_extra_fields   lmsf_after_product_imported
Scheduled sync  lmsf_sync_extra_product_fields  lmsf_sync_product_extra
The license stub pattern
The Pro build includes a license validation system. The free build needs
a license.php file too — but without any external calls or validation
logic.

I maintain two files:

php
includes/license.php — full Pro license validation (LM4WC calls)
includes/license-stub.php — stub that returns safe defaults
// license-stub.php — ships in the free build
function lmsf_get_license_tier(): string { return ‘free’; }
function lmsf_get_license_key(): string { return ”; }
function lmsf_is_license_valid(): bool { return false; }
function lmsf_upgrade_url(): string {
return ‘https://www.laterretagames.com/migrate-from-shopify-to-woocommerce/‘;
}
The build script swaps them:

if $FREE_BUILD; then
cp “$LICENSE_STUB” “$STAGE_DIR/includes/license.php”
else
cp “$LICENSE_FILE” “$STAGE_DIR/includes/license.php”
fi

Same function signatures, different implementations. The main plugin
calls lmsf_upgrade_url() to render the "Get Pro" button — it works in
both builds without any conditional.

What this architecture can't do
Discovery is harder. A user on the free build can't see a list of
Pro features inside the plugin unless you explicitly build a "What's in
Pro" section. The features are absent, not locked — so there's nothing
to surface automatically.

Solution: Build static Pro feature cards into the free dashboard.
Show exactly what the migration didn't do because Pro wasn't active.

php
// Free dashboard — always visible
if ( defined( ‘LMSF_FREE_VERSION’ ) && LMSF_FREE_VERSION ) {
// Show cards: images not downloaded, sync not active, tags not mapped
}

Deactivation hooks need a constant. register_deactivation_hook()
requires the main plugin file path. If Pro files reference it, they need
a constant defined in the main plugin:

php
// Main plugin
define( ‘LMSF_FILE’, FILE );

// Pro: scheduler.php
register_deactivation_hook( LMSF_FILE, ‘lmsf_clear_cron_schedule’ );



Is it worth the complexity?
Yes — for three reasons:

WP.org compliance is non-negotiable. Getting suspended means
losing organic discovery permanently. The hook architecture is
the only clean way to stay compliant.
The architecture is actually better code. Hooks as extension
points is how WordPress itself is designed. Pro features are properly
isolated — a bug in images.php can't affect the migration core.
It scales. Every new Pro feature is a new file in includes/pro/.
No changes to the main plugin required beyond adding an extension point.
The free plugin on WP.org:
👉 [Laterreta Migrator for Shopify](https://wordpress.org/plugins/laterreta-migrator-for-shopify/)

If you're building a freemium WP plugin and have questions about the
compliance side, happy to go into more detail in the comments.

Agent Traces Need to Cross the MCP Boundary | Focused Labs

Agent runtime sends traceparent through MCP into downstream spans.

Observability for AI agents running through MCP has a new failure point: the MCP tool call.

Good. The broad version of this conversation has already been beaten to death. Agents need traces. Agents need evals. Agents need feedback loops. Fine. The sharper production question is what happens when the agent leaves the planner and crosses into a tool server owned by another team, another vendor, another runtime, or another cloud account.

That boundary is where the trace disappears.

Honeycomb is running O11yCon in San Francisco this week. Christine Yen’s line in the announcement gets at the issue: agents are writing code, agents are triaging incidents, agents are running production through orchestration, and engineering has little visibility into what the agents did, let alone whether they added value. The visibility gap for these agents is along the path between the model’s decision, the tool server, and the downstream services affected by the action.

The production shape is distributed tracing with a model in the loop.

A planner says “tool failed.” An MCP server just sees an unrelated tools/call. A database sees a single query. A payment API sees a single request. The observability backend sees all these individual pieces and, operationally, has no idea what to do with them. Nobody can say whether the model chose the wrong tool, the planner’s MCP client lost context somewhere along the line, the server failed to accept the call, or the downstream service simply timed out.

Logs within a given service are comfortable to view because the local nature of the stream makes them easy to interpret. However, as soon as an incident affects multiple services or tools, that comfortable stream of logs disappears.

MCP made tool integration portable. It did not magically make tool behavior observable. Focused has been pushing this shape for a while. In Developing AI Agency, the point was that useful agents need real engineering systems around them. In Streaming agent state with LangGraph, the point was that intermediate state matters while long-running work is happening. MCP adds a protocol boundary to that same production story. If the trace cannot cross it, the agent becomes opaque at the exact moment it starts doing useful work.

MCP gave us the carrier

MCP made tool integration for production tool calls easier. Making the behavior of those calls observable is a different job.

This brings us to a simple and useful place: SEP-414 reserves the W3C trace keys for W3C trace context propagation through MCP. So the MCP tools/call request can include trace context as part of params._meta, next to the tool name and arguments.

MCP typically wants _meta keys that start with a DNS-prefixed name. SEP-414 makes an exception for the three W3C trace keys so existing OpenTelemetry propagation can work without creating twelve slightly different names for the same thing. traceparent stays traceparent, tracestate stays tracestate, and baggage stays baggage.

Tiny standardization, huge operational consequence.

A universal set of properties for W3C trace context is a small thing to request. Without SEP-414, every agent stack invents its own set of properties in params: io.modelcontextprotocol.traceparent, otel_trace_parent, correlation IDs encoded in a vendor envelope, plus the special shape required by a proprietary monitoring stack. The resulting observability swamp would be indistinguishable from what exists today with services and their HTTP traces.

First, the agent runtime starts a new span or continues an existing one. Then the MCP client for that runtime injects W3C trace context into params._meta for the call. When the MCP server processes the call, it extracts the W3C trace context from params._meta. Then the server creates a new server span. Tool code invoked by that server, including API calls to databases, queues, workflow engines, and other services, runs under the same trace context.

Agent runtime sends traceparent through MCP into downstream spans.

The tool boundary is where agent observability either survives or dies.

HTTP spans will not save the agent loop

A tempting shortcut is to assume the transport already has tracing. The MCP server runs over HTTP. The ingress span exists. The collector sees requests. Done.

Nope.

That is why OpenTelemetry’s MCP semantic conventions matter: HTTP spans only contain information about transport. Streamable MCP transports can contain more than one request, and one MCP operation can spread across retries and transports. The transport context and MCP context are related, but different.

A streamable HTTP request can sit under multiple MCP messages. A retry can create multiple transport-level attempts for one logical operation. Stdio has no HTTP request to hang a trace on at all. If instrumentation stops at the transport layer, the team is just looking at plumbing. The production question lives one layer up: what MCP method was called, what tool was called, what session was involved, what error type was returned, and which downstream spans received the trace context.

A trace is useful when it follows the boundary. In the simplest case, a single trace starts with a span created by the agent runtime. The span name should be boring and low-cardinality, with names like tools/call get_weather, tools/call query_customer, or tools/call create_ticket. The attributes carry the information that matters in production: mcp.method.name, gen_ai.tool.name, mcp.session.id, mcp.protocol.version, network.transport, and error.type. OpenTelemetry warns against adding high-cardinality resource URIs to span names by default. That creates backend cardinality problems for no benefit.

The same thing is true for baggage. Baggage is useful for correlation. It is also an attractive nuisance. A tenant hint here, a route class there, an evaluation cohort for a particular set of runs. Fine. But prompts, secrets, user emails, access tokens, and customer data do not belong in baggage because trace context is supposed to cross service boundaries.

Google’s Cloud Trace documentation treats tracing through remote MCP request metadata as an implementation detail. A remote server can accept traceparent in headers or _meta. Once that tracing information is accepted and the trace is sampled, the server emits spans for the requested operation, including failures caused by the agent or by the tool, and latency caused by the client, network, or server processing.

Sampling policy becomes relevant for observability of the agent’s tool work. If the agent’s tool work is not sampled, the tool’s work cannot be reconstructed later by whoever wired up the chat UI.

Fragmented truth still loses the incident

Separate traces can be valid. A vendor-operated MCP server may want a clean service boundary. A client team may not own the server. Langfuse’s docs make that distinction directly: Langfuse’s MCP tracing docs. But default separation is awful for incident management when the agent itself is causing a user-visible problem.

The agent chooses a tool. The MCP server executes the request. The database locks. The tool returns a timeout. The planner retries with slightly different arguments. The user waits. Each system can tell the truth from inside its own box. The operator still has to stitch together causality by timestamps, request IDs, Slack screenshots, and vibes (the official fourth pillar, apparently).

Fragmented traces versus one linked trace across planner, MCP server, and database.

Without propagation, every system tells the truth in isolation.

In production flow, agent traces should form a chain that represents both the decision process for a request and the execution process carried out by services. The tool spans from an agent trace should link to the corresponding service spans. Having the agent’s processing stages with nothing from subsequent services is model theater. Service spans without the corresponding tool decision are classic APM with no agent-specific information.

Honeycomb has been going down a similar route. Their Innovation Week writeup describes agent workflows that branch, retry, call tools, hand off, and trigger services. They frame Agent Timeline. The resulting view places the agent’s work inside the incident loop and shows the causal chain behind a prompt log.

The implementation surface is small

Here is a concise specification for adding distributed tracing to an agent-enabled workflow:

  • inject traceparent into MCP params._meta
  • extract it on the MCP server
  • name spans by MCP method plus stable tool or prompt name
  • attach MCP and GenAI attributes with low cardinality
  • propagate trace context to following API and database calls
  • keep sensitive data out of baggage
  • send the result to a backend that can show agent and service work together

The ecosystem around the MCP contract already does a decent amount of the heavy lifting. Grafana’s MCP server docs include attributes such as gen_ai.tool.name, mcp.method.name, and mcp.session.id, with W3C trace context propagation from _meta. MCP Toolbox telemetry docs cover attributes for MCP method, transport, protocol, toolset, tool name, and error type. LangSmith accepts OpenTelemetry ingestion, which means MCP spans do not have to sit in an observability island away from LangChain or LangGraph applications.

In practice, agent systems run across different runtimes, including planners, graphs, model gateways, tool registries, MCP clients and servers, legacy APIs, databases, queues, approval steps, and eval jobs. Evidence of proper orchestration cannot scatter across architecture components and still be reviewable by team members from AI, platform, service, and business functions. We discussed the tradeoffs in Multi-Agent Orchestration in LangGraph. For trace propagation, the same reasoning applies. Architecture can be decomposed into modular components. Evidence for correct runtime behavior cannot.

A decent review checklist is simple:

  • Can an operator start from a failed agent run and find the corresponding MCP tools/call span for the tool that failed?
  • Can they see the exact tool name without exploding cardinality?
  • Can they jump from the client span to the server span?
  • Can they see the downstream API, database, or queue work under the same trace?
  • Can they distinguish model/tool selection failure from tool/server failure?
  • Can they see error.type, latency, tokens, and quality signals near the same workflow?
  • Can they prove no secrets or PII are leaking through baggage or span attributes?

Call it what it is: a pull request.

The owner is the team that owns the boundary

The trick keeping observability in agent systems stuck is assigning MCP observability to the AI team, or to the MCP tools team, or to the database platform team, while claiming the boundary is too hard for any one team to own.

There are four parties involved here: the vendor of the tool, the platform team, the AI team, and the service team. The vendor exposes spans. The platform team runs a collector to gather those spans. The AI team creates a planner span that is passed as context to tools. The service team instruments downstream API and database calls made by tools within an agent run. Someone has to own the boundary between those groups.

Own the boundary.

For an internal MCP server, trace propagation belongs in the server template for all calls. It should not be left to individual tools. For vendor-provided MCP servers, test the contract by sending traceparent in params._meta and verifying that the backend receives the linked span. Test trace propagation from the agent runtime for every tool call after context injection, without needing to chase separate dashboards. Baggage should have a clear policy before developers discover it as a convenient place to add sensitive information.

AI agent observability will continue to sound mysterious when production monitoring means staring at transcripts of model dialogs. A transcript is one artifact. It will never show the intent behind a command, the tools used to execute it, the side effects, the latency, the errors, or the downstream work required by systems that had to deal with the output of those tools.

MCP made tools portable. SEP-414 and the OpenTelemetry MCP conventions make the tool boundary traceable. The work is wonderfully unglamorous: pass the context, name the spans, control the attributes to keep cardinality low, protect baggage from sensitive information, and then follow the tool calls as the trace crosses the same boundary as the agent.

Follow the trace, follow the agent.

Incognito Mode Won’t Save You — What Actually Protects Browser Privacy

Hero image

Hero image

Every browser has a private mode. Chrome calls it Incognito. Firefox calls it Private Window. Safari calls it Private Browsing. They all do roughly the same thing, and they’re all misunderstood in roughly the same way.

Private mode does one thing well: it stops your browser from storing a local record of your session. No history saved. Cookies deleted when the window closes. Form data not remembered. If you share a computer with someone, private mode keeps your session out of the shared history.

That’s it. That’s the complete protection.

The website you’re visiting doesn’t care whether you’re in private mode. It can’t tell. Your IP address looks identical. Your browser fingerprint is unchanged. Third-party scripts loaded on the page run the same way. Private mode is a privacy feature aimed at people sharing your device — not at the websites you visit or the networks your traffic crosses.

What Private Mode Actually Does (And Only Does)

Private mode protects against a narrow set of real threats, and it’s worth being precise about which ones.

If you log into your bank on a shared computer, private mode ensures your session cookies disappear when you close the window. The next person who opens the browser won’t be logged in as you. On public or shared computers, that’s a meaningful protection. It also prevents your browser from autofilling forms with data from your session, adding visited URLs to the shared autocomplete history, or saving new passwords into the browser’s credential store.

Browser extensions generally don’t run in private mode unless you explicitly grant them permission. This matters for extensions that modify page behavior, but it doesn’t apply to tracking scripts served by the websites themselves — those are part of the page, not part of the browser.

When people expect private mode to protect them from websites and advertisers, it fails completely. That’s not what it was built for.

The Fingerprint That Persists Through Private Mode

Your browser exposes a consistent hardware profile to every website you visit. Private mode cannot change your hardware. It cannot change how your GPU renders a canvas element, which fonts are installed on your system, or how many CPU cores your machine has.

The EFF’s Panopticlick research tested millions of browsers and found that 83.6% have a fingerprint unique enough to identify them across sessions — without setting a single cookie. Add browser plugins and that rises to 94.2%. Open a private window, and you get the same fingerprint. Clear your cookies, and you get the same fingerprint. Use a different browser on the same machine, and your fingerprint changes — but only because the hardware properties are now being reported by a different software stack.

Here’s what makes up a fingerprint:

Canvas fingerprinting exploits the fact that different GPU hardware renders text and shapes with slightly different subpixel rounding. A site draws text and emoji to a hidden canvas element, then reads the pixel data as a number. Your GPU produces a consistent number that differs from someone else’s GPU, even with identical browser versions and operating systems.

WebGL fingerprinting reads your GPU’s renderer string directly — the exact make and model of your graphics hardware. The string ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11) is a different fingerprint than ANGLE (NVIDIA, NVIDIA GeForce RTX 4060).

Font enumeration uses JavaScript to test which fonts are installed by measuring rendered text widths across different font families. The specific set of fonts differs between operating systems, between users who’ve installed additional fonts, and between personal and corporate machines.

Hardware properties — CPU core count, device memory, audio processing fingerprint, screen resolution including taskbar gap, connection type — all contribute identifiers that don’t change between sessions.

Private mode does nothing about any of this. Neither does clearing cookies. Neither does a VPN, for that matter — fingerprinting reads your browser properties directly and doesn’t travel through a tunnel the VPN can intercept.

What You’re Actually Exposing: A Breakdown

Privacy Threat Private Mode Protects? Notes
Saved local browsing history ✓ Yes Main use case
Session cookies after closing ✓ Yes Cookies deleted on close
Autofill / saved passwords ✓ Yes Data not written
IP address visible to websites ✗ No Identical to normal browsing
Browser fingerprinting ✗ No Hardware profile unchanged
WebRTC IP leaks ✗ No Real IP exposed even through VPN
Third-party tracking scripts ✗ No Scripts load the same way
ISP traffic monitoring ✗ No Traffic is unencrypted either way
DNS query logs at ISP ✗ No Queries go to same resolver

WebRTC deserves a separate note. It’s the browser API that powers in-browser video calls. When WebRTC negotiates a peer-to-peer connection, it exchanges IP addresses at the operating system level — below VPN tunnels and well below anything private mode touches. A page that initiates a WebRTC exchange can see your real local network IP even if you’re behind a VPN and in private mode simultaneously. Most people discover this when they first test it.

Free Tests That Show What’s Actually Leaking — No Account Required

Running a few free no-login tests takes about ten minutes and gives you specific numbers instead of general descriptions. The results will probably be surprising.

Cover Your Tracks by the EFF tests your browser against a database of millions of real fingerprints and shows how unique yours is — with per-attribute entropy scores. Run it in a normal window, note the result. Then open a private window and run it again. The fingerprinting result will almost certainly be identical. “Your browser has a unique fingerprint” in both cases confirms that private mode changed nothing about how sites can identify you.

BrowserLeaks runs approximately 20 individual tests: IP address, WebRTC leak status, canvas fingerprint hash, WebGL renderer string, font enumeration count, TLS fingerprint, and more. The WebRTC test is the most revealing — it shows every IP address your browser is currently exposing. If you see your real local IP listed alongside a VPN IP, you have a confirmed WebRTC leak. Running this in private mode produces the same output as normal mode.

IPLeak.net combines IP detection and WebRTC leak checking on one page. Useful for a quick combined check: if your listed IP and your WebRTC IP are different, you have a WebRTC leak.

DNS Leak Test sends DNS queries and watches which resolvers respond. DNS queries resolve domain names to IP addresses — every URL you visit triggers one. If your ISP’s resolver appears in the results (rather than your VPN provider’s or a privacy-focused public resolver), your ISP sees every domain you visit regardless of private mode or VPN.

PrivacyTests.org, maintained by a former Firefox privacy engineer, benchmarks browsers against each other across 20+ privacy tests. It shows which protections each browser ships by default versus which require configuration. Worth checking before deciding whether to change browsers or settings.

Browser Settings That Actually Work Against Fingerprinting

The fixes that address fingerprinting require either different browsers or specific configuration — private mode alone can’t help here.

Firefox has a setting called privacy.resistFingerprinting. Type about:config in the address bar, accept the warning, search for this setting, and double-click to set it to true. When enabled, Firefox returns standardized values for canvas rendering, WebGL output, font metrics, screen dimensions, and timezone — the same values for every user with the setting enabled. Your fingerprint becomes common rather than unique. This is the same technology the Tor Browser uses; Mozilla documents it in their Firefox fingerprinting protection guide.

You can pair this with media.peerconnection.enabled set to false to block WebRTC entirely, eliminating the WebRTC leak vector. If you don’t use browser-based video calls, there’s no downside.

Brave randomizes canvas and WebGL output per session by default. Your fingerprint changes between browser sessions, preventing cross-session correlation. No configuration needed — it ships this way.

For DNS leaks: both Firefox and Chrome support DNS-over-HTTPS natively. In Firefox, go to Settings → Privacy & Security → DNS over HTTPS. In Chrome: Settings → Privacy and Security → Security → Use secure DNS. Point it at a privacy-focused resolver — Cloudflare’s 1.1.1.1, Mullvad DNS, or NextDNS all offer encrypted resolution without query logging.

privacy.sexy is an open-source, no-login browser tool that generates customizable privacy hardening scripts for Windows, macOS, and Linux. Select which settings to apply — telemetry blocking, WebRTC configuration, DNS hardening, and more — and it generates a script you run locally. No account, no upload. The script runs on your machine and the service never sees your data.

The Separate Problem: What You Actively Hand Over

Browser hardening addresses what your browser exposes passively. There’s a second category: what you actively provide to online services.

Every account you create ties your activity to a persistent identity. That identity can be linked across services through data brokers, correlated with your browser fingerprint, and connected to your purchase history. This is different from passive fingerprinting — it requires you to hand over identifying information (an email address, usually) and then continue using the service.

No-login tools that process data locally short-circuit this entire chain. When an image compression tool runs entirely in your browser without uploading anything to a server — the way Squoosh does — there’s no record of you processing that file anywhere outside your device. Compare that to a service that requires signup: that service now has your email, a record of the file you processed, a timestamp, and your IP address at minimum.

The same logic applies to file encryption (hat.sh encrypts entirely client-side), collaborative whiteboards (Excalidraw works without an account), and dozens of other tool categories. These tools can still fingerprint your browser on page load — that’s impossible to fully prevent without Firefox’s privacy.resistFingerprinting or Brave’s randomization. But they can’t build a persistent usage profile on you because there’s no identity to attach one to.

This isn’t a complete privacy solution. But it meaningfully reduces the amount of data flowing from your activity to third parties.

The Test You Should Run Before Changing Anything

Open Cover Your Tracks in your current browser on a normal window. Note whether it says “unique fingerprint” or “some protection.” Then open a private window and run the same test. Note whether the result changes.

If it doesn’t change — if you get the same “unique fingerprint” result in both — you now have specific evidence that private mode provides zero additional protection from fingerprint-based tracking. That’s not a general claim about browser privacy; it’s a test result about your actual browser on your actual machine.

From there, you have a baseline. Enable privacy.resistFingerprinting in Firefox, or switch to Brave, and run the test again. You’ll see whether the change worked. Enable DNS-over-HTTPS and run the DNS Leak Test to confirm your queries are no longer going to your ISP. Each change becomes verifiable.

Private mode was built for a specific, legitimate use case. For the people sharing your device, it works. For tracking by websites and advertisers, it was never designed to help. The tools above test what’s actually happening and the settings above actually address it.

Localization pricing in 2026: why per-word is dying and what’s replacing it

If you’ve priced out a localization vendor recently, you’ve probably noticed the math doesn’t make sense. Your team ships 40 times a month, the string file changes daily, and somewhere on a sales call someone is asking you to forecast your “per-word volume for FY2026.”

Per-word billing was built for an era when companies translated a marketing brochure once a quarter and called it done. That era ended a long time ago. Most software teams are stuck retrofitting modern release cycles onto a pricing model from the late 90s.

In 2026 that’s finally breaking. Here’s what’s replacing it.

What “usage-based” actually means for localization

Usage-based pricing charges you for what you actually move through the system: API calls, characters processed, string updates, or live requests served. The closest analogy is how you already pay for Stripe or a CDN. You pay when something happens, not when a quarterly contract says you should.

A few things this gets you that flat per-seat or per-word doesn’t:

  • Costs track real release velocity instead of forecasted volume
  • Translation memory hits cost less than fresh translations, the way they should
  • You don’t lose money on the months where your product was quiet
  • Finance can actually trace spend back to specific projects or locales

The trade-off is real. You give up the false comfort of a predictable annual line item. In exchange you get something that scales with the business instead of against it.

Why this is happening now

Per-word stuck around so long because translation used to be the bottleneck. You paid a human translator $0.15 per word because a human had to read every word.

Modern pipelines inverted that. The raw translation is close to free. What costs you is the surrounding work: deciding when to use machine translation versus an LLM versus a human reviewer, feeding the model enough context that the output isn’t generic, catching the strings that need cultural adaptation rather than a literal translation.

Charging per word here is like charging per character for a database query. The unit you’re billing is no longer the unit that drives cost.

Two practical things follow. The platforms still selling per-word are usually doing it because their cost structure is human-translation-heavy. That’s not bad per se, but you should know which kind of vendor you’re buying. The platforms that re-architected around AI tend to surface usage in different units — translation memory hits, AI runs, runtime requests served — and they bill on those.

What engineering leaders are actually looking at

A few things come up consistently when engineering leaders evaluate localization platforms in 2026.

Integration depth comes up first. Does the platform hook into your existing CI/CD pipeline, or does it expect you to use its own dashboard as the source of truth? Anything that lives outside the engineering workflow tends to get abandoned within a year.

Runtime delivery is the next question. Can translations update without redeploying the app? Teams that ship multiple times a day find bundle-based localization stops working pretty quickly.

Cost transparency closes the loop. If the platform can’t tell you which feature launch cost you the most in translation, the billing model isn’t really usage-based. It’s just a different invoice.

This is roughly the lens General Translation was built around: AI translation tightly coupled to engineering workflows, runtime delivery, and usage-based billing with visibility down to the request. But the broader point is that the whole category is moving this direction regardless of which platform a team picks.

Where this is going

The honest read is that localization is starting to look less like a vendor relationship and more like an infrastructure dependency. You don’t sign a per-word contract with your CDN. You don’t pay per-seat for error monitoring. In a few years it’s going to feel just as strange that you ever did it for translation.

If you’re picking a platform right now, the most useful thing you can do is ignore the pricing page for a minute and look at how the vendor talks about their own product. Are they describing a translation service, or a piece of infrastructure? The answer usually tells you what you need to know.

Selectolax: a faster BeautifulSoup alternative for Python scraping at scale

Our API was choking at 2 requests per second. Not because of network. Not because of proxies. The CPU was at 100% just parsing HTML.

We run FlyByAPIs, a set of web scraping APIs. One of our heaviest endpoints parses full e-commerce product pages: dozens of fields, nested sections, fallback selectors for when the site A/B tests its layout. We were using Parsel (which wraps lxml under the hood), and it worked fine. Until we needed concurrency.

At 2-5 parallel requests, CPU was saturated. Parsing alone was eating 150-200ms per page. Multiply that by concurrent workers sharing the same CPU and you’ve got a bottleneck that no proxy optimization will fix.

We switched to Selectolax. Parsing dropped to 30-40ms. Went from 2-5 req/s to 15-20 req/s on the same hardware.

What is Selectolax?

Selectolax is a Python binding for the Lexbor HTML parser. Lexbor is a C library built from scratch for speed. It implements the HTML5 spec and supports CSS selectors.

Why is it faster? BeautifulSoup and Parsel build a full Python object tree. Every node becomes a Python object with attributes, methods, navigation helpers. That’s convenient if you need to manipulate the DOM, but it’s expensive when all you want is to read values out. Selectolax keeps the parsed tree in C memory and only creates Python objects when you actually access a node. Way less memory allocation and garbage collection pressure.

pip install selectolax

No extra dependencies. Works on Linux, macOS, Windows.

The basics: BeautifulSoup vs Selectolax

If you’ve used BeautifulSoup, Selectolax will feel familiar. The API is smaller, but it covers what you actually need for scraping.

BeautifulSoup:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "lxml")
title = soup.select_one("h1.product-title")
print(title.get_text(strip=True))

prices = soup.select("span.price")
for p in prices:
    print(p.get_text(strip=True))

Selectolax:

from selectolax.lexbor import LexborHTMLParser

tree = LexborHTMLParser(html)
title = tree.css_first("h1.product-title")
print(title.text(deep=False).strip())

prices = tree.css("span.price")
for p in prices:
    print(p.text(deep=False).strip())

select_one becomes css_first. select becomes css. .get_text() becomes .text(). For attributes: node.attributes.get("href") instead of node["href"].

One gotcha: text(deep=False) returns only the direct text of that node, not its children. This is actually useful when you’re parsing structured pages where a parent element contains multiple text nodes you want to extract separately. Use text(deep=True) (the default) when you want all nested text.

Where it actually matters: CPU under concurrency

Most benchmark numbers you see online (“selectolax is 5x faster than BS4!”) are measured on a single thread, single document. Cool. But that misses the real problem.

In production, if you’re running an async framework like FastAPI/uvicorn, your request handlers share a single event loop. HTML parsing is synchronous, CPU-bound work. While Parsel spends 150ms parsing one page, the event loop is blocked. Every other request just sits there waiting. It doesn’t matter that your code is async, the parsing step isn’t.

The faster the parse, the less time you block the loop, the more requests you can actually handle concurrently. That’s why the throughput gain is bigger than the raw per-document speed improvement suggests.

Before the switch, our monitoring showed CPU at 95-100% during peak traffic. Request latency was spiking because the event loop was constantly blocked by parsing. After the switch, CPU sits around 40-50% under the same load. Same hardware. Same traffic.

Scoped selectors (nested parents)

This is the optimization that made the biggest difference after the parser swap itself.

A typical product page has 40-60 fields to extract. If you run every CSS selector from the document root, the parser traverses the entire DOM tree for each field. On a 500KB HTML page with thousands of nodes, that adds up fast.

The idea is simple: scope your selectors. Grab the container element first, then query fields within that subtree.

Naive (slow):

tree = LexborHTMLParser(html)

# Every selector searches the entire document
title = tree.css_first("h1.product-title")
price = tree.css_first("#price-section .current-price")
seller = tree.css_first("#seller-info .seller-name")
stock = tree.css_first("#seller-info .stock-status")
rating = tree.css_first("#reviews .star-rating")
review_count = tree.css_first("#reviews .total-reviews")

Scoped (fast):

tree = LexborHTMLParser(html)

# Level 1: grab major page sections
seller_section = tree.css_first("#seller-info")
review_section = tree.css_first("#reviews")

# Level 2: query within the scoped subtree
title = tree.css_first("h1.product-title")  # still from root (only one on page)
price = tree.css_first("#price-section .current-price")

if seller_section:
    seller = seller_section.css_first(".seller-name")
    stock = seller_section.css_first(".stock-status")
    delivery = seller_section.css_first(".delivery-estimate")

if review_section:
    rating = review_section.css_first(".star-rating")
    review_count = review_section.css_first(".total-reviews")
    # Level 3: individual review cards within the review section
    for card in review_section.css(".review-card"):
        author = card.css_first(".author-name")
        text = card.css_first(".review-text")
        stars = card.css_first(".star-count")

You go two or three levels deep. Document to section to element to sub-element. Each level narrows the DOM subtree the parser needs to traverse. With 50+ selectors per page, this compounds quickly.

In our case, this shaved another 20-30% off parsing time on top of the Selectolax speed gain.

Fallback selectors

Websites change their HTML. Constantly. The field that lived in span.a-price .a-offscreen last week might be in .price-block .current this week. If you hardcode a single selector, your scraper breaks silently, returns None instead of a price, and nobody notices until a customer complains.

We use fallback selectors:

def select_one_with_fallbacks(node, primary_selector, fallbacks=None):
    result = node.css_first(primary_selector)
    if result is not None:
        return result
    for fallback in (fallbacks or []):
        result = node.css_first(fallback)
        if result is not None:
            return result
    return None

# Usage
price_node = select_one_with_fallbacks(
    tree,
    "span.current-price .amount",
    fallbacks=[
        ".price-block .a-offscreen",
        "#price .display-price",
    ]
)

The primary selector is the one that works today. The fallbacks are selectors that worked in previous versions of the page, or alternative ones we’ve seen on different page variants (A/B tests, regional differences).

This works well with Selectolax because each css_first call is cheap. You’re trying 2-3 selectors instead of 1, but at microsecond cost per attempt, it’s negligible. With BeautifulSoup, the overhead per selector is higher and you’d feel it more.

We actually define all our selectors in YAML config files, not in code. When a site changes its layout, we update a config file and deploy. No code changes, no test rewrites. When you run scraping at scale, data drift is your biggest enemy, and having selectors in config instead of code means you can react in minutes instead of hours. But that deserves its own post.

When Selectolax is NOT the right choice

Selectolax isn’t always the answer.

If you need to modify the HTML tree (insert nodes, delete nodes, change attributes), stick with BeautifulSoup. Selectolax is read-only.

For one-off scripts where speed doesn’t matter, BeautifulSoup is more forgiving and better documented. More Stack Overflow answers too.

If you need XPath, use Parsel or lxml. Selectolax only does CSS selectors. Same if you’re deep in a Scrapy pipeline, Parsel is native there and the integration is tight.

Where Selectolax wins: when you’re parsing HTML inside a web server or API that handles concurrent requests. When CPU is your actual bottleneck. When CSS selectors are enough (which, honestly, covers 95% of scraping use cases).

Migration cheat sheet

If you’re moving from BS4 or Parsel:

BeautifulSoup / Parsel Selectolax
soup.select("div.item") tree.css("div.item")
soup.select_one("h1") tree.css_first("h1")
tag.get_text(strip=True) node.text(deep=False).strip()
tag["href"] node.attributes.get("href")
tag.get("href", "") node.attributes.get("href") or ""
tag.find_parent("div") node.parent (then check tag)
len(soup.select("li")) len(tree.css("li"))

Watch out for None. If css_first doesn’t find anything, calling .text() on it will crash. Always guard:

node = tree.css_first("h1.title")
title = node.text(deep=False).strip() if node else None

Our numbers

Before/after on the same hardware, parsing full e-commerce product detail pages (50+ fields each):

Metric Parsel (lxml) Selectolax (Lexbor)
Parse time per page ~150-200ms ~30-40ms
Max concurrent req/s before CPU saturation 2-5 15-20
CPU usage at peak load 95-100% 40-50%
Memory per parsed page Higher (Python objects) Lower (C-level tree)

The throughput gain is larger than the raw parse-time improvement because of the event loop blocking I mentioned. Less time parsing means less time blocking the loop, which means more time for network I/O, JSON serialization, everything else.

So, what did we learn

We spent weeks optimizing proxy rotation and connection pooling before we realized the bottleneck was HTML parsing. Weeks. If you’re building scrapers or data extraction services and you’re hitting CPU limits, check your parser before throwing more infrastructure at it.

The migration took us about a day for the core parser, plus another day fixing edge cases across all our endpoints. Not bad for a 5x improvement.

pip install selectolax, swap one parser, benchmark it. You’ll see it.

We use Selectolax across all of our Amazon API endpoints. 50+ fields per page, 22 marketplaces, sub-second response times. The entire parsing layer runs on it.

If you’ve dealt with similar parsing bottlenecks, or you’re in the middle of a migration, I’d love to hear how it went.

I Spent 30 Days Switching From Cursor to Windsurf. Here’s What Actually Changed.

Eight months on Cursor. Then a coworker kept sending me Windsurf screenshots with the Cascade panel doing multi-file rewrites I thought only I could orchestrate manually. I gave myself 30 days — April 14 to May 14, 2026 — to run Windsurf as my primary coding environment on real client work. Not toy projects. Real work.This is what happened.## Why I left Cursor (temporarily)My main Cursor frustration wasn’t the model quality. Claude 3.7 Sonnet inside Cursor’s Composer is excellent. The frustration was context drift in long sessions.After about 40-50 tool calls in a single Composer session, responses started degrading. The model would “forget” constraints I’d set at session start — things like “don’t modify the test files” or “use the existing auth pattern.” I’d have to re-anchor with fresh context every 35-40 messages, which interrupted flow.I was also burning about $22-28/month on top of the Pro subscription because I kept hitting the fast-model cap and switching to API billing. Not a dealbreaker, but it added up.Windsurf’s pitch — that Cascade maintains coherent context across an entire multi-file workflow — seemed worth testing.## Setup and the first 48 hoursInstallation was straightforward. Windsurf is a VS Code fork, so my existing settings.json, keybindings, and most extensions transferred. I had my environment working in about 25 minutes.The first surprise was the Cascade panel. It’s not a chat window the way Composer is. It operates more like a task queue — you give it a goal, it proposes a plan with explicit steps, you approve or edit the plan, then it executes. The distinction matters.In Composer, you’re guiding an ongoing conversation. In Cascade, you’re approving a structured workflow. Neither is strictly better; they suit different cognitive styles.I hit two rough patches in the first 48 hours:First: Windsurf’s extension compatibility. A couple of my niche extensions (a custom linter and a schema validation tool) didn’t load cleanly. I spent about 90 minutes diagnosing this before realizing they just needed to be reinstalled fresh rather than migrated from my VS Code profile. Not a Windsurf bug, but migration friction.Second: model selection. Windsurf gives you a dropdown to choose between Claude 3.5 Sonnet, GPT-4o, and a few others. I defaulted to Claude 3.5 Sonnet for continuity. The results felt slightly different from Cursor’s implementation of the same model — my guess is system prompt differences, but I couldn’t verify.## Where Windsurf actually won*Multi-file coherence on larger refactors.* In week two, I refactored an API integration layer that touched 11 files — route handlers, middleware, type definitions, tests, and a client wrapper. I gave Cascade the goal, it generated a 7-step plan, I trimmed it to 5 steps, and it executed without losing the thread.The same task in Cursor had taken me three Composer sessions the previous month (I’d attempted it before). Each session required re-establishing context. Windsurf did it in one Cascade run, roughly 2.5 hours, with two manual corrections mid-way.I can’t say Windsurf is smarter — it’s running similar models. What’s different is that Cascade’s plan-then-execute structure means the model doesn’t have to reconstruct intent from conversation history. The intent is explicit in the plan.Reduced “where was I” overhead. After breaks, re-reading a Cascade plan to understand current state took about 10 seconds. Re-reading a Composer thread to find where I’d left off took closer to 3-5 minutes. Over 30 days this added up to maybe 3-4 hours of recovered time.The “memory” panel. Windsurf has a project-level memory feature that persists context between sessions. I used it to store things like: which database connection pattern we were using, why a particular abstraction existed, constraints on a third-party API we were wrapping. When starting a new Cascade session, I’d reference the memory note and the model stayed grounded.Cursor doesn’t have a native equivalent. I’d been maintaining a separate context file maEight months on Cursor. Then a coworker kept sending me Windsurf screenshots with the Cascade panel doing multi-file rewrites I thought only I could orchestrate manually. I gave myself 30 days — April 14 to May 14, 2026 — to run Windsurf as my primary coding environment on real client work. Not toy projects. Real work.This is what happened.## Why I left Cursor (temporarily)My main Cursor frustration wasn’t the model quality. Claude 3.7 Sonnet inside Cursor’s Composer is excellent. The frustration was context drift in long sessions.After about 40-50 tool calls in a single Composer session, responses started degrading. The model would “forget” constraints I’d set at session start — things like “don’t modify the test files” or “use the existing auth pattern.” I’d have to re-anchor with fresh context every 35-40 messages, which interrupted flow.I was also burning about $22-28/month on top of the Pro subscription because I kept hitting the fast-model cap and switching to API billing. Not a dealbreaker, but it added up.Windsurf’s pitch — that Cascade maintains coherent context across an entire multi-file workflow — seemed worth testing.## Setup and the first 48 hoursInstallation was straightforward. Windsurf is a VS Code fork, so my existing settings.json, keybindings, and most extensions transferred. I had my environment working in about 25 minutes.The first surprise was the Cascade panel. It’s not a chat window the way Composer is. It operates more like a task queue — you give it a goal, it proposes a plan with explicit steps, you approve or edit the plan, then it executes. The distinction matters.In Composer, you’re guiding an ongoing conversation. In Cascade, you’re approving a structured workflow. Neither is strictly better; they suit different cognitive styles.I hit two rough patches in the first 48 hours:First: Windsurf’s extension compatibility. A couple of my niche extensions (a custom linter and a schema validation tool) didn’t load cleanly. I spent about 90 minutes diagnosing this before realizing they just needed to be reinstalled fresh rather than migrated from my VS Code profile. Not a Windsurf bug, but migration friction.Second: model selection. Windsurf gives you a dropdown to choose between Claude 3.5 Sonnet, GPT-4o, and a few others. I defaulted to Claude 3.5 Sonnet for continuity. The results felt slightly different from Cursor’s implementation of the same model — my guess is system prompt differences, but I couldn’t verify.## Where Windsurf actually won*Multi-file coherence on larger refactors.* In week two, I refactored an API integration layer that touched 11 files — route handlers, middleware, type definitions, tests, and a client wrapper. I gave Cascade the goal, it generated a 7-step plan, I trimmed it to 5 steps, and it executed without losing the thread.The same task in Cursor had taken me three Composer sessions the previous month (I’d attempted it before). Each session required re-establishing context. Windsurf did it in one Cascade run, roughly 2.5 hours, with two manual corrections mid-way.I can’t say Windsurf is smarter — it’s running similar models. What’s different is that Cascade’s plan-then-execute structure means the model doesn’t have to reconstruct intent from conversation history. The intent is explicit in the plan.Reduced “where was I” overhead. After breaks, re-reading a Cascade plan to understand current state took about 10 seconds. Re-reading a Composer thread to find where I’d left off took closer to 3-5 minutes. Over 30 days this added up to maybe 3-4 hours of recovered time.The “memory” panel. Windsurf has a project-level memory feature that persists context between sessions. I used it to store things like: which database connection pattern we were using, why a particular abstraction existed, constraints on a third-party API we were wrapping. When starting a new Cascade session, I’d reference the memory note and the model stayed grounded.Cursor doesn’t have a native equivalent. I’d been maintaining a separate context file manually — Windsurf’s built-in version is more integrated.## Where Cursor still wins*Speed on smaller tasks.* For one-shot questions, quick edits, or single-file tasks, Cursor’s Composer feels faster to reach. Cascade’s planning step is valuable for complex work but adds maybe 15-30 seconds of overhead for simple things. I found myself opening Cursor for “change this function signature” tasks while using Windsurf for anything touching more than 3 files.The chat sidebar for reference questions. Cursor’s CMD+L sidebar for asking questions about the codebase without triggering an edit session is cleaner than what Windsurf offers. I use this constantly to ask “explain what this middleware does” or “where does this type come from.” Windsurf’s equivalent is workable but the UI is busier.Bug fix workflow. When debugging, I want to paste an error, get an analysis, paste a follow-up, get a fix suggestion, and iterate quickly. This conversational loop feels more natural in Composer than in Cascade. Cascade’s structured plan doesn’t map well onto the inherently exploratory nature of debugging.I ended up keeping Cursor installed and using it specifically for debugging sessions throughout the 30 days.Plugin ecosystem confidence. This might normalize over time, but the mild anxiety about extension compatibility didn’t fully go away. I knew Cursor’s extension behavior. I was still learning Windsurf’s edge cases at day 30.## The Cascade workflow that actually changed how I refactorAround day 12, I stopped thinking of Cascade as an “AI assistant” and started treating it more like a junior developer I could hand a well-scoped task to.The mental model shift: I’d spend 5-10 minutes writing a proper task description with context, constraints, and acceptance criteria — essentially a small internal ticket. Then I’d paste that into Cascade and let it propose a plan. I’d review the plan with the same attention I’d give a PR description, edit it, then approve.The result was better than prompting conversationally. Because I had to write the task as a proper spec, I caught ambiguities before Cascade touched the code. The model’s plan was also more useful as a review artifact — I could share it in Slack to explain what had changed before the PR was ready.One specific example: on day 19, I used this workflow to refactor our caching layer. The spec I wrote was 6 bullet points. Cascade’s plan was 8 steps. I merged two steps, added a constraint about maintaining the existing interface for legacy callers, and approved. The execution took 38 minutes. The PR review comment from my team lead was “unusually clean diff.” I’m fairly sure the structured plan is why — it forced me to think before generating.## After 30 daysI’m running both tools. Windsurf handles the architectural work — the multi-file refactors, the feature builds that touch 6+ locations, anything where I need coherent state across a long session. Cursor handles the tactical stuff — debugging, quick edits, reference questions.The framing I’ve settled on: Windsurf is a project manager that can also code. Cursor is a senior developer you’re pair programming with. The project manager framing is actually useful. You give it goals and plans, not just questions.Would I fully switch? Not yet. The extension uncertainty, the debugging workflow preference, and the muscle memory I’ve built in Cursor make a clean switch impractical. But I use Windsurf more hours per week now than I did at day one.The thing that surprised me most: the biggest productivity gain wasn’t from any AI feature. It was from being forced to write better task specs because Cascade requires them. The tool made me a more deliberate engineer, even in the sessions where I used Cursor afterward.—Running a similar comparison? Curious what setup you’re using — particularly whether people with larger monorepos find Cascade’s coherence advantage more or less pronounced at scale.*nually — Windsurf’s built-in version is more integrated.## Where Cursor still winsSpeed on smaller tasks.* For one-shot questions, quick edits, or single-file tasks, Cursor’s Composer feels faster to reach. Cascade’s planning step is valuable for complex work but adds maybe 15-30 seconds of overhead for simple things. I found myself opening Cursor for “change this function signature” tasks while using Windsurf for anything touching more than 3 files.The chat sidebar for reference questions. Cursor’s CMD+L sidebar for asking questions about the codebase without triggering an edit session is cleaner than what Windsurf offers. I use this constantly to ask “explain what this middleware does” or “where does this type come from.” Windsurf’s equivalent is workable but the UI is busier.Bug fix workflow. When debugging, I want to paste an error, get an analysis, paste a follow-up, get a fix suggestion, and iterate quickly. This conversational loop feels more natural in Composer than in Cascade. Cascade’s structured plan doesn’t map well onto the inherently exploratory nature of debugging.I ended up keeping Cursor installed and using it specifically for debugging sessions throughout the 30 days.Plugin ecosystem confidence. This might normalize over time, but the mild anxiety about extension compatibility didn’t fully go away. I knew Cursor’s extension behavior. I was still learning Windsurf’s edge cases at day 30.## The Cascade workflow that actually changed how I refactorAround day 12, I stopped thinking of Cascade as an “AI assistant” and started treating it more like a junior developer I could hand a well-scoped task to.The mental model shift: I’d spend 5-10 minutes writing a proper task description with context, constraints, and acceptance criteria — essentially a small internal ticket. Then I’d paste that into Cascade and let it propose a plan. I’d review the plan with the same attention I’d give a PR description, edit it, then approve.The result was better than prompting conversationally. Because I had to write the task as a proper spec, I caught ambiguities before Cascade touched the code. The model’s plan was also more useful as a review artifact — I could share it in Slack to explain what had changed before the PR was ready.One specific example: on day 19, I used this workflow to refactor our caching layer. The spec I wrote was 6 bullet points. Cascade’s plan was 8 steps. I merged two steps, added a constraint about maintaining the existing interface for legacy callers, and approved. The execution took 38 minutes. The PR review comment from my team lead was “unusually clean diff.” I’m fairly sure the structured plan is why — it forced me to think before generating.## After 30 daysI’m running both tools. Windsurf handles the architectural work — the multi-file refactors, the feature builds that touch 6+ locations, anything where I need coherent state across a long session. Cursor handles the tactical stuff — debugging, quick edits, reference questions.The framing I’ve settled on: Windsurf is a project manager that can also code. Cursor is a senior developer you’re pair programming with. The project manager framing is actually useful. You give it goals and plans, not just questions.Would I fully switch? Not yet. The extension uncertainty, the debugging workflow preference, and the muscle memory I’ve built in Cursor make a clean switch impractical. But I use Windsurf more hours per week now than I did at day one.The thing that surprised me most: the biggest productivity gain wasn’t from any AI feature. It was from being forced to write better task specs because Cascade requires them. The tool made me a more deliberate engineer, even in the sessions where I used Cursor afterward.—Running a similar comparison? Curious what setup you’re using — particularly whether people with larger monorepos find Cascade’s coherence advantage more or less pronounced at scale.## One thing I would do differentlyIf I started over, I would front-load the Cascade mental model adoption. The first week I kept treating Cascade like a slightly different Composer, which meant I was prompting conversationally when I should have been writing specs.The practical difference: in Composer, I write something like “add rate limiting to this endpoint” and iterate based on what I get back. In Cascade, I write a proper spec before touching the interface — endpoint path, what counts as a limit exceeded, where the counter state lives, what the error response shape looks like, whether existing tests cover the limit behavior. That 5-minute spec saves 20-30 minutes of mid-task correction.Once I internalized that distinction — Cascade is for planned execution, Composer is for conversation — the tool clicked. It might take less than 12 days if you approach it deliberately from day one rather than gradually.I would also spend an hour at the start setting up project memory properly. I added it retroactively around day 8, and I could see the difference immediately in how sessions started. Memory notes take 10-15 minutes to write well. The return: I stopped re-explaining the same architectural decisions at the start of every session. Over 30 days, that recovered roughly 40-60 minutes of setup time across sessions.## One thing I would do differentlyIf I started over, I would front-load the Cascade mental model adoption. The first week I kept treating Cascade like a slightly different Composer, which meant I was prompting conversationally when I should have been writing specs.The practical difference: in Composer, I write something like add rate limiting to this endpoint and iterate based on what I get back. In Cascade, I write a proper spec before touching the interface — endpoint path, what counts as a limit exceeded, where the counter state lives, what the error response shape looks like, whether existing tests cover the limit behavior. That 5-minute spec saves 20-30 minutes of mid-task correction.Once I internalized that distinction — Cascade is for planned execution, Composer is for conversation — the tool clicked. It might take less than 12 days if you approach it deliberately from day one rather than gradually.I would also spend an hour at the start setting up project memory properly. I added it retroactively around day 8, and I could see the difference immediately in how sessions started. Memory notes take 10-15 minutes to write well. Over 30 days, that recovered roughly 40-60 minutes of setup time across sessions.One last observation: the two tools are cheaper to run in combination than either alone at full usage. My combined billing dropped from the 22-28 dollar range to about 14-16 dollars over the trial month. Unexpected benefit.