# ElixirConf EU 2026: Three Technical Shifts That Matter

Estimated read time: 3 minutes

Málaga hosted the ElixirConf EU 2026. Here’s what changed.

1. Type Inference Arrives (No Annotations Required)

Elixir 1.20 introduces whole-program type inference. The compiler now understands function signatures, guard conditions, and map key domains without explicit type annotations.

What this means: A function performing arithmetic on map values now infers that the input must be a map containing numeric keys. Pattern matching across clauses carries type knowledge forward. Handle nil in one branch? Subsequent branches know the value cannot be nil.

The system also tracks map mutations through the Map module, emitting compile-time warnings when operations reference keys proven to be absent.

For teams: Fewer runtime errors. Earlier logic flaw detection. Testing effort shifts from defensive type checking to business logic validation.

Requires Erlang/OTP 27 or later.

2. DurableServer: State That Survives Node Failures

Chris McCord’s keynote introduced DurableServer—a new abstraction for stateful processes that persist across node failures and support seamless state migration in distributed clusters.

The problem it solves: Traditional GenServer implementations tie process state to node lifecycle. Distributed applications struggle with process locality guarantees.

The solution: DurableServer integrates with pluggable storage backends. Process state lives independently of the node.

Complementary sessions demonstrated Kafka-backed architectures and Oban processing at scale. The combination enables architectures that maintain consistency without external coordination layers.

For teams: Redesign stateful components for resilience without rewriting application architecture.

3. Compilation Gets Faster (Up to 4x)

Elixir 1.20 delivers measurable compilation improvements through:

  • Lazy module loading
  • Parallel dependency resolution

Large codebases see build time reductions of up to four times in benchmark scenarios.

Tooling additions:

  • mix source for better visibility
  • Enhanced IEx debugging output
  • Type-aware autocomplete in IEx
  • Improved error messaging for undefined functions

For teams: Lower cognitive overhead when working with large, modular applications.

What This Means for You

Role Implication
Teams evaluating Elixir Enhanced type system reduces test coverage needs for edge cases
Existing Elixir projects Upgrade to 1.20 for incremental type-aware development. Compiler detects redundant clauses and dead code, supporting refactoring with higher confidence
Engineering leadership The “one monolith, many teams” pattern presented at Málaga shows Elixir’s module isolation and supervision trees provide natural boundaries for team autonomy

One More Thing: AI Patterns

Multiple talks addressed AI integration. The BEAM’s lightweight process model aligns naturally with asynchronous, stateful AI orchestration.

Two libraries worth noting:

  • Legion – Code execution by AI agents using pure Elixir
  • Arcana – Retrieval-augmented generation pipelines without external runtimes

Concurrent inference requests. Familiar supervision patterns. No additional orchestration layers.

Sources: ElixirConf EU 2026 announcements, Elixir 1.20 release candidate documentation, and community resources compiled on the Elixir Forum.

Week 1: 100 Days of Solana

Identity on Solana Finally Clicked for Me When I Compared It to SSH Keys

Coming from a Web2 background, I always thought “blockchain identity” sounded way more complicated than it actually is.

People throw around terms like wallets, addresses, private keys, and self-custody, and at first it honestly feels like learning an entirely new internet.

But after spending the past few days working with Solana wallets and keypairs, something finally clicked for me:

A Solana wallet is basically your internet identity backed by cryptography instead of a company database.

And surprisingly, the best analogy I found was SSH keys.

We Already Use This Idea in Web2

If you have ever connected to a remote server using SSH, you already understand the core concept behind Solana identity.

You generate:

  • a public key
  • a private key

You place the public key on the server, and the server trusts you because only you have the matching private key.

No password needs to be repeatedly stored or sent over the network.

That is actually very close to how identity works on Solana.

When you create a Solana wallet, you generate a cryptographic keypair:

  • the public key becomes your wallet address
  • the private key proves ownership

Your wallet address is safe to share publicly. It is basically your on-chain identity.

Something like this:

14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5

At first glance, it looks random and unreadable, but there is actually a reason for the format.

Solana uses Base58 encoding, which intentionally removes visually confusing characters like:

  • 0 and O
  • I and l

Small detail, but honestly pretty smart.

The Weirdest Part: Nobody Creates Your Account

This was probably the biggest mindset shift for me.

In Web2, every account exists because some company stores it in a database.

Your Gmail account exists because Google says it exists.

Your GitHub username exists because GitHub stores it.

Your bank account exists because a bank manages it.

But on Solana, there is no company creating your identity.

No signup form.
No email verification.
No “forgot password.”

Your identity exists because you generated a keypair.

That is it.

The network simply recognizes cryptographic ownership.

And only whoever controls the private key can approve actions for that wallet.

Ownership Feels Very Different

In Web2, we say we “own” our accounts, but realistically platforms still control access.

Accounts can be:

  • suspended
  • locked
  • recovered
  • deleted

On Solana, ownership works differently.

If you hold the private key, you control the wallet.

Nobody can reset it for you.
Nobody can recover it for you.
Nobody can override your signature.

That freedom is powerful, but it also comes with responsibility.

Losing a password in Web2 is annoying.

Losing a private key in Web3 can mean losing access permanently.

I think that is why people emphasize wallet security so much in crypto. Your wallet is not just an app — it is literally your identity and ownership layer.

One Identity Across Everything

Another thing I found really interesting is how portable blockchain identity becomes.

In Web2, every app wants its own login:

  • username
  • password
  • Google OAuth
  • GitHub OAuth
  • email verification

On Solana, the same wallet can interact across completely different applications.

Once you connect your wallet, apps can recognize:

  • what tokens you own
  • NFTs you hold
  • governance votes
  • transaction history
  • on-chain activity

Without creating another account.

That part honestly feels futuristic compared to the fragmented identity systems we use today.

Identity Becomes Infrastructure

The more I learn about Solana, the more I realize wallets are not just for storing crypto.

They are the foundation for interacting with the network itself.

Your wallet becomes:

  • your login
  • your signature
  • your ownership proof
  • your reputation
  • your access layer

And unlike traditional platforms, it works across the entire ecosystem instead of being trapped inside one company’s database.

That was the moment blockchain identity finally started making sense to me.

Not as “magic internet crypto stuff,” but as a completely different model for proving ownership and trust online.

MLH Challenge: [# Identity on Solana Finally Clicked for Me When I Compared It to SSH Keys

Coming from a Web2 background, I always thought “blockchain identity” sounded way more complicated than it actually is.

People throw around terms like wallets, addresses, private keys, and self-custody, and at first it honestly feels like learning an entirely new internet.

But after spending the past few days working with Solana wallets and keypairs, something finally clicked for me:

A Solana wallet is basically your internet identity backed by cryptography instead of a company database.

And surprisingly, the best analogy I found was SSH keys.

We Already Use This Idea in Web2

If you have ever connected to a remote server using SSH, you already understand the core concept behind Solana identity.

You generate:

  • a public key
  • a private key

You place the public key on the server, and the server trusts you because only you have the matching private key.

No password needs to be repeatedly stored or sent over the network.

That is actually very close to how identity works on Solana.

When you create a Solana wallet, you generate a cryptographic keypair:

  • the public key becomes your wallet address
  • the private key proves ownership

Your wallet address is safe to share publicly. It is basically your on-chain identity.

Something like this:

14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5

At first glance, it looks random and unreadable, but there is actually a reason for the format.

Solana uses Base58 encoding, which intentionally removes visually confusing characters like:

  • 0 and O
  • I and l

Small detail, but honestly pretty smart.

The Weirdest Part: Nobody Creates Your Account

This was probably the biggest mindset shift for me.

In Web2, every account exists because some company stores it in a database.

Your Gmail account exists because Google says it exists.

Your GitHub username exists because GitHub stores it.

Your bank account exists because a bank manages it.

But on Solana, there is no company creating your identity.

No signup form.
No email verification.
No “forgot password.”

Your identity exists because you generated a keypair.

That is it.

The network simply recognizes cryptographic ownership.

And only whoever controls the private key can approve actions for that wallet.

Ownership Feels Very Different

In Web2, we say we “own” our accounts, but realistically platforms still control access.

Accounts can be:

  • suspended
  • locked
  • recovered
  • deleted

On Solana, ownership works differently.

If you hold the private key, you control the wallet.

Nobody can reset it for you.
Nobody can recover it for you.
Nobody can override your signature.

That freedom is powerful, but it also comes with responsibility.

Losing a password in Web2 is annoying.

Losing a private key in Web3 can mean losing access permanently.

I think that is why people emphasize wallet security so much in crypto. Your wallet is not just an app — it is literally your identity and ownership layer.

One Identity Across Everything

Another thing I found really interesting is how portable blockchain identity becomes.

In Web2, every app wants its own login:

  • username
  • password
  • Google OAuth
  • GitHub OAuth
  • email verification

On Solana, the same wallet can interact across completely different applications.

Once you connect your wallet, apps can recognize:

  • what tokens you own
  • NFTs you hold
  • governance votes
  • transaction history
  • on-chain activity

Without creating another account.

That part honestly feels futuristic compared to the fragmented identity systems we use today.

Identity Becomes Infrastructure

The more I learn about Solana, the more I realize wallets are not just for storing crypto.

They are the foundation for interacting with the network itself.

Your wallet becomes:

  • your login
  • your signature
  • your ownership proof
  • your reputation
  • your access layer

And unlike traditional platforms, it works across the entire ecosystem instead of being trapped inside one company’s database.

That was the moment blockchain identity finally started making sense to me.

Not as “magic internet crypto stuff,” but as a completely different model for proving ownership and trust online.

MLH Challenge: [https://www.mlh.com/events/100-days-of-solana/challenges]

How I built an anonymous daily leaderboard — and why it resets every night

Every competitive typing site has a leaderboard. Most of them are terrible.

TypeRacer shows you an all-time WPM rank that you’ll never crack. MonkeyType has a global top-1000 that’s been locked in by dedicated grinders for years. Both create the same problem: if you’re not already in the top tier, the leaderboard is demotivating noise.

When I built Clackpit, I wanted a leaderboard that actually worked for a normal person sitting down to practice. That meant rethinking two things: identity and time horizon.

The problem with persistent identity

Most leaderboards require an account. Name, email, password — the whole thing. That’s fine for a product where you’ve already committed to the user relationship. But for a typing site, where someone might show up for a 3-minute race and never return, it’s a massive conversion blocker.

I wanted anyone to be competitive immediately. No friction.

The solution I landed on: anonymous handles that persist in localStorage, chosen from a curated wordlist on first visit.

const ADJECTIVES = ['swift', 'nimble', 'sharp', 'quick', 'bright', /* ... */];
const NOUNS = ['falcon', 'arrow', 'spark', 'dash', 'flare', /* ... */];

function generateHandle(): string {
  const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
  const num = Math.floor(Math.random() * 99) + 1;
  return `${adj}${noun}${num}`;
}

// Store in localStorage so it persists across sessions
function getOrCreateHandle(): string {
  const stored = localStorage.getItem('clackpit_handle');
  if (stored) return stored;
  const handle = generateHandle();
  localStorage.setItem('clackpit_handle', handle);
  return handle;
}

The handle is sticky — come back tomorrow and you’re still swiftfalcon42. You can change it, but you don’t have to. It’s yours without signing up.

The problem with all-time leaderboards

Persistent leaderboards have a nasty property: they compound. The longer they exist, the more locked-in the top positions become. A new user who types 90 WPM sees a leaderboard full of 140+ WPM monsters and correctly concludes that competing is pointless.

The fix is surprisingly simple: reset the leaderboard every night at midnight UTC.

A daily leaderboard has a completely different social dynamic:

  • Everyone starts equal each day. The person who showed up for the first time today can be #1 by the end of the day.
  • Yesterday’s champion is gone. That 140 WPM monster? They have to earn it back.
  • Streaks become the real achievement. Staying in the top 10 for a week is more interesting than a stale number on a permanent board.

The implementation

I’m running on Cloudflare Workers with KV storage. The leaderboard is a sorted list stored in KV with a TTL that expires at the next midnight UTC.

// Calculate seconds until next midnight UTC
function secondsUntilMidnightUTC(): number {
  const now = new Date();
  const midnight = new Date(now);
  midnight.setUTCHours(24, 0, 0, 0);
  return Math.floor((midnight.getTime() - now.getTime()) / 1000);
}

async function submitScore(
  kv: KVNamespace,
  handle: string,
  wpm: number,
  accuracy: number
): Promise<void> {
  const key = 'leaderboard:daily';
  const ttl = secondsUntilMidnightUTC();

  const raw = await kv.get(key);
  const entries: LeaderboardEntry[] = raw ? JSON.parse(raw) : [];

  // Update or insert this handle's best score
  const existing = entries.findIndex(e => e.handle === handle);
  if (existing >= 0) {
    if (wpm > entries[existing].wpm) {
      entries[existing] = { handle, wpm, accuracy, ts: Date.now() };
    }
  } else {
    entries.push({ handle, wpm, accuracy, ts: Date.now() });
  }

  // Keep top 100, sorted by WPM descending
  entries.sort((a, b) => b.wpm - a.wpm);
  const top100 = entries.slice(0, 100);

  await kv.put(key, JSON.stringify(top100), { expirationTtl: ttl });
}

The TTL does the reset automatically. No cron job, no cleanup task. When the KV key expires, the leaderboard is gone. Next write creates a new one that expires at the next midnight.

What I learned

The daily reset changes behavior in ways I didn’t predict.

I expected it to just be “fairer.” What I didn’t expect was that it makes people check back more often. If you posted a 95 WPM score and there are 3 hours left in the day, you have a reason to come back tomorrow and see if you held on. If the leaderboard were permanent, there’s no urgency.

Anonymous handles have a surprising amount of personality. quietarrow71 feels different from swiftfalcon42. People started identifying with their handles even without accounts.

The all-time leaderboard problem is worse than I thought. After watching a few users interact with the board, I noticed that the first thing most people do is scroll to find where they rank. On a daily board, everyone is visible (it only has today’s players). On a permanent board, most newcomers wouldn’t appear at all. That invisibility is demoralizing.

If you want to see it in action: Clackpit. Sprint mode submits to the daily board after your race. Try it and see where you land before midnight resets it.

Curious what others have done for leaderboards — permanent, daily, weekly? Drop your approach in the comments.

Lessons from three months of vibe coding (and a complexity score of 53)

TL;DR — I spent three months vibe-coding a side project with an AI agent. It felt fast and productive until I opened a file and saw 3000 lines, functions that should have been one helper but were copy-pasted ten times, and a cyclomatic complexity score of 58 in one of them. The agent wasn’t the problem. The missing feedback loop was. This is what I should have set up on day one — and what I now consider non-negotiable when working with AI agents.

uv run complexipy . --top 10
./rounds/views.py
    current_state 58 (last: 52, Δ = +6)  ❌ FAILED
    host_advance_post_round 45 (last: 9, Δ = +36)  ❌ FAILED
    host_rewind_round 37 (new, Δ = +37)  ❌ FAILED
    audience_panel 34 (last: 35, Δ = -1)  ❌ FAILED
    debate_vote_results 18  ❌ FAILED
    host_create_game 16  ❌ FAILED
    _debate_winner_details 12  ✅ PASSED
    audience_prompt_submitted 11  ✅ PASSED
    host_select_vote_winner 11  ✅ PASSED
    preview_screen 11  ✅ PASSED

Failed functions:
 - ./rounds/views.py: audience_panel, current_state, debate_vote_results, host_advance_post_round, host_create_game,
host_rewind_round

The moment it clicked

I wasn’t running a tool. I wasn’t reviewing a PR. I was just poking around the repo to remind myself where something lived, and I opened a file that had grown past 3000 lines.

Inside, I found functions that should have been one shared helper, copy-pasted with one or two lines different each time. Every time I’d asked the agent to add a feature that was “similar but slightly different,” it had just duplicated the whole thing and tweaked the bits that varied.

I ran radon out of curiosity. One function clocked in at cyclomatic complexity 58. Fifty-three branches in a single function. That was the moment I realized I had a problem I couldn’t easily fix.

How I got there

I want to be honest about how this started, because I think it’s the most common path.

It was a small idea — a Django side project. I knew how to code; this wasn’t me learning Python from scratch. But I was learning how to work with an AI agent. I didn’t know the workflows, the tooling around agents, what good guardrails looked like, or how other people were structuring this kind of work. So I was figuring it out as I went.

I told myself I was doing spec-driven development. I’d write a description of what I wanted, hand it to the agent, and review the output. In reality, I was vibe coding. I wasn’t actually reading the diffs. I was checking that the feature worked — does the endpoint return the right thing? does the test pass? — and moving on. The agent shipped, I accepted, repeat.

For weeks it felt great. Features landed. Tests were green. I had momentum.

The slow collapse

The problem wasn’t a single bad commit. The problem was a thousand small, locally rational choices.

Every time I asked for a feature that was “like the last one but with a small change,” the agent didn’t extract a shared helper — it copied the previous implementation and edited it. That’s the safest move for the agent in any given turn: copying working code is less likely to break than refactoring. Without anything pushing back, copying wins every time.

The if/elif chains kept growing. New cases got appended, not refactored into a dispatch table or a strategy. Branches piled on branches. One function ended up at CC 58.

The CSS — same story. New components got their own styles, copy-pasted from the previous component, slightly tweaked. There was no design system, no shared tokens, no enforced layering. It just sprawled.

By the time I noticed, two things were true at once:

  1. The codebase was big enough that I couldn’t hold it in my head anymore.
  2. I was afraid to touch it myself. Not because I didn’t know how — because any change risked breaking something I couldn’t reason about. The agent had become the only entity that could navigate the mess, and it was the one that had created the mess.

That’s the worst place to be. The point of using an AI agent is to amplify what you can do. If you end up unable to refactor your own code without the agent, you haven’t amplified anything. You’ve outsourced something you can’t take back.

The actual lesson

Here’s what I missed: the agent has no perception of whether code is good or bad. None. It doesn’t know your conventions, your architecture, your standards. It doesn’t get tired. It doesn’t refactor unless something tells it to.

I thought I was giving it feedback. I wasn’t. “Make it cleaner” is not feedback an agent can act on. “Reduce complexity” is not feedback either. The agent needs feedback that is specific, machine-readable, and automatic — the kind you get from a linter, a type checker, an architectural test.

When I had no feedback loop, the agent did exactly what an unsupervised junior dev would do under pressure: it solved each problem in the easiest local way. Copy-paste. Append a branch. Skip the refactor. Each move was rational on its own. The aggregate was slop.

The reframe that finally landed for me: the agent behaved correctly given the environment I gave it. The environment was the bug, not the agent.

Why quality gates are non-negotiable now

Quality gates — linters, type checks, complexity limits, architectural fitness functions, duplication detectors — were a “good engineering practice” before AI. They were nice to have. You could ship without them.

With AI-assisted development, that changes. Here’s why:

  • The rate of code production is now far higher than your rate of code comprehension. Your mental context window didn’t grow when you started using an agent. The codebase’s growth rate did.
  • The agent has no memory of your conventions across sessions. Anything you don’t encode in tooling will drift.
  • The agent has no incentive to refactor. Refactoring increases diff size and risk. Copying is cheaper. Without a gate, entropy wins.
  • “Just review it carefully” doesn’t scale. That defeats the entire point of using an agent. If every diff needs a human read for style and complexity, you’ve lost the speedup.

So gates aren’t optional anymore. They’re prerequisites. You set them up before the agent writes a line, not after. The time you spend configuring them up front is the rule of our job now — it’s not overhead, it’s the work.

Matt Pocock frames this as “outrunning your headlights”: AI generates code faster than you can verify it, and the only way to stay inside the beam is the same discipline that always kept engineers honest — incremental delivery, test-first, and structures you can reason about. The gates are what put the headlights back on.

When the agent produces something that violates a gate, the failure is automatic, specific, and machine-readable. You feed it back to the agent, the agent fixes it, you move on. That is the feedback loop I was missing for three months.

The checklist I wish I’d had on day one

I’m building this in tiers. Pick the tier that matches the project’s lifespan.

Tier 0 — Bare minimum (any project, even a weekend hack)

Set up before the agent writes a single line.

  • pyproject.toml with ruff for lint + format. Enable complexity rules (C901, max-complexity around 10), bugbear (B), simplify (SIM), pyupgrade (UP), and a sensible subset of pylint (PL).
  • Strict type checking with mypy or pyright in strict mode for your own modules.
  • Pre-commit hooks wired to both, so violations can’t even land in a commit.
  • A task runner like mise (or a Makefile / justfile) with check, test, fix tasks. One obvious command for the agent to run.
  • A short CONTRIBUTING.md, AGENTS.md, or .cursorrules stating: max function length, max complexity, layering rules (router → service → repository), no business logic in routers, no SQL outside repositories, no copy-paste — extract a function or ask.
  • A skill library that encodes your architecture decisions as agent-readable workflows, not just rules. Rules tell the agent what not to do; skills tell it what to reach for. superpowers, get-shit-done, and mattpocock/skills are good starting points — or write framework-specific ones: “when building a FastAPI endpoint, always layer router → use case → repository, never skip levels.”

Tier 1 — Recommended for anything you’ll touch in a month

  • pytest with coverage gated at a real number (60–80%, not 100%).
  • A duplication detector (pylint --enable=duplicate-code, or jscpd).
  • bandit for security smells.
  • CI that runs all of this on every PR and blocks merge.
  • A single integration test that boots the app and hits /health. This alone catches most “the agent broke imports” failures.

Tier 2 — Production-grade

  • Architectural tests — Python lets anything import anything; you need to enforce lanes explicitly. Three options depending on taste: import-linter (contract-based, closest to config), pytest-archon (same idea expressed as pytest tests — easier for complex rules), deply (layer-based, closer to ArchUnit if you’re coming from Java). Good writeups: roman.pt, dev.to/vashkatsi.
  • Mutation testing on critical modules (mutmut). Agents love writing tests that pass without testing anything.
  • Dependency scanning (pip-audit).
  • Complexity tracked over time with radon or xenon in CI — fail on regressions, not just absolute thresholds.
  • Contract tests against your OpenAPI spec (schemathesis). Free coverage given you’re on FastAPI.

How to give the agent useful feedback

This is the part that changed my workflow most.

Don’t say: “this code is messy, clean it up.”
Do say: “here is the output of ruff check app/services/user_service.py. Fix every finding without using # noqa. If a fix changes a function signature, list the call sites first and propose the change.”

Don’t say: “reduce complexity.”
Do say: “this function dispatches on event_type with a 58-branch if/elif chain. Replace it with a dict mapping event_type to handler functions, one handler per branch, each unit-tested.”

The pattern: specific, mechanical, verifiable. Name the refactor. Cite the tool output. Constrain the solution. That’s how you get a refactor instead of a rename.

The tooling in my reference project

After the Django project, I started a separate FastAPI reference project specifically to encode what I’d learned. Different framework, but the principles are framework-agnostic. The point isn’t FastAPI vs. Django — the point is what the setup looks like before any feature code lands.

You can find it here: github.com/maxkrivich/fast-api-reference-project

A quick note before the tool list: you don’t need enterprise tooling for this. SonarQube, Codacy, Snyk, and if your company already pays for them, use them. But every gate I’m about to list is open source and free, and the entire point — feeding structured failures back to an agent — works just as well with ruff and mypy as it does with a commercial SaaS. Don’t let “we don’t have SonarQube” be the reason you skip the gates.

Here’s what’s in the repo, briefly:

  • ruff — single tool for lint + format, fast enough to run on save. Complexity is gated at max-complexity = 10 — a threshold that would have caught my CC=58 immediately. Config lives in ruff.toml.
  • ty in strict mode — Astral’s new type checker. Catches a huge class of agent mistakes (wrong types, missing returns, unhandled Optional) before they reach runtime.
  • prek — Rust-based git hooks manager (same idea as pre-commit, but faster and no Python dependency). Runs ruff, bandit, import-linter, pylint, gitleaks, and hadolint on every commit. Config in prek.toml.
  • mise — one entry point for tasks and tool versions. The agent runs a single command (mise run full-ci-check) and gets a structured report it can act on. Config in mise.toml.
  • pytest + coverage — the baseline. Gated at a real number, not aspirational.
  • bandit — security smells. Catches agent moves like subprocess.run(..., shell=True) with user input.
  • pip-audit — dependency CVEs, run as uv audit in CI. Agents will happily pin a vulnerable version if you don’t check.
  • import-linter — enforces hexagonal architecture via import contracts. Seven contracts defined in pyproject.toml that make the build fail if a router imports a SQLAlchemy session directly, or a domain model touches an HTTP adapter. Python lets anything import anything — this is how you enforce the lanes.
  • pylint (duplicate-code only) — runs on every commit via prek, flagging copy-paste before it accumulates. The exact failure mode that wrecked my Django project.
  • radon / xenon — complexity tracking. In dev deps for local inspection; the plan is to wire xenon into CI to fail on regressions, not just absolute thresholds.
  • CI via GitHub Actions — every PR runs the full gate set; nothing merges that hasn’t passed. Workflows live in .github/workflows/.

What gates won’t catch

Honest disclaimer, because I don’t want to oversell this.

Gates catch mechanical slop — complexity, duplication, style, types, surface-level security. They don’t catch architectural slop — wrong abstractions, leaky boundaries, anemic domain models, a service that should have been three services. They don’t catch a bad data model. They don’t catch a missing concept.

What gates do is reclaim your attention. Once mechanical quality is automated, your code reviews shift from “is this clean?” to “is this the right shape?” That’s a much better use of human attention, and it’s the only kind of review that’s actually worth your time when working with an agent.

You still have to think. The gates just stop you from drowning while you do it.

Where I’m at now

The project that hit CC 58 is at a point where I think it’s easier to rewrite than refactor. That’s a hard thing to admit, but it’s where I am. The next version starts with the checklist in place before any feature lands.

If there’s one thing I’d want you to take from this: set up the gates first. Not after the first feature. Not after the first refactor. First. The hour you spend configuring ruff and pre-commit on day one is the hour that prevents the month you’d otherwise spend untangling slop on day ninety.

The agent is not the problem. The environment we give it is.

If you’ve been through something similar, or have tooling I missed, I’d genuinely like to hear it — leave a comment or ping me on GitHub.

30 CVR Improvement Checks for EC Sites — Where to Start When Sales Don’t Move

30 CVR checks across 6 areas

“Ad sessions keep coming in, but purchases are not growing.” “Add-to-cart looks fine, but checkout drops off.” Every EC operator hits this wall, and the answer is almost always the same one — a CVR (Conversion Rate) leak somewhere in the funnel.

CVR is a simple formula — purchasing sessions ÷ all sessions — but the levers that move it are scattered across 30+ tactics, and the order you fix them in matters more than the list itself. If you start from the bottom of the impact curve, you spend three weeks on a banner test that moves nothing.

This post organizes the most common CVR improvement levers into 6 areas × 5 items = 30 checks, sorted by impact range and engineering effort. Fix them top to bottom and you fix the ones that move revenue first.

TL;DR

  • CVR improvement levers fit into 6 areas: Tech, UX, Trust, Pricing, Cart & Checkout, Personalization. Each area has a different impact range and effort cost.
  • The order to fix them in is the top-left of an Impact × Effort 4-quadrant map: LCP, cart save, shipping disclosure, form minimization — 8 items in the high-impact × low-effort quadrant move roughly half of the available revenue.
  • Chasing CVR alone is a trap: aggressive couponing lifts CVR but drops AOV and gross margin. Watching CVR, AOV, and RPS (Revenue Per Session) together is the safer view.

The big picture — where in the funnel does CVR actually leak

CVR is one of the three components of the revenue decomposition formula:

Revenue = Sessions × CVR × AOV

If sessions stay flat and CVR moves from 1.5% to 2.0%, revenue grows by roughly 33% with zero extra ad spend. That is why CVR improvement is one of the highest-ROI levers in EC operations.

CVR itself is not a single number — it is the product of pass-through rates at each funnel stage:

  • Product Detail Page (PDP) view rate
  • Add-to-cart rate
  • Checkout initiation rate
  • Purchase completion rate

Identifying which stage is leaking — using GA4 funnel reports, or a real-time visitor feed — narrows the candidate fixes dramatically before you spend any engineering time.

Here is the full 6-area × 5-item map at a glance.

30 CVR checks across 6 areas

Tech area — speed and errors quietly bleed CVR

The Tech area is the foundation. If Core Web Vitals are broken, every UX and pricing improvement on top of it leaks into the void.

  1. LCP (Largest Contentful Paint) under 2.5s — the largest above-the-fold element (usually the hero image or product image) needs to paint fast.
  2. CLS (Cumulative Layout Shift) under 0.1 — buttons that shift during load cause misclicks and silent abandonment.
  3. 404 monitoring — old SKUs and retired URLs hit from newsletters and ad creatives drop into 404 land.
  4. Mobile layout regressions — weekly QA across iOS Safari, Android Chrome on the form and the buy button.
  5. HTTPS and mixed content — leftover http:// references in images or iframes break the lock icon.

Out of these five, the highest-leverage one is LCP improvement. The rough field heuristic among EC operators is that moving LCP from 4s to 2s lifts CVR by about 5–15%.

UX area — forms and navigation are the real abandonment drivers

If product intent is high but purchases are not landing, the leak is almost always in the UX area.

  1. Checkout form fields ≤ 8 — strip to address, name, phone, card.
  2. Guest checkout available — forcing account creation kills CVR by 20–30%.
  3. CTA button color and label — clear contrast and unambiguous labels (Add to Cart, Checkout).
  4. Search bar accuracy — handle typos and product name variants.
  5. Mobile scroll depth on PDP — the add-to-cart button needs to be visible without long scrolling.

The standout here is opening guest checkout. Effort is small, CVR impact is large. According to Baymard Institute‘s cart abandonment research, “had to create an account” sits in the top reasons for cart abandonment year after year.

Trust × Pricing × Cart — 15 checks that hold the “just about to buy” moment

Users with intent get pushed back at the last step by three areas — trust, pricing, and the cart/checkout flow itself.

Trust area (5 checks)

  1. Shipping cost and ETA shown up-front — discovering shipping cost only at the cart is the single most cited cart abandonment cause.
  2. Returns and refund policy reachable in 1 click from PDP.
  3. Review count and star rating displayed at the top of PDP.
  4. SSL mark and payment logos — Visa, Mastercard, PayPay, Amazon Pay logos signal accepted methods.
  5. Contact path — chat or phone number in both the footer and PDP.

Pricing & promotion area (5 checks)

  1. Compare-at price — show MSRP vs sale price, or a one-line market reference.
  2. Free-shipping threshold hint — “¥X away from free shipping” on the cart.
  3. Coupon clarity — never spring a coupon field at the last checkout step.
  4. Out-of-stock alternatives — recommend same-category items when stock is zero.
  5. Restraint on flash-sale urgency — overdone urgency actually drops CVR.

Cart & checkout area (5 checks)

  1. Persistent cart — hold cart contents for 7+ days without login.
  2. Multiple payment methods — at least 3 of card, conbini, PayPay, Amazon Pay, BNPL.
  3. Postal code auto-fill for address input.
  4. Numeric keyboard invoked for card number on mobile.
  5. Specific error messages — Please check the card number length beats Card number is invalid.

Out of these 15, the three with the strongest cost-to-impact ratio are persistent cart × shipping disclosure × free-shipping threshold. The effort is roughly one engineering day combined, and the CVR lift typically clears single-digit percent.

Personalization — the last nudge, built into the system

The final push toward conversion comes from showing each visitor something different based on their context.

  1. PDP recommendations based on browsing history.
  2. Cart abandonment recovery email within 24 hours, one message.
  3. Exit-intent popup — trigger free-shipping messaging when the mouse moves toward the browser chrome.
  4. Loyalty perks surfaced to returning visitors.
  5. Live chat triggered right before exit.

Now that all 30 items are on the table, the impact range × effort 4-quadrant map below shows where to start. The 8 items in the top-left (high impact × low effort) move roughly half of the available revenue first.

CVR improvement: Impact × Effort 4-quadrant map (1-30 numbered)

After all 30 checks are addressed, the CVR ceiling on the site itself starts to show.

The “CVR-only” trap, and watching revenue instead

Going through all 30 checks will lift CVR — but the revenue lift tends to come in smaller than expected. The reason is that some CVR-lifting moves quietly drop AOV.

A “20% off site-wide coupon” lifts CVR in the moment, but AOV and gross margin drop in parallel. Plugging it back into Revenue = Sessions × CVR × AOV, revenue moves up slightly while gross profit goes down. CVR is a great metric to chase, but only when watched alongside AOV.

I’m building a tool called RevenueScope that watches CVR, AOV, and RPS (Revenue Per Session) on one screen, broken down by channel. GA4 measures the traffic side beautifully but requires hopping across reports to get to CVR — RevenueScope approaches it from the revenue side first and reads back to the channel, so the impact of each of these 30 checks shows up as “which channel moved how much in revenue” instead of “which page moved in CVR alone”.

“Fix all 30 CVR checks” is step one. Step two — verifying that the fixes moved revenue, not just CVR — is the part that protects you from the coupon trap.

Related posts on the same series:

References

  1. Google Web.dev — Core Web Vitals (May 2026)
  2. Baymard Institute — 46 Cart Abandonment Rate Statistics 2024 (2024)

What is the one check on your own site you have been postponing the longest? Mine was guest checkout for too long. Curious whether the Tech-first ordering (LCP before everything else) matches what other operators are seeing, or if you start from UX/forms — drop a note below.

Which Tech Roles Will Dominate Vietnam’s IT Industry in the Next Few Years?

Vietnam’s IT Market Feels Like It’s Entering A Different Era

Over the past few months, I’ve been seeing more and more discussions around salaries, hiring trends and career directions in Vietnam’s tech industry. And honestly, the market feels very different compared to just a few years ago.
Back then, the “safe route” usually meant:

  • frontend/backend development

  • mobile engineering

  • fullstack paths

  • climbing toward senior developer or tech lead

But recently the conversation has shifted heavily toward:

  • AI Engineering

  • MLOps

  • Data Engineering

  • GenAI integration

  • AI-assisted workflows

At the same time, many developers are also starting to question whether traditional web/app development is becoming oversaturated.

A Vietnam IT Salary Guide I Found Surprisingly Insightful

Out of curiosity, I looked through this: https://www.jt1.vn/salaryguide

A few insights from it were surprisingly interesting. The biggest thing that stood out was how AI/ML-related roles are currently among the highest-paid positions in Vietnam’s IT market.

According to the guide, engineers with AI/ML, MLOps and GenAI integration skills are earning noticeably higher compensation compared to many traditional engineering positions.

The report also mentioned something called the “Trust Paradox” in AI development: developers increasingly use AI tools for speed and productivity but still don’t fully trust the outputs generated by AI systems. That idea honestly feels very real right now.

Developers May Be Shifting From “Coding” to “Supervising AI”

With tools like Copilot, Cursor, GPT-based workflows, and AI agents becoming more common, it feels like developers are slowly moving toward a different role. Not necessarily replacing engineers but changing what engineers actually do.

Instead of writing every line manually and solving everything from zero, developers may spend more time: reviewing AI-generated outputs, debugging generated code, designing architecture, validating business logic and orchestrating systems.

So What Will Actually Dominate The Next Few Years?

That’s the part I’m curious about. Right now AI/ML looks incredibly hot, especially in terms of salary growth and hiring trends, but tech changes insanely fast.

A few years ago everyone was talking about blockchain, Web3, crypto and metaverse. Now the conversation is completely different again.
So I’m wondering do you think AI/ML-related roles will truly dominate the next phase of the industry, or will another field/position become the next major wave in tech?

Would love to hear perspectives from people already working in different areas of the industry.