I built an open dataset of 1,119 SaaS webhook events. Here’s what surprised me.

If you have ever tried to wire up webhooks from more than three SaaS apps, you already know the punchline: every vendor invented their own conventions, and none of them are wrong, but none of them agree either.

I was building agent tooling that had to understand many of them. Halfway through, I stopped trying to keep it in my head and started writing it down. Then I kept writing it down. The result is now an open dataset.

1,119 webhook events. 30 platforms. One schema. Free, CC-BY-4.0.

What it is

A normalized catalog covering Stripe, GitHub, Slack, Notion, Linear, Jira, HubSpot, Salesforce, Zendesk, Intercom, Discord, Twilio, Calendly, Mailchimp, Zoom, Microsoft Teams, PagerDuty, Pipedrive, Asana, ClickUp, Front, Help Scout, Loom, Greenhouse, Ashby, BambooHR, Gusto, Attio, Close, and Freshdesk.

For every event, you get:

  • event_name and trigger_description
  • payload_schema as JSON Schema (draft 2020-12)
  • auth_method, signature_header, and the exact signing algorithm
  • delivery_guarantees and retry_policy
  • idempotency_key_header (if the vendor provides one)
  • docs_url back to the canonical vendor docs
  • Format: JSONL per vendor, plus Parquet for the full set

A sample row

Here is what a single Stripe event looks like, trimmed:

{
  "vendor": "stripe",
  "category": "payments",
  "event_name": "account.application.authorized",
  "trigger_description": "Fires when a user authorizes a Stripe application.",
  "auth_method": "hmac-sha256",
  "signature_header": "Stripe-Signature",
  "signature_algorithm_detail": "HMAC-SHA256 with versioned scheme; header contains timestamp and v1 hash. Verify timestamp to prevent replay attacks.",
  "delivery_guarantees": "at-least-once",
  "retry_policy": {
    "max_attempts": null,
    "backoff": "Exponential backoff over multiple hours.",
    "total_retry_window": "PT72H"
  },
  "payload_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "...": "..." } },
  "docs_url": "https://docs.stripe.com/api/events/types"
}

Same shape across all 30 vendors. That is the entire point.

The surprising stuff

A few things jumped out once everything was in one schema:

Signing is not standardized in any way. A small sampler:

Vendor Auth Header Detail
Stripe HMAC-SHA256 Stripe-Signature Timestamp + v1 hash, replay-window enforced
GitHub HMAC-SHA256 X-Hub-Signature-256 Plus legacy SHA1 on X-Hub-Signature
Slack HMAC-SHA256 X-Slack-Signature 5-minute timestamp window
Shopify HMAC-SHA256 X-Shopify-Hmac-Sha256 Base64 of HMAC of raw body
Linear HMAC-SHA256 Linear-Signature Hex digest only

Same algorithm, five different envelopes. Anyone writing one verifier and trying to reuse it has a bad afternoon ahead.

Retry policies are wildly different. Stripe retries for 72 hours with exponential backoff. GitHub does not retry by default at all (it depends on app type). Slack retries 3 times. Some vendors do not publish a retry policy in their docs, which means you should not rely on one.

Idempotency support is hit-or-miss. GitHub gives you X-GitHub-Delivery. Stripe gives you the event id. Several vendors give you nothing, which means you either dedupe by payload hash or accept duplicates.

Max payload sizes are mostly undocumented. GitHub publishes 25 MB. Most vendors do not say.

These are not opinions. They are facts I would rather not have had to learn the hard way.

Why this exists

I am building agents that integrate with many SaaS products. Two things have to be true for an agent to call any webhook tool correctly:

  1. The agent needs the payload schema to know what fields it can rely on.
  2. The agent needs the auth contract to know how to validate inbound deliveries.

If that information is scattered across 30 different docs sites in 30 different shapes, the agent cannot use it. Once it is in a single schema, the agent can.

Same logic applies to anything that has to interop with many vendors at once: an integration platform, a security scanner that audits webhook configurations, a docs site that needs to render comparison tables, a research notebook.

Use it however

from datasets import load_dataset

ds = load_dataset("automatelab/saas-webhooks")

stripe_events = ds["train"].filter(lambda r: r["vendor"] == "stripe")
print(stripe_events[0]["payload_schema"])

The dataset is CC-BY-4.0. No email gate, no sign-up. Attribution is appreciated when you ship something interesting on top of it.

It updates monthly from source. If you find a missing event or a vendor that should be there, the GitHub issue tracker is the right place to drop it.

Links

If you build something on top of it I would genuinely like to know. The whole point of putting it under CC-BY is that the work compounds.

Watch out, your recruiter might be a scam

it

A recruiter shows up with a good-looking AI engineer role.

The company looks plausible enough. The position sounds real. The process moves like a normal interview process. Nothing too strange at the beginning.

Then comes the technical test.

“Clone this private repository and run it locally.”

That was the moment the whole thing started to smell wrong.

The candidate refused to run it on the main machine, which was the correct decision. Later, the repository was inspected safely inside a virtual machine. And the result was not “bad code”, or “a sloppy take-home assignment”, or “some weird dependency problem”.

It was malware.

Hidden inside the repository was a .vscode/tasks.json file that tried to auto-execute a curl | bash command when the project was opened. That command downloaded a dropper from a disguised domain. The dropper installed another script under ~/.vscode/, used nohup for persistence, and fetched a second stage payload.

That second stage was not subtle about its intentions. It went after crypto wallets, SSH keys, AWS credentials, browser sessions, environment variables, and opened a command-and-control channel.

And if the candidate had simply run npm start, the project would have sent the entire process.env to an external server and executed arbitrary JavaScript received in the response.

That is full remote code execution.

Not “oops, I installed a bad package”.

Not “this repo looks suspicious”.

Full compromise of a developer workstation.

it's a trap

this has a name

This is not an isolated scam.

It is part of a documented campaign known as Contagious Interview, attributed to North Korean state-backed operators connected to the Lazarus ecosystem. Different vendors track pieces of this activity under names like NICKEL ALLEY, Void Dokkaebi, and related DPRK clusters.

The pattern is brutally effective because it abuses something developers already consider normal: technical interviews.

A fake recruiter contacts a developer. The job is usually in crypto, Web3, AI, trading, or some adjacent high-salary niche. The conversation looks professional enough. The candidate gets a coding assignment. The assignment is hosted on GitHub, GitLab, or Bitbucket. The repository looks like a normal app.

Then the trap closes.

Sometimes the malware is hidden in an npm dependency. Sometimes it is in a package script. Sometimes it is in obfuscated JavaScript. Sometimes it is fetched from a cloud host like Vercel. And increasingly, it is wired into developer tooling itself.

That last part matters.

Microsoft has documented cases where the attack uses Visual Studio Code workflows. The victim opens the downloaded project in VS Code, gets the normal Workspace Trust prompt, and if they trust the folder, VS Code can execute the repository task configuration. Sophos documented the same abuse pattern through .vscode/tasks.json, with tasks configured to fetch malware using curl or wget depending on the victim’s operating system.

That means the dangerous action is not always “run the app”.

In some cases, the dangerous action is “open the repo in the editor and trust it”.

That is a very different risk model from the one most developers carry in their heads.

why developers are the target

Attackers are not targeting developers because we are special.

They are targeting developers because our machines are ugly treasure chests.

A normal developer laptop may contain:

  • SSH private keys
  • GitHub and GitLab sessions
  • AWS credentials
  • GCP or Azure credentials
  • Kubernetes configs
  • .env files
  • production-like database URLs
  • CI/CD tokens
  • package registry tokens
  • browser cookies
  • password manager sessions
  • crypto wallet extensions
  • source code for private systems

That is not “one endpoint”. That is a jumping point into companies, cloud accounts, deployment systems, source repositories, and sometimes customer data.

This is why the campaign is so focused on engineers, especially people working around crypto, trading, AI, infrastructure, and startup ecosystems. A recruiter can pressure a job seeker in a way a random phishing email cannot. The victim wants the job. The victim wants to look responsive. The victim does not want to be the annoying person who refuses to run the take-home project.

That social pressure is the exploit.

The malware is just the implementation detail.

the campaign is industrial now

The scale is the part that should make engineering leaders pay attention.

This is not one clever operator manually sending sketchy repositories on LinkedIn.

Socket has been tracking the Contagious Interview supply-chain activity across package ecosystems and reports hundreds of malicious npm packages tied to the campaign. In one 2025 wave alone, Socket counted 338 malicious npm packages with more than 50,000 downloads. By the later 2025 waves, public reporting had counted well over 535 malicious npm packages and more than 80,000 total downloads. The campaign tracker later showed activity continuing into 2026 across npm, PyPI, Cargo, Go, and Composer artifacts.

Dark Reading, citing Trend Micro research, reported that the campaign evolved into a worm-like supply-chain threat: compromised developer projects can carry malicious VS Code task configuration, which then spreads when other developers clone and trust the repository. Trend Micro also reported more than 750 infected repositories and more than 500 malicious VS Code task configurations in March 2026.

Sophos’ NICKEL ALLEY research shows the same operator pattern from another angle: fake companies, fake LinkedIn credibility, GitHub accounts posing as legitimate software organizations, Vercel-hosted payloads, and repositories dressed up as Web3 or full-stack projects. In many observed cases, the repository is only the loader. The real payload lives off-platform on Vercel or attacker-controlled infrastructure, which makes takedowns and static repository review less reliable.

Microsoft’s write-up names payload families such as OtterCookie, BeaverTail, Invisible Ferret, and FlexibleFerret. The important point is not the malware branding. The important point is what they do: exfiltrate secrets, profile the machine, monitor clipboard contents, take screenshots, fetch more modules, and execute attacker-supplied commands.

This is an interview pipeline turned into malware delivery infrastructure.

And it has been active for years. Microsoft traces related Contagious Interview activity back to at least late 2022. Other reporting describes heavy abuse from 2023 onward. It is still active in 2026.

what gets stolen

If this lands on your real machine, assume the attacker is not just stealing the toy project.

Assume they are going after:

  • crypto wallets and wallet browser extensions
  • seed phrases and private keys
  • SSH private keys
  • AWS credentials
  • cloud provider configs
  • GitHub, GitLab, and Bitbucket sessions
  • npm and package registry tokens
  • browser session cookies
  • .env files
  • clipboard contents
  • screenshots
  • keystrokes
  • source code
  • CI/CD access

Also assume they may keep access.

Some payloads are remote access trojans. Some open command-and-control channels. Some install persistence. Some fetch new code after the first stage has already passed the easiest checks.

That is why “I opened it but did not run anything” is no longer a good enough safety line. If an editor task fired, if a dependency installed, if a package script ran, or if some obfuscated loader fetched a second stage, the damage may already be done.

the red flags

Here is the checklist I would keep close if I were interviewing right now.

🚩 They ask you to clone a private or unfamiliar repository and run it locally.

🚩 The company exists only on LinkedIn, with no convincing web presence, customer footprint, team page, funding history, or normal public trail.

🚩 The recruiter found you, but has no mutual connections, a thin profile, or a recently created account.

🚩 The technical test is a whole project to download instead of a sandboxed exercise.

🚩 They are in a hurry: “just run it, it is quick”, “we need this today”, “screen share while you install it”.

🚩 The repo contains .vscode/tasks.json, suspicious workspace settings, postinstall or preinstall scripts, obfuscated dependencies, minified blobs, encoded URLs, or network calls during startup.

🚩 They insist you use your own machine instead of a sandbox, VM, browser IDE, or company-provided environment.

🚩 The role is crypto, Web3, trading, AI, or developer tooling, especially if the compensation is strangely generous for the amount of process involved.

And the counter-check:

✅ Legitimate technical interviews usually use CoderPad, HackerRank, LeetCode, Replit shared sessions, GitHub Codespaces, or a constrained environment.

✅ Legitimate companies can verify the recruiter, company domain, interview process, and hiring manager.

✅ Legitimate recruiters do not need you to run proprietary mystery code on your personal machine.

✅ Legitimate engineering teams should not get offended when a developer asks for a safer way to inspect a take-home assignment.

If they do get offended, that is useful signal.

what to do instead

First defence: scan the repository before opening it.

Use scanrepo.dev. It checks repositories for hidden scripts, suspicious dependency behavior, malicious patterns, and known attack signatures. This is exactly the kind of lightweight defensive step that should become normal for recruiter-provided code.

Then slow down.

Do not open an untrusted repository directly in your main VS Code profile. Do not grant Workspace Trust to a folder just because the prompt appears during an interview. That prompt is not decoration. It changes what the project is allowed to do.

If you must inspect the code locally, use a disposable VM or a container with no access to your home directory, SSH keys, cloud credentials, browser profiles, password manager, or clipboard history.

Before running anything:

  • inspect .vscode/tasks.json
  • inspect .vscode/settings.json
  • inspect package.json
  • search for postinstall, preinstall, prepare, and install scripts
  • search for curl, wget, bash, sh, powershell, eval, Function, child_process, exec, spawn, axios, fetch, and suspicious encoded strings
  • check for hidden files and unusual directories
  • check commit history
  • check contributor profiles
  • check whether dependencies are new, typosquatted, or oddly unpopular
  • check whether the app calls external servers on startup

For VS Code specifically, review Workspace Trust settings under:

Settings -> Security -> Workspace Trust

If you regularly inspect unknown code, create a separate editor profile for hostile repositories. Better yet, inspect them in a VM and throw the VM away afterwards.

For npm projects, never start with npm install on your real machine. Package lifecycle scripts are code execution. A malicious postinstall is not less dangerous because it is in package.json.

For AI and crypto roles, be even more suspicious. That is where the attacker return is highest.

what companies should change

This is not only an individual developer problem.

Companies should stop normalizing “clone this repo and run it locally” as an interview pattern.

If your hiring process requires candidates to run code, provide a sandbox. Use a browser IDE, a Codespaces-style environment, a prepared VM, or a constrained runner. Make it boring. Make it auditable. Make it safe.

And if you are hiring engineers, say explicitly that candidates are not expected to run unknown code on their personal machines. That one sentence would remove a lot of social pressure from the candidate.

Security teams should also treat recruitment as an attack surface. Developers interviewing elsewhere may still be using corporate machines. Even if that violates policy, it happens. If your protection strategy assumes engineers never mix personal hiring activity with work devices, your strategy is based on a fantasy.

Monitor for editor-launched shells. Watch for VS Code or Cursor spawning bash, curl, wget, PowerShell, Python, or Node processes that immediately talk to low-reputation domains. Look for package installs that launch network activity unrelated to normal registries. Hunt for broad filesystem enumeration around .env, wallet, key, password, seed, and credential patterns.

This campaign is successful because it lives in the gap between “security training” and “how developers actually work”.

Close the gap.

trust your instinct

The candidate in the story did the most important thing right: they paused.

That is the real lesson.

Not every weird recruiter is Lazarus. Not every bad take-home assignment is malware. Not every small company with a thin LinkedIn page is fake.

But a stranger asking you to clone a private repo and run it locally is not a harmless request anymore.

It is an executable trust decision.

Treat it like one.

If it feels off, stop. Scan it. Open it in a disposable environment. Ask for a sandbox. Ask them to verify the company and process. Ask why the test cannot run in a browser environment.

A real company will survive those questions.

A fake recruiter will get impatient.

That impatience may be the warning that saves your machine.

references

I Built a Next-Gen DNS Diagnostic CLI in Rust That Visualizes DNSSEC Trust Chains

If you’ve ever tried to debug a DNSSEC misconfiguration using dig, you know the pain. You’re staring at a wall of raw text, manually cross-referencing DS records against DNSKEY records, tracing through TLD delegations one query at a time. It works — but it’s exhausting.

I wanted something better. So I built shohei — a Rust-powered DNS diagnostic CLI that makes the invisible visible.

github.com/kent-tokyo/shohei

What Is shohei?

shohei is a next-generation DNS diagnostic tool that goes well beyond what dig or drill offer. It renders DNS resolution as color-coded terminal trees, walks you through DNSSEC trust chains step by step, and supports modern transports like DoH and DoT — all from a single command.

Think of it as dig if dig was designed for humans first.

The Three Hard Problems shohei Solves

Building a DNS diagnostic tool sounds straightforward until you try to make it actually useful. Here are the problems that motivated most of the design.

1. DNSSEC Trust Chains Are Invisible in Standard Tools

DNSSEC forms a cryptographic chain from the root zone down to individual records. The chain looks like this:

Root KSK (trust anchor, hardcoded in resolvers)
  └── Root ZSK (signs root zone records)
        └── DS record for .com  ← hash of .com's KSK
              └── .com KSK
                    └── .com ZSK (signs .com zone)
                          └── DS record for example.com
                                └── example.com KSK
                                      └── example.com ZSK
                                            └── RRSIG on A record ← what you care about

When something breaks in this chain — a DS mismatch, an expired RRSIG, a missing DNSKEY — dig gives you raw records and leaves you to piece together the failure yourself. shohei walks the whole chain and tells you exactly where validation fails and why.

. (Root)
└── com.  [DS 30909 → DNSKEY 30909 ✓]
    └── example.com.  [DS 12345 → DNSKEY 12345 ✓]
        └── www.example.com.  A 93.184.216.34
            └── RRSIG (ZSK tag 12345) expires 2026-06-01 ✓

A broken chain looks like:

. (Root)
└── com.  [DS 30909 → DNSKEY 30909 ✓]
    └── example.com.  [DS 99999 → DNSKEY ✗ NO MATCHING KEY FOUND]
        └── ⚠ Validation failed: DS/DNSKEY mismatch at example.com.

2. Iterative Resolution Is a Black Box

Standard resolvers are recursive — they do the work for you and return an answer. That’s convenient, but useless when you’re debugging a delegation problem, a glue record mismatch, or propagation that hasn’t reached a specific nameserver yet.

shohei’s --trace mode sends queries the way a resolver actually would — manually, hop by hop:

[1] Query root servers for example.com. A
    → a.root-servers.net. (198.41.0.4)  4ms
    ← REFERRAL: com. NS [a.gtld-servers.net., ...]

[2] Query TLD servers for example.com. A
    → a.gtld-servers.net. (192.5.6.30)  11ms
    ← REFERRAL: example.com. NS [ns1.example.com., ns2.example.com.]
       GLUE: ns1.example.com. A 205.251.196.1

[3] Query authoritative servers for example.com. A
    → ns1.example.com. (205.251.196.1)  8ms
    ← ANSWER: www.example.com. A 93.184.216.34  TTL 3600

Each hop shows which server was queried, its IP, round-trip time, and exactly what was returned. Delegation loops, missing glue records, and lame delegations all become immediately visible.

3. Resolver Disagreements Are Hard to Diagnose

Split-horizon DNS, stale caches, and regional differences mean two resolvers can return completely different answers for the same query. Figuring out why usually involves running dig @8.8.8.8 and dig @1.1.1.1 separately, then comparing the output by hand.

shohei’s --compare mode queries both resolvers concurrently and shows a unified diff:

Comparing 8.8.8.8 vs 1.1.1.1 for example.com A

  ANSWER
  8.8.8.8:   93.184.216.34  TTL 3521
  1.1.1.1:   93.184.216.34  TTL 120

  AUTHORITY
+ 8.8.8.8:   example.com. NS ns1.example.com.  TTL 172800
- 1.1.1.1:   (none)

  FLAGS
  8.8.8.8:   QR AA TC RD RA AD  ← AD set (DNSSEC validated)
  1.1.1.1:   QR AA TC RD RA     ← AD not set

The TTL difference here tells you 1.1.1.1 has a nearly-fresh cache entry while 8.8.8.8 is serving a stale one. The missing AD flag on 1.1.1.1 is immediately visible.

Feature Overview

DNSSEC Chain Visualization

Full trust chain from root to record, color-coded by validation status. Green for valid, red for failures, yellow for warnings (e.g., signature expiring within 7 days).

Iterative Resolution Tracing (--trace)

Manual hop-by-hop resolution with timing, referral details, and glue record display. Shows exactly what a recursive resolver would do.

DoH and DoT Support

Modern DNS transports with no external dependencies:

shohei example.com --doh https://cloudflare-dns.com/dns-query
shohei example.com --dot 1.1.1.1

Useful for testing whether a domain resolves correctly over encrypted transports, or for bypassing a local resolver that might be interfering.

Resolver Comparison (--compare)

Side-by-side diff of two resolvers. Flags TTL differences, missing records, DNSSEC validation status mismatches, and flag differences:

shohei example.com --compare 8.8.8.8 1.1.1.1

Multi-Type Queries

Query multiple record types in a single command instead of running dig four times:

shohei example.com A AAAA MX TXT SOA

Output is grouped by type with consistent formatting.

Watch Mode (--watch)

Auto-refresh at a set interval. Useful for monitoring TTL countdown, propagation in real time, or catching flapping records:

shohei example.com --watch 5  # refresh every 5 seconds

The display updates in-place, so changes are immediately visible without scrolling.

Interactive TUI (--tui)

Built with ratatui, the TUI gives you three navigable panels in a single terminal window:

  • Records panel — all RRsets for the queried domain, sorted by type
  • DNSSEC panel — the full trust chain with validation status
  • Trace panel — iterative resolution hops with timing

Switch between panels with Tab, scroll with arrow keys, and exit with q. Useful when you need to explore a domain’s DNS health interactively rather than in a single pass.

Reverse DNS (--ptr)

PTR lookups for IPv4 and IPv6, with automatic in-addr.arpa / ip6.arpa formatting:

shohei --ptr 8.8.8.8
shohei --ptr 2001:4860:4860::8888

Batch Mode & Script-Friendly Output

Pipe a list of domains for bulk querying, with JSON output for downstream processing:

cat domains.txt | shohei --batch --json | jq '.[] | select(.dnssec_valid == false)'

The --minimal flag strips color and tree formatting for plain-text pipelines.

Why Rust?

Three reasons:

Performance. DNS diagnostics involve parallel resolution paths — querying multiple nameservers concurrently, running resolver comparisons simultaneously, tracing iterative hops while fetching DNSSEC records in parallel. Rust’s async ecosystem via tokio handles this efficiently without GC pauses that would skew timing measurements.

Correctness. DNSSEC validation is stateful and involves cryptographic operations across multiple record types. Getting it wrong silently is worse than getting it wrong loudly. Rust’s type system lets you encode validation state as types — a validated chain is a different type than an unvalidated one, and the compiler enforces that distinction.

hickory-dns. The hickory-dns crate (formerly trust-dns) is a mature pure-Rust DNS implementation with first-class DNSSEC, DoH, and DoT support. It handles wire-format parsing, cryptographic validation, and transport negotiation — the parts that are easy to get subtly wrong — so I could focus on the UX layer.

Core Dependencies

Crate Purpose
hickory-dns DNS resolution, DNSSEC validation, DoH/DoT transport
clap CLI argument parsing with derive macros
ratatui Interactive TUI framework
owo-colors Terminal colorization without ANSI escape string building
comfy-table Table rendering with Unicode box-drawing
tokio Async runtime for concurrent resolver queries
serde_json JSON output serialization

Installation

cargo install shohei

Or build from source:

git clone https://github.com/kent-tokyo/shohei
cd shohei
cargo build --release
./target/release/shohei example.com

The TUI is included by default. If you want a smaller binary without ratatui, build with --no-default-features.

Quick Reference

# Basic query — records + DNSSEC chain
shohei example.com

# Multiple record types in one shot
shohei example.com A AAAA MX TXT SOA

# Iterative resolution trace (hop by hop)
shohei example.com --trace

# DNSSEC chain only
shohei example.com --dnssec

# Compare two resolvers
shohei example.com --compare 8.8.8.8 1.1.1.1

# Query over DoH
shohei example.com --doh https://cloudflare-dns.com/dns-query

# Query over DoT
shohei example.com --dot 1.1.1.1

# Watch mode (refresh every 5s)
shohei example.com --watch 5

# Reverse DNS
shohei --ptr 8.8.8.8

# Interactive TUI
shohei example.com --tui

# Batch mode with JSON output
cat domains.txt | shohei --batch --json

# Minimal output for scripting
shohei example.com --minimal

What’s Next

  • DANE/TLSA validation — cross-reference TLS certificate fingerprints against DNS to verify DANE configurations
  • NSEC/NSEC3 zone walking — for security research and CTF scenarios
  • HTML report export — a shareable snapshot of a domain’s full DNS health, including DNSSEC chain and trace output
  • CAA record checking — verify Certification Authority Authorization records alongside DNSSEC

Feedback Welcome

shohei is MIT licensed and open to contributions. If you work with DNS professionally — or just find yourself reaching for dig more than you’d like — I’d love to hear what features would be most useful.

github.com/kent-tokyo/shohei

Built with Rust, a healthy obsession with DNSSEC, and too many late nights staring at zone files.

# Mastering Spring Security: Architecture, JWT Patterns, and Production Gotchas

If you have ever integrated Spring Security into an enterprise application, you know it feels like magic—until a random 401 Unauthorized or 403 Forbidden breaks your production build.

To build secure, predictable APIs, you have to look past the boilerplate annotations and understand how Spring Security coordinates filters, manages authentication contexts, and delegates authorization. Let’s break down how the internal engine works, look at optimal JWT strategies for microservices, and tackle common interview and production questions.

1. The Core Architecture Blueprint

Every request entering a secure Spring Boot application journeys through a layered ecosystem before it ever hits your @RestController.

The Request Lifecycle Pipeline

Request ➔ DelegatingFilterProxy ➔ SecurityFilterChain (Authentication Filter) 
        ➔ AuthenticationManager ➔ AuthenticationProvider ➔ UserDetailsService 
        ➔ SecurityContextHolder ➔ AuthorizationFilter ➔ Your Controller

  1. The Gateway (DelegatingFilterProxy): Servlet containers don’t natively know about Spring beans. This proxy bridges the gap, handing the request over to Spring’s managed SecurityFilterChain.
  2. The Interceptor (AuthenticationFilter): Filters extract raw credentials. For instance, a BasicAuthenticationFilter looks for a header starting with Basic, decodes the Base64 payload, and extracts the username and password.
  3. The Tokenization: Once extracted, the filter wraps these raw credentials into an unauthenticated token object, such as a UsernamePasswordAuthenticationToken.
  4. The Coordinator (AuthenticationManager): The manager (typically implemented as ProviderManager) holds a registry of available AuthenticationProvider instances. It loops through them, matching a provider via its .supports(Class<?> authentication) method.
// Simplified look under the hood of ProviderManager
for (AuthenticationProvider provider : getProviders()) {
   if (!provider.supports(toTest)) {
       continue;
   }
   try {
       result = provider.authenticate(authentication);
       if (result != null) {
           copyDetails(authentication, result);
           break; // Authentication succeeded!
       }
   } catch (AuthenticationException e) {
       // Handled appropriately or thrown down the line
   }
}

  1. The Resolver (AuthenticationProvider & UserDetailsService): The provider calls your database or identity provider (via UserDetailsService), matches the passwords, and returns a fully populated, authenticated Authentication object.

2. Authentication Outcomes: Success vs. Failure

What happens right after the AuthenticationManager completes its evaluation?

On Success: Populating ThreadLocal

The engine constructs a Principal object containing user information and roles, then injects it directly into the SecurityContextHolder. By default, this uses a ThreadLocal strategy.

Architectural Win: Because it is stored in a ThreadLocal wrapper, the active user’s context stays accessible across any controller, service, or repository executed within that specific request thread—without forcing you to pass user objects as method arguments.

On Failure: The Exception Translation Layer

If credentials mismatch, an exception is thrown. The AuthenticationManager bubbles this back to the filters. This is where the ExceptionTranslationFilter shines: it intercepts the security exceptions and translates them into appropriate HTTP responses (like a 401 Unauthorized challenge or custom JSON payload) via the configured AuthenticationEntryPoint.

3. Stateful vs. Stateless Context Management

How your application remembers authenticated users depends heavily on your architecture layout:

Aspect Stateful Session Stateless (JWT / Basic Auth)
Storage Location Server memory (HttpSession) Client-side Token
Re-Identification Tracks requests using a JSESSIONID cookie Inspects incoming token header on every request
Filter Handling SecurityContextHolderFilter extracts context from the session Token filter builds context completely fresh per request
Thread Strategy Cleared out and reloaded from session store Cleared completely at the end of the thread lifecycle

4. The JWT Pattern for Distributed Microservices

When implementing stateless JSON Web Tokens, you will usually extend OncePerRequestFilter. This ensures your validation logic triggers exactly once per request lifecycle, bypassing edge cases where internal servlet forwards or redirects double-trigger a regular filter.

Incoming Request ➔ JWT Filter ➔ .parseClaimsJws(token) [Validates Signature & Expiry] ➔ Set SecurityContext ➔ Chain Continues

Propagating JWT Tokens Downstream

In a microservices ecosystem, you have two primary options when Service A needs to call Service B:

  1. Token Propagation (Impersonation): Forward the original client token received by your controller to downstream services.
  2. Service-to-Service Identity: Service A obtains its own dedicated machine-to-machine JWT from the Authorization Server (cached until expiry) and attaches it to calls bound for Service B.

If you are using the Token Propagation approach, you don’t need to rebuild the string payload manually from user fields. Spring Security caches the raw token value inside the security context:

// Extracting the original token inside a RestTemplate/WebClient Interceptor
String token = ((Jwt) SecurityContextHolder.getContext()
       .getAuthentication().getPrincipal())
       .getTokenValue();

// Attach 'token' to the Authorization Bearer header for outbound calls

5. Method-Level Security & Role Hierarchies

By dropping @EnableMethodSecurity on a configuration class, Spring creates runtime proxies around your beans, using Aspect-Oriented Programming (AOP) to evaluate security rules before or after a method fires.

Setting up a Clean Role Hierarchy

Instead of cluttering your code with complex expressions like @PreAuthorize("hasAnyRole('SALESREP', 'ADMIN')"), clean it up by declaring a logical hierarchy bean. This ensures an ADMIN implicitly inherits all capabilities belonging to a SALESREP.

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("ROLE_ADMIN > ROLE_SALESREP");
    return hierarchy;
}

Overriding Hierarchy Defaults & Custom Beans

If you run into an isolated edgecase where an endpoint must be accessed by a SALESREP but explicitly blocked for an ADMIN, combine logical operators or delegate to a custom Spring component:

// Inline evaluation override
@PreAuthorize("hasRole('SALESREP') and !hasRole('ADMIN')")

// Delegation to a specialized security service
@PreAuthorize("@securityService.canAccessLead(authentication)")

When to leverage @PostAuthorize

While @PreAuthorize prevents a method from starting if criteria aren’t met, @PostAuthorize allows a method to completely execute, evaluates the returned entity, and throws an Access Denied exception if conditions fail. This is ideal for domain-driven permissions:

@PostAuthorize("returnObject.isPublic() or returnObject.getOwner() == authentication.name")
public Document findDocumentById(Long id) {
    return documentRepository.findById(id); 
}

6. Real-World Production Deep Dive (Q&A)

Q: If a security chain features multiple filters, can we skip remaining filters once one successfully authenticates the request?

No, but also Yes. The structure of a FilterChain is static; once built, the request must physical pass through every node in that sequence to reach the servlet. However, you can skip the heavy internal execution logic inside your custom filters by wrapping your functional logic in a conditional check (e.g., executing only if SecurityContextHolder.getContext().getAuthentication() == null). If it is already authenticated, the filter can immediately invoke filterChain.doFilter(request, response) and return early.

Q: How do we handle Blacklisting/Logouts for stateless JWTs?

Because JWTs are self-contained and stateless, they remain valid until their expiration date, even if a user logs out.

  • The Scalable Approach: Keep token life-cycles highly restricted (e.g., 15 minutes) to minimize windows of vulnerability.
  • The Strict Approach: Upon logout, record the token signature into a fast, centralized key-value data cache like Redis with a Time-To-Live (TTL) tracking its remaining validity duration. On subsequent filter evaluations, query Redis. If the token matches the blacklist, halt the request execution.

Q: What if a JWT token expires right in the middle of downstream processing?

Spring Security introduces a default clock skew tolerance configuration (typically 60 seconds) to account for slight server time variances across instances. To survive unexpected mid-flight expiration scenarios across deeper, time-consuming service calls, ensure downstream services implement sensible internal network timeout/tolerance settings or use API gateways that renew downstream-bound tokens automatically.

Q: Why are users experiencing random 401 Unauthorized or 403 Forbidden errors?

  • Clock Drift: If your distributed cluster nodes drift out of sync temporally, a token minted on Server A might be treated as prematurely expired or not-yet-valid by Server B. Ensure all cluster nodes use Network Time Protocol (NTP) daemons to keep system clocks tightly aligned.
  • Configuration Conflicts: If a URL path pattern is protected via your SecurityFilterChain settings (e.g., requiring .hasRole("ADMIN")) but the underlying method controller is annotated with an explicit @PreAuthorize("hasRole('USER')"), both criteria must pass. A configuration mismatch here results in a frustrating 403 Forbidden response.
  • Token Refresh Failures: Front-end clients missing precise execution loops for silent token-refresh operations often send expired signatures, causing unexpected 401 Unauthorized errors during normal user flows.

I Built a Complete Japanese Study App in One Day — Without Writing a Single Line of Code

main input

One paste. Five study tools. Zero setup. Here’s how I built NihongoSpark for the MeDo Hackathon.

What I Built

NihongoSpark is a web app that takes any Japanese text — or even just an English description of a topic — and instantly generates five interconnected study tools: flashcards, a quiz, a mind map, a kanji breakdown, and a JLPT grammar pattern spotter.

The entire application was built using MeDo, Baidu’s no-code AI app builder, without writing a single line of code manually. From the first prompt to a deployed, publicly accessible URL took less than a day.

The Problem It Solves

Japanese learners constantly juggle multiple tools:

  • Anki for flashcards
  • Jisho for kanji lookups
  • Separate grammar reference apps
  • Quiz generators that don’t accept custom input

None of these tools talk to each other, and none let you feed in your own source text. Studying a news article or manga panel means manually creating materials from scratch — which nobody does.

NihongoSpark collapses that entire workflow into a single input box.

Key Features

⛩️ Kanji Breakdown (signature feature)

For every kanji found in the text:

  • Radical decomposition with meanings — e.g. 語 = 言 (speech) + 吾 (I)
  • On’yomi and kun’yomi readings with example words
  • JLPT level badge (N5 to N1)
  • AI-generated mnemonic — a short story linking radicals to meaning, so you actually remember it
  • Stroke count

This is what no generic flashcard app does. The mnemonic alone makes kanji memorable in a way rote repetition never does.

🧠 Mind Map

Interactive SVG mind map of the key concepts in the text. Nodes are draggable and clickable for definitions. Includes a full-screen expand modal for dense texts and a JP/EN toggle that instantly switches all node labels between Japanese and English.

🃏 Flashcards

Vocabulary cards with furigana and English meaning. 3D flip animation on click. Cards you mark “Review again” cycle back into the deck. Completion screen when all cards are mastered.

📝 Quiz

5 multiple-choice questions mixing three types:

  • Meaning — “What does 食べる mean?”
  • Reading — “How do you read 東京?”
  • Fill in the blank — “私は___が好きです。”

Includes a visible timer bar, instant feedback with explanation, and a score summary. The quiz only starts when you click “Start Quiz” — no auto-start surprise.

📐 Grammar Pattern Spotter

Scans the text for JLPT N5–N1 grammar patterns. Each pattern is:

  • Highlighted in the original text with color coding by level (N5 green → N1 red)
  • Listed below with its name, JLPT level, English explanation, and an example sentence

Example Interaction

Input:

東京は日本の首都で、世界で最も人口が多い都市の一つです。

Output across all five panels:

Panel Output
Kanji 東 = 木 (tree) + 日 (sun) → “The sun rising behind a tree — that’s east.” JLPT N4
Flashcards 首都 → Capital city · 人口 → Population · 都市 → City
Quiz Q: What does 首都 mean? A) Population B) Capital city ✓ C) Metropolis D) Country
Mind Map Tokyo → [Capital] → [World’s largest] · Tokyo → [Japan] → [Asia]
Grammar 〜の一つ (N4) “one of ~” · 〜で (N5) location particle

Demo & Screenshots

(Screenshots to be added — see the live app link below)

main input
overall
kanji breakdown
flashcard
mindmap
quiz
grammar

Live app: NihongoSpark on MeDo

Demo video: YouTube

How I Used MeDo — The Full Build Sequence

The entire app was built through MeDo’s chat interface in 8 prompts. No code editor, no terminal, no deployment config.

Prompt 1 — Initial app generation
Described the full app in plain English. MeDo generated a Requirement.md and offered a “Generate App” button. One click and the skeleton was live.

Prompt 2 — Flashcards panel
Specified flip animation, deck cycling behavior, and progress indicator. Functional on first try.

Prompt 3 — Quiz panel
Added three question types, a timer bar, and a score summary screen.

Prompt 4 — Mind Map panel
SVG-based interactive mind map with draggable nodes and click-to-define tooltips.

Prompt 5 — Kanji Breakdown panel
The core feature: radical decomposition, dual readings, JLPT badge, AI mnemonic, stroke count per character.

Prompt 6 — Grammar Pattern Spotter
Full-width fifth panel detecting N5–N1 patterns with color-coded highlights and per-pattern explanations.

Prompt 7 — Polish & UX fixes
Fixed textarea contrast (black on black bug), added “Start Quiz” button, built mind map expand modal with JP/EN toggle, reorganized panel layout to: Kanji + Mind Map / Flashcards + Quiz / Grammar full-width.

Prompt 8 — Deploy
One click. MeDo deployed to Baidu Cloud with a live public URL.

Total credits used: ~400

Lessons Learned

Prompt specificity is everything.
Vague prompts give vague results. The more you describe exact behavior, layout, and edge cases upfront, the fewer fix iterations you need — and each iteration costs credits.

One feature per prompt.
Trying to build two panels in one prompt leads to one being half-baked. Focused prompts gave much cleaner results every time.

Credit budget is real.
Each prompt costs ~15 credits. You get 300 free credits on signup + 100 per day. That’s roughly 25–30 prompts total. Plan your build sequence before you start — don’t discover the prompt order by trial and error.

MeDo handles full-stack surprisingly well.
Frontend, backend logic, and AI API calls were all wired together without any manual configuration. For a no-code tool, that’s genuinely impressive.

Bug fixes need precise symptom descriptions.
When the textarea had black text on black background, describing the exact visual symptom (not the suspected cause) gave a faster fix than speculating about CSS conflicts.

Find your differentiator early.
Generic flashcard apps already exist. The Kanji Breakdown with AI mnemonics is what makes NihongoSpark worth building and worth submitting. Define your unique feature first, then build everything else around it.

What’s Next

Features intentionally left out of scope for this release that would make strong v2 additions:

  • Spaced repetition algorithm (SM-2) for flashcard scheduling
  • Audio pronunciation via text-to-speech
  • Progress tracking and study history
  • Export to Anki-compatible CSV
  • JLPT mock exam mode using the grammar spotter data

Built with MeDo for the Build with MeDo Hackathon 2026 · #BuiltWithMeDo

2026 SERP API Industry Insights: How Developers Can Navigate the New Wave of Search Data

Introduction

In the realm of digital marketing and data analytics, Search Engine Results Page (SERP) data has always been crucial for understanding user behavior, optimizing content strategies, and monitoring competitors. However, with the rapid evolution of the search engine landscape, particularly the deep integration of Artificial Intelligence (AI), traditional methods of acquiring SERP data are facing unprecedented challenges. For developers, understanding and effectively utilizing SERP APIs is no longer optional but a prerequisite for building future intelligent applications and data-driven solutions. This article will delve into the latest trends, mainstream products, and their impact on developers in the 2026 SERP API industry, offering practical advice for choosing the right API.

Four Core Forces Shaping the 2026 SERP API Landscape

The SERP API market is undergoing a profound transformation driven by both technological advancements and market demands. Here are the four key factors influencing this domain:

  1. The Sunset of Google Custom Search JSON API

The Google Custom Search JSON API was once a convenient way for many developers to obtain SERP data. However, according to Google’s developer documentation
, the API stopped accepting new customers in 2025 and is scheduled to be fully discontinued on January 1, 2027. This change forces thousands of teams relying on this API to seek alternatives. Due to its inherent limitations (e.g., a maximum of 10 results per request, a daily cap of 10,000 queries, and no inclusion of ads or AI Overviews), it has long been insufficient for modern data needs. Therefore, migrating to a more powerful and contemporary SERP API has become an urgent priority.

  1. AI Overview Parsing Becomes Standard

The presence of AI Overviews (AIO) in SERPs has significantly increased. BrightEdge tracking indicates that AIOs are triggered in approximately 48% of tracked queries
. seoClarity data shows a 475% year-over-year growth in mobile AIOs between September 2024 and September 2025
. This implies that if a SERP API cannot parse and structurally return the generative content, cited sources, inline videos, and injected ads within an AIO, the data it provides will be partial and incomplete. Further research by Ahrefs points out that the overlap between pages cited in AIOs and those ranking in the organic top 10 has dropped from 76% to 38%
, indicating that ranking and AIO citations are now distinct signals. Developers need to track both to gain comprehensive search insights.

  1. Multi-Engine Coverage Becomes the New Norm

With the rise of AI search, users are no longer solely dependent on Google. AI search surfaces like ChatGPT, Perplexity, Gemini, and Copilot are rapidly gaining market share. For instance, Conductor’s 2026 enterprise benchmark report shows that ChatGPT drives 87.4% of AI referral traffic
. Different AI search surfaces exhibit unique citation behaviors and ranking signals. Therefore, a modern SERP API must be able to abstract Google, Bing, and these emerging AI search surfaces, providing parsed data through a unified JSON interface. APIs lacking multi-engine coverage will lead to data fragmentation, increasing integration and management costs for developers.

  1. True Cost Per Call Far Exceeds Stated Prices

After the deprecation of the n=100 parameter, the true cost of SERP API calls has become more complex. Stated prices are often misleading, and developers need to consider the following factors:

•
AI Overview Surcharge: Some APIs charge extra for enabling AIO parsing, while others include it
.

•
Pagination Multiplier: Each additional 10-result page may cost 75-100% of the base fee
. This means retrieving 100 results (n=100) could cost 5-9 times more than retrieving 10 results.

•
Monthly Minimums vs. Pay-Per-Call: Some APIs offer monthly plans with overage rates, while others adopt a purely pay-per-call model
.

•
Latency Tier Surcharge: Certain APIs provide different service tiers with varying speeds, where faster speeds incur higher costs
.

When choosing an API, developers must carefully calculate the true cost in a production environment, rather than just focusing on the advertised
“per thousand calls” price.

Overview of Mainstream SERP API Products

In the 2026 SERP API market, numerous excellent products have emerged, each with distinct focuses on features, pricing, and target users. Here’s an overview of some mainstream providers:

•
cloro: With its AI-native, cross-surface SERP intelligence as a core strength, cloro offers deep parsing of Google, ChatGPT, Perplexity, Gemini, Copilot, and other AI search surfaces, including AI Overview. Its pricing model is pay-per-call, primarily targeting SEO, AI search, and brand monitoring teams.

•
SerpApi: As a mature and stable product in the market, SerpApi is known for its broad engine support, covering over 80 search engines including Google, Bing, Baidu, Yandex, eBay, and YouTube, and provides reliable AI Overview parsing. It employs a monthly plan plus overage pricing model, suitable for developers and enterprises requiring broad data sources.

•
DataForSEO: Specializing in large-scale and comprehensive SEO data services, DataForSEO supports engines like Google, Bing, Yahoo, Baidu, and Yandex, and offers AI Overview parsing. Its pay-per-call (credit-based) model makes it an ideal choice for enterprises building SEO tools and those needing highly customized data.

•
Apify: As a web automation platform, Apify provides flexible scraping capabilities through its pre-built Google Search Actor, supporting AI Overview parsing via headless Chrome. Its pay-per-compute unit model attracts developers needing flexible scraping and automation capabilities.

•
SearchApi: This product strikes a good balance between multi-engine coverage and AI parsing, supporting Google, Bing, YouTube, ChatGPT, and Perplexity, and offering AI Overview parsing. Its monthly plan pricing model is suitable for developers seeking versatile and easy-to-use APIs.

•
Serper: Known for being lightweight and fast, Serper primarily supports Google and Bing, and offers partial AI Overview parsing. Its monthly plan pricing model is suitable for developers requiring speed and lightweight solutions.

In-depth Look at Selected Products:

•
cloro: As an AI-native solution, cloro focuses on providing parsed data for Google SERP, AI Overview, AI Mode, and multiple AI search surfaces like ChatGPT, Perplexity, Gemini, and Copilot
. Its unique selling point is the ability to return all this data through a single endpoint family, along with actual cited source URLs, which is crucial for teams needing to comprehensively track AI search citations and rankings.

•
SerpApi: A veteran player in the market, SerpApi is known for its extensive search engine coverage (supporting over 80 engines) and stable service
. It offers reliable AI Overview parsing, making it an ideal choice for developers who need structured SERP data from diverse sources.

•
DataForSEO: DataForSEO targets enterprise users, offering a comprehensive suite of SEO data APIs, including SERP data, keywords, and backlinks
. Its pay-per-call credit system and flexible query speed options make it a top choice for enterprises requiring large-scale, highly customized data. However, its complex documentation and multi-step integration process might be challenging for novice developers.

•
Apify: As a web automation platform, Apify provides SERP scraping services through its pre-built Google Search Actor
. It leverages headless browsers to handle CAPTCHAs and anti-bot mechanisms, ensuring data reliability. While potentially slightly slower than pure API solutions, its flexibility and powerful automation capabilities make it a strong tool for developers needing custom scraping logic and integration into broader automation workflows.

How to Choose the Right SERP API?

For developers, selecting an appropriate SERP API requires considering multiple factors:

1.
Data Requirements: What types of SERP data do you need? Just organic results, or also ads, local packs, knowledge panels, and most importantly, AI Overviews and multi-engine AI search results?

2.
Coverage: Which countries, languages, or devices (desktop/mobile) do you need to track search results for? Is support for search engines other than Google or AI search surfaces required?

3.
Real-time vs. Scale: How high are your real-time data requirements? Do you need instant results, or can queued processing be accepted? What is your expected query volume?

4.
Cost-Effectiveness: Carefully calculate the true cost in a production environment, including base fees, surcharges, pagination costs, and monthly minimums. Choose the pricing model that best matches your budget and usage patterns.

5.
Integration Difficulty & Developer Experience: Does the API provide clear documentation, SDKs, or example code? Is there active community support? Is the integration process complex?

6.
Reliability & Support: What is the API provider’s stability, SLA (Service Level Agreement), and customer support responsiveness?

Conclusion

The 2026 SERP API market is in an exciting period of transformation. The retirement of the Google Custom Search JSON API, the widespread adoption of AI Overviews, and the rise of multi-engine AI search collectively shape a more complex yet potentially richer ecosystem. For developers, this presents both challenges and opportunities. By deeply understanding these trends and carefully selecting the right SERP API based on project needs, developers can unlock unprecedented search data insights, laying a solid foundation for building the next generation of intelligent applications and data-driven products.

How I Built a Free Random Letter Generator — And Got 7,800+ Impressions in 8 Weeks with Zero Ad Spend

I’m not a full-time developer. I’m an SEO and WordPress specialist. So when I decided to build and launch a web tool from scratch, I knew I had to be smart about every decision — from the tech stack to how I’d get it in front of people.

This is the story of how I built randomletter.io — a free, fast, accessible random letter generator — and what I learned along the way.

Why I Built It

The idea was simple: I wanted to create a suite of small, useful generator tools under one brand. Random letter generator was Tool #1.

I looked at what was already out there. Most existing tools were cluttered, slow, or just plain ugly. I knew I could do better — and more importantly, I knew a clean, fast, accessible tool could rank well on Google with the right SEO foundation.

So I got to work.

The Stack

I kept it deliberately simple:

  • WordPress as the CMS (I know it well, it’s fast to deploy)
  • Kadence Theme — lightweight, performance-friendly
  • Custom Vanilla JS plugin for the actual tool logic
  • LiteSpeed Cache + Cloudflare for speed
  • Rank Math SEO for on-page optimization
  • Hostinger for hosting

No React. No npm. No build pipeline. Just clean, dependency-free Vanilla JS wrapped in a custom WordPress plugin.

Building the Tool Plugin

The core tool lives in a custom plugin at wp-content/plugins/. I built it entirely in Vanilla JS — no frameworks, no libraries.

Why Vanilla JS? A few reasons:

  1. Performance — no library overhead. Every millisecond matters for PageSpeed scores.
  2. Simplicity — fewer moving parts means fewer things to break.
  3. Control — I own every line of code.

The plugin handles letter generation, display modes (single letter, multiple letters), and user settings like excluding vowels or consonants. It enqueues its own CSS and JS cleanly, without touching the theme.

One early mistake: I accidentally enqueued Google Fonts twice — once in the theme and once in the plugin. Fixed it, and PageSpeed jumped immediately. Small things like that compound fast.

Accessibility from Day One

This was non-negotiable for me. A tool that teachers and students use needs to work for everyone.

I made sure:

  • All interactive elements have proper accessibility labels
  • Colour contrast meets accessibility standards (I had to fix --rlg-text-muted — the default was too light)
  • The tool is fully keyboard navigable
  • Screen reader announcements work correctly when a new letter generates

Result: 100/100 Accessibility score on PageSpeed Insights. Not a nice-to-have — a baseline.

The PageSpeed Obsession

I’ll be honest — I became a little obsessed with PageSpeed scores.

After a lot of tweaking — removing duplicate font loads, deferring non-critical CSS, optimising the LiteSpeed Cache configuration, and setting up Cloudflare properly — here’s where I landed:

Metric Mobile Desktop
Performance 98 100
Accessibility 100 100
Best Practices 100 100
SEO 100 100

A perfect or near-perfect score doesn’t guarantee rankings — but it removes a barrier. Google won’t penalise you for being fast.

SEO Strategy: Content Over Tricks

I didn’t do anything clever with SEO. I just did the basics well:

  • Targeted low-competition keywords
  • Wrote genuinely useful supporting content
  • Built clean internal linking
  • Made sure the site was fully crawlable

No black hat. No shortcuts. Just content that actually helps people.

The Results (8 Weeks In)

As of May 2026, with zero ad spend:

  • 7,870 impressions on Google Search Console
  • 85 clicks
  • Average position: 18.2 (and improving)
  • 7 pages indexed
  • Impressions in 145 countries — India, USA, UK, Japan, Canada, Philippines, and counting

Not viral numbers. But for an 8-week-old site with no backlinks budget, I’ll take it.

What’s Next

Continuing to grow organically — more content, more visibility, more tools under the same brand. Slow and steady.

No shortcuts. No rushing.

What I Learned

  1. Vanilla JS is underrated. You don’t need a framework for a focused, single-purpose tool.
  2. Accessibility and performance aren’t opposites. You can have both if you’re intentional from the start.
  3. WordPress gets a bad reputation it doesn’t always deserve. With the right stack, it’s genuinely fast.
  4. SEO is slow. That’s fine. Compound interest is slow too. Keep going.

If you’re thinking about building a small web tool — just start. Keep the scope small, the code clean, and the user experience tight.

You can check it out at randomletter.io — feedback always welcome.

TryHackMe Brains Challenge Walkthrough

So this is the Brains room on TryHackMe. Its a two part challenge, you hack into a box (red team) and then you investigate what happened (blue team). Pretty cool combo actually.

Here’s my video walkthrough if you wanna follow along:

Part 1: Hacking the Box

First thing, start the machine and grab the IP address. Then open a terminal and run nmap to see whats available:

nmap <target-ip>

You’ll see three things open:

  • Port 22 (SSH)
  • Port 80 (HTTP)
  • Port 50000 (something weird, looks like a database but its not)

If you go to port 80 in the browser its just a blank page, nothing there. You can run gobuster to discover hidden folders and files on it but trust me, waste of time, there’s nothing.

The interesting one is port 50000. Try connecting with telnet:

telnet <target-ip> 50000

Hit enter a few times and you’ll see it returns HTML. So its actually a web server not a database. Open it in Firefox:

<target-ip>:50000

You’ll see TeamCity running. Note the version number, in this case 2023.11.3.

Finding the Exploit

Go to exploit-db.com and search for “TeamCity”. You’ll find a bunch of vulnerabilities, the one we want is the authentication bypass / remote code execution one. Note the CVE number and search for it on GitHub.

I used the exploit from wolf hacker. Copy the code, save it as a .py file. When you run it you might get a missing module error:

pip install faker

Then run the exploit pointing at your target:

python exploit.py -t <target-ip>

If it works youll get a shell. Test it:

whoami

You should see something like “ubuntu”. Then check the home folder:

ls /home/ubuntu

Youll find a flag.txt, cat it out:

cat /home/ubuntu/flag.txt

Thats your first flag, machine hacked.

Privilege Escalation

While your in there check what you can run as sudo:

sudo -l

In this case everything is allowed which means you can just do:

sudo whoami

And youll see root. Thats it, full privilege escalation. Pretty easy one but good to practice the concept.

Part 2: Investigation with Splunk

Now close that machine and start the second one (the blue team box). Put the new IP in your browser and youll see the Splunk interface.

Go to Search and Reporting. Start with a broad search to see everything:

index=*

Set the time to “All Time” and search. Youll see around 4000 events.

Question 1: What plugin was installed after exploitation?

Just search:

index=* plugin

Youll see an event about a plugin being uploaded. The full name is right there in the log, copy it.

Question 2: What malicious package was installed?

Search for:

dpkg

Then filter to look through installed packages. Youll see loads of normal Linux stuff, lib this, lib that. Keep going through the pages and youll spot one called “data-collector”. Thats the dodgy one, nothing legit should be called that.

Question 3: What is the backdoor user?

Search for:

useradd

Youll find an entry where useradd was run with the username “evil-user” and a home folder to match. Yeah not subtle lol. Thats your answer.

Done

Thats the full Brains room. Red team side you exploited a TeamCity auth bypass to get a shell and escalated to root. Blue team side you used Splunk to find the plugin, the malicious package and the backdoor user that the attacker left behind.

If you want to practice the Linux commands used in this walkthrough check out https://practicelinux.com

Should You Still Learn Coding in the Age of AI? The Question Every Developer Is Quietly Asking

The end is coming meme

A few years ago, the roadmap felt clear.

Learn programming.
Build projects.
Practice algorithms.
Get hired.
Build a stable career.

That promise brought an entire generation into tech.

People stayed up until 2:00 a.m. debugging errors they barely understood. They watched the same tutorial three times because something just refused to click. They spent weekends building portfolio projects nobody asked for, hoping one day somebody would finally notice.

The end is coming meme

And honestly? For a while, the promise felt real.

Software engineering became one of the most recommended careers on the internet. Every platform repeated the same message:

“Learn to code. Your future self will thank you.”

So people listened.

They got computer science degrees.
They joined bootcamps.
They solved hundreds of LeetCode problems after work or school.
They sent hundreds of resumes into application portals that never responded.

send job applications meme

And now…

The same people are opening LinkedIn every morning to another headline about AI replacing engineers, companies freezing hiring, or thousands of developers getting laid off.

At some point, almost every developer has quietly asked themselves the same question:

Was all of this even worth it?

The Fear Around AI Feels Very Real

We should stop pretending people are overreacting.

The anxiety in the tech industry right now is real.

You see someone open an AI coding assistant, describe an app in plain English, and suddenly a working prototype appears in minutes.

keyboard typing meme

A few years ago, building that same thing might have taken days.

That changes how people think about software engineering.

It especially hits beginners hard.

Because when you see AI generating code instantly, it becomes easy to wonder whether all those years spent learning syntax, debugging, architecture, and frameworks are slowly becoming irrelevant.

And honestly, I understand why so many people feel discouraged.

The industry itself isn’t helping.

Every week, another company announces “AI-first restructuring” like it’s some futuristic badge of honor. Investors applaud. Executives write optimistic posts about productivity.

But behind those announcements are real developers trying to figure out what happened to the career path they were told was safe.

And here’s the part nobody says loudly enough:

A lot of these layoffs are not purely caused by AI.

Many companies massively overhired during the pandemic. Money was cheap, growth expectations were unrealistic, and engineering teams expanded faster than they probably should have.

Now the market changed.

So instead of saying:

“We made bad hiring decisions.”

…it sounds much better to say:

“We are restructuring around AI innovation.”

AI became part strategy, part narrative, and part shield for decisions companies were already heading toward.

That doesn’t make the fear less painful for developers. But it does change the conversation.

The Problem With “Vibe Coding”

There’s another topic that keeps coming up lately: vibe coding.

And to be fair, some of it is genuinely impressive.

People with little technical experience can now build surprisingly useful things using tools like AI coding assistants, no-code platforms, and prompt-based workflows.

Vibe coding meme

That speed is real.

But there’s also something dangerous hidden underneath the excitement.

When someone doesn’t truly understand the code they generated, they also don’t understand when the code is failing.

And software rarely breaks at the perfect moment.

It breaks at 2:13 a.m. in production.

It breaks when users are losing data.

It breaks when systems behave differently under real traffic.

It breaks when edge cases appear that nobody thought about during the demo.

That’s where experience matters.

Because the hardest part of engineering was never just typing code into a file. The hard part is understanding systems deeply enough to debug them when reality stops matching expectations.

AI can accelerate development.

But acceleration without understanding creates a different kind of problem.

And eventually, companies will run into that reality.

Companies Might Be Creating a Bigger Problem

One thing that genuinely worries me is how many companies are slowing down junior hiring.

Every senior engineer people admire today was once a confused beginner.

They made mistakes in low-risk environments.
They asked bad questions.
They broke things.
They got mentored.
They slowly learned how real systems work.

That process takes years.

You cannot skip it with prompts.

If companies stop investing in junior developers because AI looks cheaper in the short term, they may create a massive experience gap later.

Because senior engineers don’t magically appear out of nowhere.

Experience need loop meme

The industry still needs people who understand infrastructure, debugging, scalability, architecture, reliability, security, and long-term system design.

Those skills are built through experience, not generated instantly.

And I think some companies are going to realize that much later than they should.

So… Should You Still Keep Coding?

so should you keep coding

I think the answer depends on why you started in the first place.

If coding was only about chasing salaries, then yes, this moment probably feels terrifying.

But for a lot of people, that wasn’t the real reason.

Most developers remember a specific moment when programming suddenly became exciting.

Maybe it was a tiny Python script that finally worked.

Maybe it was a personal website you proudly showed your family.

Maybe it was automating something annoying and realizing:

“Wait… I can actually build things.”

That feeling matters more than people admit.

Because programming changes the way you think.

You learn how to approach overwhelming problems calmly.
You learn how to debug confusion instead of panicking inside it.
You learn how to break impossible-looking systems into smaller solvable pieces.

Those skills do not disappear because AI exists.

In fact, they become even more valuable.

Because the people who will thrive in the AI era are probably not the people who memorize syntax the fastest.

They’re the people who understand systems, context, tradeoffs, and problem-solving deeply enough to guide the tools correctly.

AI changes the workflow.

It does not eliminate the need for thinking.

The Future of Software Engineering Probably Looks Different

Here's what I'm thinking, meme

I do think software engineering is changing permanently.

Junior roles may evolve.
Interview expectations may shift.
The way we build products is already changing rapidly.

But I don’t think this means coding is dead.

I think it means shallow knowledge is becoming less valuable while deep understanding becomes more important.

The developers who survive long-term probably won’t be the ones competing with AI.

They’ll be the ones learning how to work with it while still understanding what’s happening underneath.

And honestly?

That has always been true in tech.

Every major shift changed the tools.
The internet changed development.
Cloud platforms changed development.
Open source changed development.
Frameworks changed development.

Now AI is changing development too.

But the people who kept learning usually adapted.

Maybe This Is the Real Skill

Maybe programming was never really about memorizing languages.

Maybe the real skill was learning how to stay curious when things stop making sense.

Learning how to sit with frustration long enough to solve something difficult.

Learning how to think clearly when systems become messy.

Fire meme

That mindset still matters.

Probably more than ever.

Thanks for reading! 🙏🏻
I hope you found this useful ✅
Please react and follow for more 😍
Made with 💙 by Hadil Ben Abdallah
LinkedIn GitHub Twitter

I Built a Real-Time Zombie Outbreak Simulator Using Uber’s H3 Spatial Index and Web Workers

There are two kinds of developers in this world:

1.Those who build practical, enterprise-grade, high-value CRUD applications.
2.Those who spend their weekends writing heavily optimized mathematical simulations calculating exactly how fast an army of fast-moving zombies would turn the city of London into an absolute buffet.

I fall squarely into the second camp.

I wanted to build a web-based, interactive zombie apocalypse simulator capable of handling thousands of distinct geographical zones in real-time at 60 FPS. No abstractions, no random generic circles expanding on a flat canvas—I wanted real-world city structures, actual road networks, weather vectors (wind directions), population densities, and dynamic 3D building reveals. Here is an engineering breakdown of how I combined Uber’s H3 Hexagonal Hierarchical Spatial Index, a custom SIZD mathematical epidemiological model, Web Workers for concurrent crunching, and an A road-network search algorithm* to build a highly responsive apocalypse sandbox.

The Core System Architecture: Separation of Church and Slaughter

If you try to run complex mathematical simulations for thousands of map coordinates directly on the browser’s main UI thread, your application will freeze up faster than a civilian cornered by a zombie horde.To keep the UI running at a buttery-smooth 60 FPS, the app splits responsibilities cleanly between two distinct architecture layers:

┌────────────────────────────────────────────────────────┐
│               MAIN UI THREAD (Angular)                 │
│  - MapLibre GL Layer rendering & 3D Extrusions         │
│  - RequestAnimationFrame loops for particle tracers    │
├────────────────────────────────────────────────────────┤
                            │  ▲
        Worker Messages:    │  │ Worker Responses:
        { type: 'TICK' }    │  │ { type: 'TICK_RESULT', payload: ... }
                            ▼  │
┌────────────────────────────────────────────────────────┐
│            BACKGROUND WEB WORKER (TS)                  │
│  - Seeded Mulberry32 Pseudo-Random Number Generator    │
│  - Cellular H3 Neighborhood Math (gridDisk, gridRing)   │
│  - SIZD Differential Equations for cell metrics        │
└────────────────────────────────────────────────────────┘

The background Web Worker manages the complete state of the simulation grid inside a lightweight Map layout (let grid: Map<string, SimCell>), processes data slices incrementally, and posts only lightweight updates back to the UI thread.

Deep-Dive Code Breakdown

Let’s tear open the chest cavity of the code and analyze the exact algorithms and mathematics driving this apocalypse.
1. The Math Under the Hood: The SIZD Model

Traditional pandemic tracking models use the classic SIR Model (Susceptible, Infected, Recovered). But since zombies don’t exactly “recover,” we had to upgrade this to a custom SIZD Model inside calculations-worker.ts:

-Susceptible (Healthy civilians)

-Infected (Bitten, incubating, panicking)

-Zombie (Reanimated, hungry)

-Dead (Fully neutralized or consumed)

Every single cell on the map runs differential equation steps on every clock tick. Here is how the worker evaluates state transitions per cell:

const VARIANT_PARAMS: Record<ZombieVariant, VariantParams> = {
  standard: {
    gamma: 0.16, delta: 0.025, mu: 0.05, betaMult: 1.0,
    seedZombiePct: 0.06, seedInfectedPct: 0.12,
    pzZombiePct:   0.20, pzInfectedPct:   0.10,
  },
  fast: {
    gamma: 0.26, delta: 0.010, mu: 0.08, betaMult: 1.3,
    seedZombiePct: 0.08, seedInfectedPct: 0.15,
    pzZombiePct:   0.25, pzInfectedPct:   0.12,
  },
  horde: {
    gamma: 0.32, delta: 0.006, mu: 0.10, betaMult: 1.7,
    seedZombiePct: 0.14, seedInfectedPct: 0.22,
    pzZombiePct:   0.30, pzInfectedPct:   0.15,
  },
};

gamma dictates the speed at which an Infected person turns into an active Zombie. Notice that in the horde configuration, conversion rates double (0.32), giving civilians almost zero time to get their affairs in order.
delta simulates humans fighting back. It is a zombie kill-rate coefficient. In horde mode, the sheer mass of numbers drops human defense efficacy down to a despair-inducing 0.006.

2. The Spatial Index: Uber’s H3 Hex Grid

Using rectangular latitude/longitude blocks for mapping introduces severe distortion near the poles, and calculating distances using spherical trigonometry over thousands of points is incredibly slow. Instead, the simulation wraps cities into an elegant hexagonal grid using Uber’s H3 spatial index.
Why hexagons?
Because unlike squares, every neighbor of a hexagonal cell shares an identical distance from its center point. This makes cellular automata wave propagation beautiful and simple.
Inside the worker loop, we handle the spread to neighboring areas using h3.gridDisk:

const ringSize = VARIANT_RING[variant];
const neighborIds = h3.gridDisk(cellId, ringSize).filter((id) => id !== cellId);

for (const neighborId of neighborIds) {
  const neighbor = grid.get(neighborId);
  if (!neighbor || neighbor.status !== 'clean') continue;
  if (neighbor.population <= 0) continue;

  const p = spreadProbability(cell, neighbor) * maturityFactor;

  if (rng() < p) {
    infectCell(neighbor, 0.005);
    dirtyCellIds.add(neighborId);
    spreadEvents.push({ fromCenter: cell.center, toCenter: neighbor.center });
  }
}

The Maturity Gate & The Wind Matrix

To prevent the virus from looking like an unnatural, perfectly growing circle, I implemented two neat modifiers:

  1. Maturity Gate: Freshly infected cells have incubated the virus for fewer ticks, meaning they spread sluggishly. As a cell matures (maturity = Math.min(1, ticksInfected / 4)), its viral transmission probability maximizes (1.0), creating realistic pulsing waves of infection instead of a robotic uniform march.

  2. Deterministic Wind: When initializing the simulation, a pseudorandom direction vector (windAngle and windStrength) is calculated. The spread probability math incorporates this vector, pushing the outbreak along specific prevailing paths (e.g., chasing weather dynamics or structural layouts).

3. Simulating Chaos: Panic Flight Long Jumps

Real people do not sit quietly waiting for their hex cell to be overrun. They climb into cars, hit highways, and flee. This human behavior is modeled using Panic-Flight Long Jumps:

if (cell.status === 'overrun' && maturity >= 1 && hasTransit(cell)) {
  if (rng() < LONG_JUMP_BASE_P) {
    const jumpRing = 3 + Math.floor(rng() * 3); // Ring 3 to 5
    const candidates = h3.gridRing(cellId, jumpRing);
    if (candidates.length > 0) {
      const target = candidates[Math.floor(rng() * candidates.length)];
      const tgt = grid.get(target);
      if (tgt && tgt.status === 'clean' && tgt.population > 0 && hasTransit(tgt)) {
        infectCell(tgt, 0.003);
        dirtyCellIds.add(target);
        spreadEvents.push({ fromCenter: cell.center, toCenter: tgt.center });
      }
    }
  }
}

If an infected hex reaches maximum maturity and sits adjacent to major transportation lines (hasTransit(cell)), it triggers a jump. The engine leaps over intermediate rings entirely using h3.gridRing(cellId, jumpRing), instantly creating isolated, non-contiguous cluster breakouts miles away from the primary containment zone.

4. Determinism: Mulberry32 Seeded PRNG

If a user shares a link to a simulation run, the recipient should witness the exact same chaotic sequence of events. Native Math.random() cannot handle this because it’s completely non-deterministic. To solve this, the simulation implements a custom Mulberry32 pseudorandom generator inside the background thread:

createRng(seed: number): () => number {
  let s = seed;
  return () => {
    s |= 0;
    s = (s + 0x6d2b79f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

By passing down an explicit simulation seed integer, this fast, bitwise generator returns identical floating-point sequences every single time, giving us total reproducible control over the chaos.

5. Client Thread Optimization: Micro-routing with A* & Heaps

When an infection spreads from one cell to another, the UI draws a particle stream representing the civilian flight line. Drawing straight lines over real geometry looks terrible. Instead, map.ts intercepts these spread events and routes them along the city’s actual OpenStreetMap road network.To ensure the mapping stays completely performant, it uses an optimized A Routing Strategy* paired with a custom low-overhead Binary Min-Heap:

class MinHeap {
  private d: { id: number; f: number }[] = [];
  get size() { return this.d.length; }
  push(x: { id: number; f: number }) { /* O(log n) tree balancing bubble up */ }
  pop(): { id: number; f: number } | undefined { /* O(log n) bubble down */ }
}

Instead of looping blindly through thousands of vertices across the whole city to locate the nearest intersection node, the application maps the node array indices into uniform coarse spatial coordinate boxes (nodeGrid) via a hashing utility:

function gk(lng: number, lat: number, s: number): string {
  return `${Math.floor(lng / s)},${Math.floor(lat / s)}`;
}

This allows the lookup engine to query a constrained localized $5times5$ tile region grid during its search (for (let dx = -2; dx <= 2; dx++)), shrinking node localization from a sluggish O(N) operation down to an instantaneous O(1) lookup.
Once localized, the A* paths are handed to MapLibre GL for rendering. If the distance exceeds an absolute boundary, the engine gracefully fallbacks to linear tracking, preventing long-distance jumps from eating valuable clock cycles.

6. Visual Trickery: 3D Building Extrusion Reveals

Uploading thousands of complete complex geometry features to GPU buffers on every frame causes noticeable stuttering. To avoid this, map.ts utilizes feature state binding.
When a city is loaded, every building is indexed into its respective H3 grid index cell using h3.latLngToCell at resolution level 9 (BUILDING_RES = 9).

dirtyCells.forEach((cell) => {
  const indices = this.buildingCellIndex.get(cell.cellId);
  if (!indices) return;
  const next = cell.status === 'overrun' ? 'overrun' : cell.status === 'infected' ? 'infected' : 'clean';
  indices.forEach((idx) => {
    this.map.setFeatureState({ source: 'city-buildings', id: idx }, { revealStatus: next });
  });
});

By leveraging generateId: true on the GeoJSON layout, MapLibre assigns constant numerical IDs tracking index entries. When a hex vector updates, the system fires targeted setFeatureState updates only for the structural elements linked to that specific altered grid boundary.
MapLibre GL translates these state changes via data-driven styling expressions directly inside the GPU paint pipeline:

'fill-extrusion-height': [
  'match',
  ['coalesce', ['feature-state', 'revealStatus'], 'clean'],
  'overrun', 90,
  'infected', 45,
  3
]

As a result, buildings dynamically “grow” from a baseline footprint height of 3 meters up to an ominous 90-meter crimson spire when completely overrun—handled entirely via high-speed GPU interpolation transitions, with zero data-reupload overhead!

What I Learned About High-Performance JavaScript

Avoid JSON serialization in loops: Do not pass thousands of deep objects over postMessage every 16ms. The background thread filters down to dirty cells only and caps visual spread tracers to small batches per tick.

GPU expressions are magic: Keeping your JS logic small and letting MapLibre’s underlying shaders handle transitions (fill-extrusion-color-transition) keeps performance exceptionally light.

Hexagons rule the world: Square bounding coordinates make math tedious. Hex grids turn adjacent neighborhood discovery into a fast, fixed array query.

Try out the project here: Zombie-Outbreak

Please leave a star on the github repo if you liked what you saw.
Thanks.