95+ browser-based dev tools that never touch a server

Every developer has a folder of bookmarks for one-off tools —
a JWT decoder here, a Base64 encoder there, a cron expression
tester somewhere else. Half of them send your data to a server
you know nothing about.

I built Wizbit.to to fix that.

What it is

95+ free developer utilities, all running 100% in your browser.
No server ever sees what you paste. No account. No ads. No limits.

What’s inside

Data & encoding

  • JSON formatter, beautifier, minifier & validator
  • Base64 encoder/decoder
  • CSV → JSON, CSV → Markdown, YAML ↔ JSON converters
  • ENV file parser and ENV → JSON/YAML converter

Security & crypto

  • JWT decoder with expiry checker and claims differ
  • Hash generator (SHA-256, SHA-512)
  • RSA key pair generator & X.509 certificate inspector
  • BIP39 seed phrase generator
  • Password generator & strength checker

Developer utilities

  • Regex tester with live match highlighting
  • Cron expression builder and syntax decoder
  • SQL formatter
  • Webhook visualizer and Stripe webhook simulator
  • UUID/GUID generator (v4, v5, v7, bulk)
  • HTTP status explorer
  • CIDR subnet calculator
  • Semver comparator

Frontend & CSS

  • SVG → JSX converter and SVG cleaner
  • CSS shadow, clip-path, and glassmorphism generators
  • Color converter (HEX, RGB, HSL)
  • Bezier curve and spring physics playground

AI & LLM tools

  • Prompt tokenizer and token calculator
  • Context trimmer and prompt compressor
  • AI response cleaner and text humanizer

VFX & camera (yes, really)

  • Depth of field visualizer
  • LUT .cube file 3D visualizer
  • ACES color space matrix calculator
  • Timecode and frame arithmetic calculator
  • Focal length / FOV / crop factor matcher

Why it’s different

Most online dev tools are thin wrappers around a server call.
That’s fine for public data — but when you’re pasting a JWT,
an RSA private key, an ENV file, or a seed phrase, you’re
trusting a server you’ve never audited.

Wizbit processes everything locally. The code runs in your tab.
Close the tab and it’s gone.

Ghost Bugs Cost $40K: A Neural Debugging Postmortem

When AI silently fails for weeks

A production RAG system handling 12,000 queries/day recently ran for three weeks delivering silent errors, resulting in an estimated $40K in flawed decisions before anyone noticed.

The issue wasn’t a crash or a syntax error. It was vector embedding drift—a silent failure state where the system returned incorrect results that appeared entirely plausible on the surface.

These are often called “ghost bugs.” They don’t throw runtime exceptions, they don’t trigger error logs, and they typically pass standard unit tests. Below is an analysis of how this happens, how to identify it, and how to build a monitoring system to catch it.

Tool: Debug your vectors with Vector Distance Calculator

The $40K mistake

In this case study, the RAG pipeline recommended suppliers based on vector search:

query → vector search → top 3 results → LLM ranking.

For three weeks, the system recommended “Supplier B” over “Supplier A,” even though Supplier B was 23% more expensive. The team trusted the outputs because they looked structurally correct.

The root cause was simple: the embedding model was updated from text-embedding-3-small to text-embedding-3-large, but the older documents in the database were never re-indexed. The vectors lived in entirely different dimensional spaces, but the database didn’t reject the query.

// The silent failure
const queryVector = await embed(query, 'large'); // 3072 dims
const results = await db.search(queryVector);
// Returns vectors from 'small' model (1536 dims)
// Database pads with zeros. No error. Wrong results.

Calculating cosine similarity between mismatched dimensions still returns a mathematical output—it is simply the wrong output.

Ghost bug #1: Dimensional drift

This is a common RAG failure mode. To prevent this during deployments, systems should validate dimensions programmatically:

import { cosineSimilarity } from './vector-utils';

async function detectDimensionalDrift() {
  const testQuery = "test document for embedding";

  // Embed with current model
  const currentEmbedding = await embed(testQuery);

  // Check database sample
  const sample = await db.getRandomVector();

  if (currentEmbedding.length !== sample.vector.length) {
    throw new Error(
      `DIMENSIONAL MISMATCH: Current=${currentEmbedding.length}, DB=${sample.vector.length}`
    );
  }

  // Also check distribution
  const similarity = cosineSimilarity(currentEmbedding, sample.vector);
  if (similarity > 0.99) {
    console.warn('Suspiciously high similarity – possible duplicate model');
  }
}

Running this check as part of continuous integration or deployment pipelines can prevent dimensionality mismatches entirely.

Tool: Test embeddings with RAG Chunk Simulator

Ghost bug #2: The chunk boundary failure

Standard RAG practices often involve chunking documents at fixed limits (e.g., 512 tokens). However, key context can easily be split across chunk boundaries, rendering the retrieved data incomplete.

Example:

  • Chunk 1: “Supplier A: $100/unit. Terms: Net 30.”
  • Chunk 2: “Excludes bulk discount of 40% for orders >1000 units.”
  • Query: “cheapest supplier for 2000 units”

The system retrieves Chunk 1 because it mentions the price, but misses Chunk 2 because the discount details fell into a separate vector. As a result, the model recommends the wrong supplier.

The solution: overlapping chunks with metadata

function smartChunk(text, size = 512, overlap = 128) {
  const chunks = [];
  const sentences = text.split('. ');

  let currentChunk = '';
  let currentSize = 0;

  for (const sentence of sentences) {
    const tokens = estimateTokens(sentence);

    if (currentSize + tokens > size) {
      // Save current chunk with overlap metadata
      chunks.push({
        text: currentChunk,
        metadata: {
          has_continuation: true,
          next_chunk_preview: sentences.slice(0, 3).join('. ')
        }
      });

      // Start new chunk with overlap
      const overlapText = currentChunk.split(' ').slice(-overlap).join(' ');
      currentChunk = overlapText + ' ' + sentence;
      currentSize = estimateTokens(currentChunk);
    } else {
      currentChunk += ' ' + sentence;
      currentSize += tokens;
    }
  }

  return chunks;
}

Ghost bug #3: Temperature creep

LLM parameters are highly sensitive. A configuration meant for creative tasks can cause hallucinations if applied to analytical ranking tasks.

Consider a configuration setup like this:

// config.js
export const LLM_CONFIG = {
  temperature: process.env.LLM_TEMP || 0.7,
  //...
};

If an environment variable in production is accidentally modified (e.g., setting LLM_TEMP=1.2 for testing without reverting it), the model can produce highly inconsistent or hallucinated supplier rankings without throwing a system error.

The solution: runtime configuration validation

function validateLLMConfig(config) {
  const issues = [];

  if (config.temperature < 0 || config.temperature > 1) {
    issues.push(`Temperature ${config.temperature} out of bounds [0][1]`);
  }

  if (config.temperature > 0.3 && config.use_case === 'ranking') {
    issues.push('High temperature for ranking task – expect inconsistency');
  }

  // Check for drift from baseline
  const baseline = 0.7;
  if (Math.abs(config.temperature - baseline) > 0.2) {
    issues.push(`Temperature deviated >0.2 from baseline`);
  }

  if (issues.length > 0) {
    throw new Error(`LLM Config Validation Failed:n${issues.join('n')}`);
  }
}

Tool: Validate your JSON configs with JSON Validator

Building a neural debugger

Traditional debuggers are not designed to inspect vector spaces. Monitoring a production RAG system requires tracking the invisible metrics of embeddings and distributions.

Component 1: Embedding fingerprinting

You can verify the stability of your embedding space by regularly testing a static group of phrases:

async function createEmbeddingFingerprint() {
  const testPhrases = [
    "the quick brown fox",
    "supplier pricing data",
    "technical specifications",
    "random unrelated text about cats"
  ];

  const fingerprints = await Promise.all(
    testPhrases.map(async phrase => ({
      phrase,
      vector: await embed(phrase),
      hash: simpleHash(await embed(phrase))
    }))
  );

  await db.saveFingerprint({
    timestamp: Date.now(),
    model: EMBEDDING_MODEL,
    fingerprints
  });

  return fingerprints;
}

// Check for drift on a daily schedule
async function checkForDrift() {
  const baseline = await db.getLatestFingerprint();
  const current = await createEmbeddingFingerprint();

  for (let i = 0; i < baseline.fingerprints.length; i++) {
    const similarity = cosineSimilarity(
      baseline.fingerprints[i].vector,
      current[i].vector
    );

    if (similarity < 0.95) {
      alert(`EMBEDDING DRIFT DETECTED: ${baseline.fingerprints[i].phrase}`);
    }
  }
}

Component 2: Query-result monitoring

Analyzing the distributions of search scores helps flag anomalies before they impact users:

async function monitorQueryQuality(query, results) {
  const metrics = {
    query,
    timestamp: Date.now(),
    resultCount: results.length,
    avgSimilarity: results.reduce((sum, r) => sum + r.score, 0) / results.length,
    topScore: results[0]?.score || 0,
    scoreVariance: calculateVariance(results.map(r => r.score))
  };

  if (metrics.avgSimilarity < 0.7) {
    logWarning('Low similarity scores – possible embedding mismatch');
  }

  if (metrics.scoreVariance < 0.01) {
    logWarning('All scores nearly identical – possible dimensional issue');
  }

  if (metrics.topScore > 0.99) {
    logWarning('Suspiciously perfect match – check for data leakage');
  }

  await db.logMetrics(metrics);
}

Component 3: The “Golden Query” set

Run automated tests against queries with static, known correct answers to evaluate performance continuously:

const GOLDEN_SET = [
  {
    query: "cheapest supplier for bulk orders",
    expected_top_result: "supplier-a",
    min_similarity: 0.85
  },
  {
    query: "supplier with fastest delivery",
    expected_top_result: "supplier-c",
    min_similarity: 0.80
  }
];

async function runGoldenTests() {
  const failures = [];

  for (const test of GOLDEN_SET) {
    const results = await ragSearch(test.query);
    const topResult = results[0];

    if (topResult.id !== test.expected_top_result) {
      failures.push({
        test: test.query,
        expected: test.expected_top_result,
        got: topResult.id,
        similarity: topResult.score
      });
    }

    if (topResult.score < test.min_similarity) {
      failures.push({
        test: test.query,
        issue: 'low_similarity',
        score: topResult.score,
        threshold: test.min_similarity
      });
    }
  }

  if (failures.length > 0) {
    await alertEngineering('GOLDEN TEST FAILURES', failures);
  }

  return failures.length === 0;
}

// Execute tests at regular intervals (e.g., every 5 minutes)
setInterval(runGoldenTests, 5 * 60 * 1000);

Typical metrics after implementation

Implementing these checks transitions a team from reactive troubleshooting to proactive observability:

Metric Unmonitored Monitored
Detection Time Weeks (or never) Minutes
Silent Failures Caught None High
Engineering Stance Reactive Proactive

Tool: Format your debug logs with JSON Formatter

Auditing your RAG setup

If you are running a production RAG system, a quick baseline audit is highly recommended:

  1. Verify dimensions: Ensure all stored vector spaces strictly match your runtime embeddings model.
  2. Establish golden tests: Set up a list of key queries with deterministic target results.
  3. Audit similarity distribution: Look for unexpectedly flat or perfect similarity scores.
  4. Enforce configuration schema: Guard runtime variables like temperature with schema validation.

RAG applications operate on non-deterministic models. If you are not monitoring the hidden parameters of your vector spaces, you may be missing critical bugs.

Have you encountered silent errors in your AI applications? Share your debugging strategies or experiences in the comments below.

Tools referenced in this post:

  • Vector Distance Calculator
  • RAG Chunk Simulator
  • JSON Validator
  • JSON Formatter

Read more: Debugging RAG Vector Distance

SECPAC: A Lightweight CLI Tool to Password-Protect Your Environment Variables 🚀

If you are tired of dealing with plain-text .env files and manual editing, SECPAC is a lightweight Node.js CLI tool built to bring structured management and optional password encryption to your local environment variables. By serving as a faster, more secure alternative to traditional setups, it allows you to easily view, grab, or mask sensitive API keys directly from your terminal. Check out the project, join our growing community at r/secpac, and let me know how it improves your development workflow!

PasteCheck v1.7 + v1.8 — Hints that tell you what to fix, and a nudge panel that tells you where to start

Most linters tell you what’s wrong. They stop there.

PasteCheck has always shown you the error and the line. But “var is discouraged” doesn’t tell a beginner what to actually do about it. “Empty catch block” doesn’t tell you why it matters or what to replace it with.

That changes in v1.7.

Every error and warning message now ends with one actionable sentence. Not a docs link. Not a vague suggestion. One clear fix.

  • Prefer 'let' or 'const' over 'var' — use 'const' if the value never changes, 'let' if it does
  • 'eval()' executes arbitrary code — dangerous and a security risk — remove it and find a safer alternative like JSON.parse() or a lookup object
  • Empty catch block — errors are silently swallowed — add at least console.error(e) so failures don't disappear silently

This covers every language PasteCheck supports — JavaScript, TypeScript, Python, HTML, and CSS. 30 messages updated across the entire linter.

Two Reddit commenters validated this independently before it was built. Both said the same thing unprompted — knowing what’s wrong isn’t enough, you need to know what to do next. That’s what v1.7 ships.

v1.8 adds something no other paste tool has.

When errors or warnings exist, a collapsible “What to do next” panel appears below the results. Not per-error guidance — big picture debugging logic. The kind of thing an experienced developer would tell you if they were sitting next to you.

  • If you have errors: fix the first one before the rest. Errors cascade. One missing bracket can cause five false positives below it.
  • If you have warnings only: no errors is good — work through warnings one at a time using the fix hints above.
  • If you have both: ignore the warnings for now. Fix the errors first. Warnings can wait until your code runs.

It’s collapsed by default. It doesn’t get in the way. It’s there when you need it.

Both features were designed around the same principle — PasteCheck doesn’t just find your mistakes, it helps you understand them.

Built entirely from an Android phone. Live at pastecheck.co.uk.

8 Real Ways Developers Make Money in 2026 (Ranked by Effort)

Every “make money as a developer” post promises passive riches by Friday. This isn’t that.

I’ve tried most of these. Some paid rent. Some paid for coffee. One quietly pays me while I’m asleep. Below is an honest ranking — sorted from “most effort for the money” to “least” — with realistic timelines and the catch for each, because the catch is the part nobody writes about.

1. Freelancing / contracting

Effort: High · Time to first dollar: 1–6 weeks · Ceiling: Very high

Still the fastest reliable path. Upwork and Fiverr are crowded races to the bottom; the money is in Toptal, Contra, Gun.io, or — best of all — your own network. Your second client should come from your first client’s referral, not a bidding war.

The catch: You’re trading hours for money, plus the unpaid hours of finding clients, scoping, and chasing invoices. It’s a business, not a side hustle. Treat it like one or it’ll treat you like one.

Tip: Niche down. “React developer” competes with 200,000 people. “I rescue stalled Next.js migrations for B2B SaaS” competes with almost none and charges 3x.

2. Build and sell digital products

Effort: High up front, low after · Time to first dollar: 1–3 months · Ceiling: High

Boilerplates, UI kits, Notion templates, VS Code themes, icon sets, Figma files, code generators. Build once, sell forever. Gumroad, Lemon Squeezy, and the GitHub Marketplace make distribution trivial.

The winners solve a boring problem you’ve already solved for yourself. Auth boilerplate. A Stripe + webhooks starter. A deploy script you keep copy-pasting.

The catch: “Build once” is a lie — you’ll support, update, and market it forever. Distribution is 80% of the work and writing the code was the fun 20%.

3. Micro-SaaS / indie hacking

Effort: Very high · Time to first dollar: 3–12 months · Ceiling: Life-changing

The dream: a small product with recurring revenue you own end to end. Totally real — see the indie hackers shipping $5–50k/month tools. Also where most developers quietly burn six months and ship to crickets.

The catch: The hard part isn’t the code, it’s finding a problem people will pay for before you build. Talk to users first. Charge from day one. If nobody pays for the landing page, nobody pays for the product.

4. Teaching: courses, YouTube, cohorts

Effort: High · Time to first dollar: 2–6 months · Ceiling: High

Udemy, YouTube, Egghead, or your own cohort-based course. You don’t need to be the world’s best engineer — you need to be one chapter ahead of your audience and good at explaining.

The catch: It compounds slowly and publicly. Your first 20 videos will flop. The audience is the asset, and audiences take a year+ to build.

5. Technical writing

Effort: Medium · Time to first dollar: 2–4 weeks · Ceiling: Medium

Companies pay $200–$1,000+ per article for quality developer content. Dev tool companies (databases, auth, observability, anything DX-focused) constantly need writers who can actually code. Check Draft.dev, the DevRel job boards, or just pitch companies whose tools you use.

The catch: You need writing samples to get hired to write. Publish a few strong posts (like this one) on your own blog or dev.to first, then point to them.

6. Open source sponsorship

Effort: Medium-high · Time to first dollar: Slow · Ceiling: Medium

GitHub Sponsors, Open Collective, Polar. If you maintain something people depend on, companies and individuals will fund it. Some maintainers fund full-time work this way.

The catch: Sponsorship follows traction, not the other way around. This pays because your project is useful to thousands, which is years of unpaid work first. Treat it as upside on work you’d do anyway, not a plan.

7. Bug bounties

Effort: High · Time to first dollar: Unpredictable · Ceiling: Very high

HackerOne, Bugcrowd, Immunefi (the last one pays eye-watering sums for smart-contract bugs). If you genuinely enjoy breaking things, the top hunters out-earn senior salaries.

The catch: Brutally feast-or-famine and steep learning curve. You can spend 40 hours and find nothing, or one hour and find $20k. Most people find nothing for a long time.

8. Get paid for your profile — even when you’re not job hunting

Effort: Low · Time to first dollar: Days · Ceiling: Modest (but truly passive)

Here’s the one most developers haven’t tried, and the reason I wrote this list.

Think about your last job search. If someone had paid you $100/hour to do it, what would you have earned? Probably thousands. Instead, companies pay for recruiters, job boards, and ads to find you — and you, the actual product, get nothing.

A newer platform called Timofi flips that. You record a short set of clips — basically a mini interview (how you think, a project you’re proud of, a tradeoff you’d defend) — and drop one link on your LinkedIn, GitHub, or resume. When a recruiter wants to unlock it and actually see you talk, they pay a small fee, and you keep 60%.

The part that sold me: it works even when you’re not looking for a job. If you’re a solid engineer, recruiters already DM you constantly and you ignore them. This makes the good ones pay for your attention. Worst case, someone unlocks, watches, and nothing comes of it — you still got paid for the time. The downside has a floor.

The honest catch: It’s early. The broader hiring marketplace is rolling out gradually, and your earnings scale with how much profile traffic you actually have — so it pays best if you already get inbound from recruiters. It won’t replace your salary. Think “found money on traffic you were ignoring anyway,” not “quit your job.” Setup takes a few minutes, so the effort-to-upside ratio is hard to argue with.

Full disclosure for the dev.to crowd: I think this category — getting paid for your own attention — is genuinely new, which is why it’s on the list. Try it, don’t try it, but the framing alone is worth stealing.

So which one should you actually pick?

  • Need money this month? → Freelancing (#1) or technical writing (#5).
  • Want something that compounds? → Products (#2), teaching (#4), or micro-SaaS (#3).
  • Want upside on work you already do? → Open source sponsorship (#6) or your profile (#8).
  • Love breaking things? → Bug bounties (#7).

The meta-lesson across all eight: stop giving your work, your time, and your attention away for free. Whether you package it as a product, a course, an article, or a paid mini interview — the developers who make money are the ones who decided their output has a price tag.

Which of these have you tried? What worked, what flopped? Drop it in the comments — genuinely curious what’s paying for the dev.to crowd in 2026.

I built a free AI-powered Git CLI that writes your commit messages for you

We’ve all been there. You just spent 3 hours fixing a bug, you stage your changes, and then you stare at the commit message input like it owes you money. You end up typing fix stuff and moving on.

I got tired of it. So I built gritch.

What is gritch?

gritch is a free, open source CLI tool that uses Groq’s free AI API (LLaMA 3.3) to help you write better commits, review your code before pushing, generate changelogs, and understand old commits all from your terminal.

What it can do

Generate a conventional commit message from your staged changes:

git add .
gritch commit

Review your code before pushing:

gritch review
gritch review --language javascript

Explain any commit in plain English:

gritch explain abc1234

Generate a changelog between two versions:

gritch changelog v1.0.0 v1.1.0

How to get started

Install it globally:

npm install -g gritch

Get a free API key from https://console.groq.com and set it:

Windows:

$env:GROQ_API_KEY="your_key_here"

macOS / Linux:

export GROQ_API_KEY="your_key_here"

Then run gritch in any Git project.

Why I built it

I wanted to build something that developers actually use daily not another todo app or weather dashboard. gritch solves a real problem I had every single day, and I built it entirely with TypeScript and Node.js with zero paid services required.

Links

If you find it useful, a star on the repo goes a long way. _

OpenLiDARViewer: A Browser-Based LiDAR and Point-Cloud Viewer

Rendering LiDAR Scans in the Browser Without Uploading Anything

Most point-cloud workflows still start the same way.

Install a desktop tool.
Import the scan.
Wait.
Inspect.
Measure.
Export.

That workflow makes sense when you are doing serious GIS, photogrammetry, survey processing, classification, or production work.

But there is another moment that is much simpler:

You just received a scan and want to know what is inside it.

Does it open?
Is it clean?
Does it have color, intensity, or classification?
Can I measure something quickly?
Can I show it to someone without making them install software?

That is the moment I wanted to improve with OpenLiDARViewer.

It is an open-source, browser-based LiDAR and point-cloud viewer built around one simple idea:

Drop a scan into the browser and inspect it locally.

No upload.
No account.
No desktop install.
No conversion step for supported formats.

Live demo: https://lidar.aurtech.mx/
GitHub: https://github.com/Aurtechmx/openlidarviewer/

The real goal: own the first 60 seconds

There are already great tools for point-cloud work.

CloudCompare is powerful.
QGIS is powerful.
Potree is excellent for publishing point clouds on the web.
Professional LiDAR software exists for a reason.

OpenLiDARViewer is not trying to replace those tools.

It is focused on a smaller, earlier step:

the first 60 seconds after you get a scan.

Before you process it.
Before you classify it.
Before you prepare a report.
Before you decide what workflow it belongs to.

Sometimes you just need a fast first look.

That sounds simple, but LiDAR data makes “just open it” surprisingly complicated.

Keeping the scan on the device

One thing I wanted from the start was simple:

The scan should not have to leave the user’s machine just to be inspected.

For LiDAR and 3D scan data, that matters.

A scan can represent a private site, a client project, a building, a terrain model, an industrial asset, or a research dataset. Uploading it to a random cloud viewer just to take a quick look is not always acceptable.

So OpenLiDARViewer runs client-side.

The browser loads the app, but the scan itself is read, parsed, analyzed, and rendered locally.

There is no backend parser.
No server-side preprocessing.
No cloud ingestion pipeline.
No “upload your file and wait.”

Just the browser, the GPU, and the file on your machine.

Supporting real-world scan formats

The viewer now supports common point-cloud and scan formats such as:

  • LAS
  • LAZ
  • E57
  • PLY
  • OBJ
  • GLB / GLTF
  • XYZ
  • CSV

That mix is intentional.

I wanted the same viewer to handle both:

  • drone / aerial LiDAR
  • phone and mobile 3D scans

Those worlds often feel separate, but in practice they are starting to overlap. People capture data from drones, phones, scanners, photogrammetry tools, and AR apps. A lightweight viewer should not care where the scan came from as long as the data can be parsed and rendered.

E57 support was especially important because it is a serious professional exchange format. It also brings more complexity: metadata, scan structure, transforms, optional attributes, and vendor differences.

Getting that kind of file working in the browser is not just a checkbox. It is an interoperability milestone.

Measurement had to become more than a demo feature

The first measurement tool was simple: pick two points and get a distance.

Useful, but limited.

The newer measurement workflow is more practical. It includes:

  • distance
  • polyline
  • area
  • height
  • angle
  • slope
  • unit switching
  • editable points
  • clearer measurement sessions

It does make it more useful for quick inspection.

Scan intelligence: not just seeing the cloud

A point cloud viewer should do more than show dots.

When I open a scan, I want quick answers:

  • How many points are in this file?
  • What is the extent?
  • What is the approximate density?
  • Does it have RGB?
  • Does it have intensity?
  • Does it have classification?
  • Are there invalid coordinates?
  • Are there suspicious outliers?
  • Does the decoded data match what the file claims?

That is why OpenLiDARViewer includes scan intelligence and validation modules.

The goal is not to replace professional QA/QC.

The goal is to give a useful first read before committing to deeper processing.

Seeing the scan matters.

Understanding whether the scan looks intact matters too.

Why the interface is more game-like than GIS-like

A lot of spatial tools inherit GIS interaction patterns.

That is fine for GIS users, but it can be intimidating for people who just want to move through a 3D scan.

OpenLiDARViewer uses a more direct navigation model:

  • Orbit mode
  • Walk mode
  • Fly mode
  • WASD movement
  • mouse-look
  • touch support on mobile
  • point inspection
  • saved viewpoints

The goal is to make the scan feel like a space you can enter, not just a dataset you loaded.

Orbit works well for objects and small sites.
Walk makes more sense for interiors or street-level scans.
Fly feels better for terrain, drone LiDAR, and wide-area data.

This is one of the parts I care about most because good interaction design can make technical data much easier to understand.

Mobile was not optional

At first, this kind of tool feels desktop-first.

Then reality shows up.

People want to open scans:

  • on site
  • on tablets
  • on phones
  • during a demo
  • while moving between field and office workflows

Mobile support creates its own set of problems:

  • touch controls
  • viewport behavior
  • limited memory
  • smaller screens
  • browser gestures
  • GPU differences
  • file picker behavior

A viewer that technically opens on mobile but feels terrible is not really mobile-friendly.

So mobile support became part of the direction, especially for phone scan exports and quick review workflows.

Feedback I am looking for

If you work with any of these areas, I would really value your feedback:

  • LiDAR
  • UAV mapping
  • GIS
  • photogrammetry
  • 3D scanning
  • WebGL / WebGPU
  • point-cloud visualization
  • browser-based technical tools

The most useful feedback would be:

  • Does your file open?
  • Does performance feel acceptable?
  • Are the controls intuitive?
  • Are the measurement tools useful?
  • Does E57 behave correctly with your files?
  • What format support is missing?
  • What breaks?
  • What would make this useful in a real workflow?

Live demo: https://lidar.aurtech.mx/
GitHub: https://github.com/Aurtechmx/openlidarviewer/

If the project is useful, a GitHub star helps. But real feedback from people who work with point-cloud data helps even more.

Final thought

The browser is becoming a serious runtime for technical software.

Not for everything.

Not for every workflow.

But for the first step — opening, inspecting, measuring, and understanding spatial data — it is starting to make a lot of sense.

That is the direction OpenLiDARViewer is exploring.

Why most freelancers undercharge (and the maths behind fixing it)

Most freelancers set their rate by looking at what others charge and picking something nearby. This feels safe. It also creates a systematic problem: you end up pricing based on what the market accepts, not what your business actually needs.

Here is the maths that most rate-setting advice skips.

The utilisation problem

If you work 40 hours a week, you are not billing 40 hours a week.

A realistic breakdown looks something like this:

  • Client calls and emails: 4-6 hours
  • Proposals and pitching: 3-5 hours
  • Admin, invoicing, accounting: 2-3 hours
  • Learning and keeping skills current: 2-4 hours

That leaves roughly 22-28 billable hours out of 40. Call it 55-65% utilisation.

A developer billing $40/hr with 20 real billable hours per week is effectively running a ~$40k/year business before taxes and downtime. That number tends to surprise people when they see it written out.

So if you want to earn $80,000 a year, you cannot divide by 2,000 hours and call it your rate. You need to divide by your actual billable hours.

$80,000 / 1,200 billable hours = $66.67/hr minimum

Before taxes. Before slow months. Before the occasional client who costs you two weeks of work and pays nothing.

The slow month multiplier

Freelance income is not linear. If you plan for 48 weeks of full utilisation and you actually get 38, your effective rate just dropped 21% below what you calculated.

The fix is to build slow months into your rate from the start, not to adjust when they happen.

A more conservative model:

  • Target: 46 billable weeks per year (not 52)
  • Utilisation: 60% of working hours
  • Effective billable hours: 46 x 40 x 0.60 = 1,104 hours

For an $80,000 annual target: $80,000 / 1,104 = $72.46/hr before tax

Add 25-30% for self-employment tax and the minimum viable rate is around $90-95/hr. Depending on your market and experience level, that number may feel impossibly high. That’s exactly why so many freelancers end up overworked and underpaid — the maths doesn’t change because the market is uncomfortable with it.

Why this matters for developers specifically

Frontend and backend developers are particularly prone to undercharging because the market has a wide range and clients often anchor to offshore rates.

That anchor is irrelevant to your cost structure. An offshore developer at $25/hr has different fixed costs, tax obligations, and market positioning. Comparing your rate to theirs is like a restaurant comparing its prices to street food in a different country.

Your rate is a function of your costs and your utilisation. Not of what someone else charges somewhere else.

A simple starting framework

  1. Calculate your target annual income (include savings, not just survival)
  2. Estimate your real billable hours (utilisation x working weeks x hours/week)
  3. Divide income by hours to get your pre-tax minimum
  4. Add your effective tax rate
  5. Add 15-20% as a buffer for slow months and difficult clients

That number is your floor. Anything below it usually means the business only works if you absorb the volatility yourself.

I got tired of doing this maths manually every time someone asked me about freelance pricing, so I built a small calculator for different roles: ratecalculator.xyz

No signup, no email, just the numbers.

We built a mahjong dangerous-tile predictor calibrated on 4.97M real hands

The problem

At a real mahjong table, software can’t help you mid-hand. The question — “is this tile safe to discard?” — comes up every few turns, and you work it out from memory and pattern recognition alone.

We wanted something you could set next to you at the table, pick up in a second, and put down without disrupting the game.

What OkkanaiPai does

Three interactions:

  1. Swipe to select the player you want to read
  2. Tap their discarded tiles to enter them
  3. See all 34 tiles color-coded by danger level

The whole flow is designed to be glanceable — you’re not staring at a screen mid-game.

The model behind it

We didn’t ship a live inference model. Instead, we calibrated a rule-based system against 4.97 million discards from Tenhou’s Houou-takujo server (the top competitive tier), covering 16 days of logged play.

The result is a set of coefficients stored as JSON inside the app. AUC on held-out data came to 0.83. Not perfect, but statistically meaningful for a glance-level judgment call at the table. Everything runs fully offline — no network request, no account, no subscription.

What it doesn’t cover

Being upfront about scope:

  • Calibration is East-round / East-seat data. Accuracy may degrade in South-round play.
  • Meld efficiency — how chii, pon, or kan reshapes hand danger — is not modeled.
  • Three-player mahjong (sanma) is not supported.

For standard 4-player East-focused games, it fits. Outside that, weight the output accordingly.

Why build it

Niixo Labs is running a sprint: small, focused iOS tools, shipped fast. OkkanaiPai explores what “calibrated statistics as UX” looks like in a game-assistance context — where a lightweight, offline coefficient table can deliver meaningful signal without any server infrastructure. Free, no ads, no IAP.

https://apps.apple.com/jp/app/id6762544982

Building a Chord Progression Generator in the Browser — Music Theory in JS, Sound via Web Audio API

Screenshot

Pop songs, jazz turnarounds, Pachelbel’s canon. Chord progressions you’ve heard a thousand times all reduce to scale degrees stacked on a diatonic scale. This tool generates progressions in 12 keys × 2 scales × 7 genre presets and plays them through the Web Audio API. ~300 lines of vanilla JS, zero dependencies.

🌐 Demo: https://sen.ltd/portfolio/chord-progression-gen/
📦 GitHub: https://github.com/sen-ltd/chord-progression-gen

Screenshot

The minimum music theory you need

If “music theory” sounds heavy, the slice you actually need to write code is surprisingly small.

1. A scale is just 7 notes

The C major scale is C, D, E, F, G, A, B. In semitones from the root: [0, 2, 4, 5, 7, 9, 11]. That’s it.

const MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
const NATURAL_MINOR = [0, 2, 3, 5, 7, 8, 10];

Natural minor (C, D, E♭, F, G, A♭, B♭) is [0, 2, 3, 5, 7, 8, 10].

2. A chord stacks the 1st, 3rd, and 5th degrees (diatonic triad)

Take notes 1, 3, 5 of the scale: C, E, G → C major triad. Start from note 2 instead: D, F, A → D minor. The starting position is the “scale degree” and we label it with a roman numeral:

Degree C major Quality
I C Major
ii Dm minor
iii Em minor
IV F Major
V G Major
vi Am minor
vii° B° diminished

Uppercase = major, lowercase = minor, ° = diminished. Every chord progression in this app is a sequence of these seven.

3. A progression is just an ordered list of degrees

  • Pop: I → V → vi → IV (in C: C → G → Am → F — the “Axis of Awesome” progression)
  • Pachelbel’s Canon: I → V → vi → iii → IV → I → IV → V
  • Jazz turnaround: ii → V → I → vi

Building the chord

The triad-building function:

export function chordTones(tonic, scaleName, degree, seventh = false) {
  const scale = SCALES[scaleName];
  const positions = seventh ? [0, 2, 4, 6] : [0, 2, 4];
  return positions.map((p) => {
    const idx = degree + p;
    const octaveShift = Math.floor(idx / 7) * 12;
    return tonic + octaveShift + scale.intervals[idx % 7];
  });
}

tonic is the MIDI number of the root (C4 = 60). When degree + p walks past the seventh scale note we need to wrap to the next octave, hence Math.floor(idx / 7) * 12. Building vi (Am) in C major: chordTones(60, "major", 5) returns [69, 72, 76] — A4, C5, E5.

For seventh chords, change positions to [0, 2, 4, 6]. The chord quality (maj7 / 7 / m7 / m7♭5) we derive from the interval pattern afterwards.

Naming chords without a lookup table

We could hardcode “Am” → “minor on A”, “Bm7♭5” → “half-diminished on B” and so on. But that table has 12 keys × 7 degrees × 4 qualities × {triad, seventh} entries, and it doesn’t extend when you add modes (Dorian, Phrygian, harmonic minor).

Better: derive the name from the interval pattern of the constructed chord:

export function chordName(tonic, scaleName, degree, seventh = false) {
  const tones = chordTones(tonic, scaleName, degree, seventh);
  const rootPc = ((tones[0] % 12) + 12) % 12;
  const third = tones[1] - tones[0];
  const fifth = tones[2] - tones[0];
  let quality = "";
  if (third === 4 && fifth === 7) quality = "";      // Major
  else if (third === 3 && fifth === 7) quality = "m"; // minor
  else if (third === 3 && fifth === 6) quality = "°"; // dim
  else if (third === 4 && fifth === 8) quality = "+"; // aug
  // ... seventh logic continues
  return NOTE_NAMES[rootPc] + quality;
}

Major third (4 semis) + perfect fifth (7 semis) → Major. Minor third (3) + perfect fifth (7) → minor. Minor third (3) + diminished fifth (6) → diminished. Names fall out of the intervals, so adding Dorian or harmonic minor later doesn’t require touching the naming logic at all.

Playing it through Web Audio

The audio engine is the kind of thing that looks complicated and turns out to be 30 lines.

import { midiToHz } from "./theory.js";

class ChordPlayer {
  constructor() {
    this.ctx = null;
  }

  ensureContext() {
    if (this.ctx) return;
    this.ctx = new AudioContext();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.18;
    const filter = this.ctx.createBiquadFilter();
    filter.type = "lowpass";
    filter.frequency.value = 2400;
    this.master.connect(filter);
    filter.connect(this.ctx.destination);
  }

  scheduleChord(tones, startTime, duration) {
    for (const midi of tones) {
      const osc = this.ctx.createOscillator();
      osc.type = "triangle";
      osc.frequency.value = midiToHz(midi);
      const gain = this.ctx.createGain();
      gain.gain.setValueAtTime(0, startTime);
      gain.gain.linearRampToValueAtTime(1 / tones.length, startTime + 0.02);
      gain.gain.linearRampToValueAtTime(0, startTime + duration);
      osc.connect(gain);
      gain.connect(this.master);
      osc.start(startTime);
      osc.stop(startTime + duration + 0.05);
    }
  }
}

Four small choices matter:

1. Use triangle oscillators

sawtooth is too harsh, sine is too thin, square sounds like a Game Boy. triangle lands in the middle and reads as “acoustic-ish” without effort.

2. ADSR can be trivial

Real pianos and strings have complex envelopes. For chord progressions you don’t need them. A 20 ms attack and a short linear release is enough to avoid clicks and sound natural:

volume
 1.0 │   /‾‾‾‾‾‾‾‾‾‾‾
     │  /              
 0.0 │_/                ____
     ├──┼──────────────┼─┼──→ time
        20ms          release start

3. Divide gain by chord size

Stacking 3 or 4 oscillators at full volume sums to clipping. Multiply each oscillator’s gain by 1 / tones.length and the master stays well-behaved no matter how many voices you add.

4. Resume the context on user gesture

Chrome’s autoplay policy refuses to let AudioContext make sound until the user has interacted with the page. If you call osc.start() before ctx.resume(), the first chord goes silently into the void:

async resume() {
  this.ensureContext();
  if (this.ctx.state === "suspended") await this.ctx.resume();
}

Skip this and you get the classic “works locally, silent in production” trap.

Architecture

theory.js   ← Music theory (pure, zero DOM/Audio dependencies)
audio.js    ← Web Audio scheduler (consumes MIDI numbers from theory.js)
app.js      ← UI glue (DOM events → theory → audio)

Nothing in theory.js knows about the DOM or about AudioContext. That means Node’s built-in test runner can verify all 25 cases of the music logic without a browser:

import { test } from "node:test";
import assert from "node:assert/strict";
import { buildProgression, tonicMidi, PRESETS } from "../theory.js";

test("Pop in C major yields C G Am F", () => {
  const out = buildProgression({
    tonic: tonicMidi("C", 4),
    scaleName: "major",
    degrees: PRESETS.pop.degrees,
  });
  assert.deepEqual(out.map((c) => c.name), ["C", "G", "Am", "F"]);
});

audio.js consumes the output of theory.js (MIDI number arrays) and has no opinion about scales or roman numerals. The dependency arrow only points one direction. When you eventually add Dorian, harmonic minor, secondary dominants, or jazz substitutions, the audio layer doesn’t change at all — theory.js keeps emitting MIDI, the rest keeps playing it.

app.js is a tiny coordinator: UI event → mutate state → rebuild progression via theory → optionally play through audio. No React, no Vue, no signals. The whole state is a plain object.

Try it

Takeaways

  • Chord progressions reduce to 7 notes × scale degrees × roman numerals — small enough to fit in your head
  • Deriving chord names from interval relationships instead of a lookup table makes the music-theory module trivially extensible
  • The Web Audio API plays chord progressions cleanly with triangle waves + minimal ADSR + autoplay-policy handling — that’s the whole synthesis layer
  • Keeping music theory pure (no DOM, no audio) means node --test can verify all the harmony rules without spinning up a browser

This is OSS portfolio #240 from SEN LLC (Tokyo). We ship small, sharp tools continuously: https://sen.ltd/portfolio/