Cover illustration

TheDaily Front

Issue No. #260726 Sunday, July 26 2026 #260726 — SUNDAY, JULY 26, 2026
From cookie pop‑ups to AI crawlers, today the web said: enough.
Sunday, July 26, 2026 The Daily Front No. #260726 — Contents
30stories
7,596points
3,880comments
307kllm tokens
Assembled with 31 model calls — 192,417 tokens read, 114,614 written.

Highlights

Running a 28.9M parameter LLM on an $8 microcontroller

A 28.9M-parameter language model runs entirely on an $8 ESP32-S3 at ~9 tokens/sec—no server, no phone-home.

A shell colon does nothing. Use it anyway

The humble shell colon is a powerhouse of idioms—use it for arguments, defaults, flow control and more.

Htmx 4.0, the first JavaScript library to release exclusively on the Game Boy

HTMX 4.0 debuts as a real Game Boy cartridge—joyful marketing for a pragmatic web tool.

Ruff v0.16.0 – Significant new updates – 413 default rules up from 59

Ruff 0.16 ships with 413 default rules and blistering speed, consolidating Python linting and formatting.

Inflect-Micro-v2: complete voice in 9.36M parameters

Inflect‑Micro‑v2 delivers complete local English TTS under 10M parameters with deterministic seeds.

From the Editor

The web’s house rules are back in fashion. Between a push to end cookie banner theater and fresh levers for blocking AI crawlers, today’s front page reads like a manifesto for user choice. Meanwhile, builders kept shipping—tiny voices from tiny chips, new lint for old code, and old ideas made new again.

  1. Kill The Cookie Banner3
  2. Cloudflare's new AI traffic options for customers4
  3. LLM Usage in Debian: Three Proposals5
  4. Rethinking legal education in the AI era6
  5. What is happening to jobs? Separating AI hype from reality7
  6. The relay market powering token resellers and fraud8
  7. Turn And Face The Strange9
  8. A shell colon does nothing. Use it anyway10
  9. Go Analysis Framework: modular static analysis by go team11
  10. Ruff v0.16.0 – Significant new updates – 413 default rules up from 5912
  11. Some more things about Django I've been enjoying13
  12. How to write English prose (2023)14
  13. Running a 28.9M parameter LLM on an $8 microcontroller15
  14. I learned PCB design, 3D printing and C just to listen to music16
  15. Show HN: CheapSecurity – Lightweight, Self-Hosted CCTV for Linux SBCs17
  16. Show HN: Reverse Minesweeper18
  17. Decker, a platform that builds on the legacy of Hypercard and classic macOS19
  18. Alien World Chemistry Found Inside Meteorite That Struck New Jersey Home20
  19. What's Under Your Feet in New York City?21
  20. The New AI Superpowers: Focus and Followthrough22
  21. Inflect-Micro-v2: complete voice in 9.36M parameters23
  22. GrapheneOS protections against data extraction from locked devices24
  23. DeepSeek pause fundraise after comments on compute gap to US leaked (transcript) [pdf]24
  24. Google Discloses $94.1B in SpaceX Stock, Marking 6% Stake24
  25. Htmx 4.0, the first JavaScript library to release exclusively on the Game Boy25
  26. Show HN: I mapped every US golf course25
  27. An ESP32 based plane radar for my desk25
  28. Clinical failure rates over the decades: yikes26
  29. Design is compromise26
  30. Introduction to Data-Oriented Design [pdf]26
The Daily Front Page 2 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Enough with the Banners
article

Kill The Cookie Banner

by rapnie·▲ 947 points·453 comments·killthecookiebanner.eu ↗
Cookie banners are made to trick you into waiving your rights.

Kill the cookie banner!

Tired of misleading cookie banners? The EU Commission has finally proposed a solution: set your privacy preferences in the browser once, and never see another banner. Unfortunately, the tracking industry is pushing back – and so far, they’ve been successful. We need YOUR help to #KillTheCookieBanner!

Learn more

Take Action!

Cookie banners are made to trick you into waiving your rights

You may think that EU privacy law requires cookie banners. But the law is clear: online tracking is prohibited by default.

Therefore, the tracking industry needs you to waive your rights. That’s why they invented cookie banners, which often are deliberately misleading as well as annoying.

This results in up to 90% of people saying “YES” – even though only around 3% actually want to be tracked online. This system is broken by design.

The solution: automatically communicate your privacy preference

In Autumn 2025, as part of a bigger legal reform*, the EU Commission finally proposed a solution to the cookie banner problem: automated signals that would communicate your privacy preferences between your device and websites or apps. You could then choose whether you want to accept, refuse, or limit tracking.

This idea is neither new nor complex. Your browser already automatically signals other preferences to websites, for example your preferred language. In some US states, such signals for privacy preferences are already legally supported.

The tracking lobby fights to keep the cookie banner

This would be a simple solution. But the tracking industry seems afraid that if you can express your preferences efficiently, it could result in lower consent rates for tracking.

Following lobbying efforts spearheaded by Google and the tracking industry, several Member States are now blocking the EU Commission’s proposal to get rid of the cookie banner.

But not only that: industry groups are also lobbying the European Parliament to reject the proposal.

We need YOUR help!

That’s where you come in: the fight to kill the cookie banner is far from over. The Member States and the European Parliament have not yet decided on their position on the issue yet.

You can take action by contacting your representative in the European Parliament or in your Member State and express your frustration.

Take Action!

*This proposal for privacy signals is part of an EU law reform called the Digital Omnibus. Most other parts of this reform are problematic and would weaken people’s rights. We want to make clear that we do not support these other aspects of the proposed reform.

With your help, we can kill the cookie banner!

Contact your relevant representative in the European Parliament or national government and express your frustration!

Take Action!

This initiative is led by civil society organisations across Europe:

The initiative is also supported by:

Your organisation wants to support this initiative as well? Find out how you can get involved here!

The Daily Front Page 3 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Cloud Rules for AI
article

Cloudflare's new AI traffic options for customers

by alphabetatango·▲ 189 points·147 comments·blog.cloudflare.com ↗
Your site, your rules.

BLOG-3337 1

One year ago, we declared the first Content Independence Day, and we gave website owners the means to take back control of their content. The deal between crawlers and website owners that had held up for 30 years — we crawl you, and you get referrals — was no longer true. AI was taking everything and sending back nothing, presenting an existential threat to website owners. And so we launched a one-click "Block AI Bots" option, along with a Pay-Per-Crawl marketplace.

A lot has changed in a year. Last July, conversations around “AI bots” centered around blocking AI training without compensation, pointing to the win–lose deal where content was used for model training with no value driven back to the website owner. But a desire for more nuance has emerged: Content owners still want to be able to protect their content, and they should be compensated for the original content that they work hard to create, curate, and share. We also know that locking down content isn’t a one-size-fits-all solution; website owners want more options than resorting to “block all automation, every time.”

If you run a small site, the problem isn’t just that someone could train models on your content — it's that nobody can find you in the first place. So you have to make a Faustian bargain: either show up in search and let AI train on you, or risk losing discoverability. This unfairly advantages incumbent search providers if they use the same bots for both search and training; and this unfair advantage incentivizes new players to be evasive as they try to close the competitive gap.

Now, AI can be anything

Today, AI can be in anything. Google search has changed from being sorted by AI to being a full answer engine that answers your question directly on the results page. And Google is not unique in this position — this is the direction in which “search” is moving.

We could debate the cutoff for what qualifies as “AI” today, just to find that the standard changes tomorrow. So, instead of defining a bot primarily as “AI” or not, our updated approach to classification will ask deeper questions about bot or agent behavior: What are they doing on my site? What are they storing? And how will they reshare my content?

A pragmatic taxonomy

To address these questions, we need a more nuanced view — a pragmatic taxonomy that aligns with the AI use cases our customers care about. So we are opening the discussion beyond AI training alone and focusing on three AI use cases that we want all customers to be able to manage:

  • Search: any behavior that collects or indexes your content, so it can answer questions about it later. The key is that Search is proactively building a database of your site to later respond to queries with. Site owners should expect to get referral traffic or other equitable compensation as a result.
  • Agent: automated behavior that is acting, usually in real time, on a person's behalf, to get something done right now. This includes chat fetch bots (e.g., ChatGPT-User) and browser-use agents (e.g., Gemini or Claude driving Chrome). The key is that it visits your web application in order to complete a job, and often there's a human waiting on the other end.
  • Training: a crawler taking your content to train or fine-tune a model. The key is that your data is permanently absorbed into the underlying architecture of the AI to improve its capabilities.

Many popular crawlers on the web fall into one of the classifications above; some fall into multiple. We classify plenty of other behaviors beyond the three above — including ads verification, feed fetching, and agentic transactions (more on this below). But we believe it should be simple for all website owners to manage access for these three AI-centered use cases. We believe that bot operators should separate their crawlers because that creates more transparency for website owners: allowing them to better understand why a given crawler is visiting them, as well as to better manage the access they extend to that crawler. If a company runs automation that builds Search indexes, acts as an Agent, and collects data to Train their models, then we strongly encourage that company to separate the automation into three separate crawlers.

We want a classification system that is scalable and representative of the world of automated traffic as it evolves. Tracking a bot’s purposes is nothing new, but our new taxonomy involves a few updates that better represent the state of bot traffic today. Most notably, we want to recognize that bots that have multiple purposes should be tracked with all purposes, not just one of them.

New options to manage AI traffic

We want to provide more options for managing different kinds of AI traffic, to all website owners on the Cloudflare network.

The managed preset to “Block AI bots” that we’ve announced in the past included single-purpose bots that crawled data for model training, as shown below: 

BLOG-3337 2

Screenshot of the existing setting to manage AI bot traffic on July 1, 2025.

But not all AI use is the same, and we want our customers to have the controls they need. So, we’re launching the ability to manage AI traffic based on three major use cases: Search, Agent, and Training crawlers. With these new options, our customers can more finely tune how they manage AI bot traffic — including customers on our Free tier.

BLOG-3337 3

Screenshot of the new options to manage AI bot traffic on July 1, 2026.

Setting new defaults

On September 15, 2026, we’ll be setting new defaults for each of these three classifications. For all new domains onboarding to Cloudflare, the categories of Training and Agent will be blocked by default on the pages that display ads, while Search will remain allowed by default. 

An ad is a signal that a website owner meant for a person to land there and see it — something monetizable that fuels the business. So, on those pages, we treat human attention as the end goal, and keep away the bots that may prevent this attention (i.e., Training and Agent bots). On the other hand, Search is the behavior that most naturally funnels back visitors, and we believe it’s in the interest of most site owners to allow this.

Another change that will apply on September 15 is that multi-purpose crawlers (specifically those that combine Search with Training) will be allowed/blocked according to all of their behaviors, in line with our call for transparency for website owners. Since the defaults will be enforced by the most restrictive applicable rules, multi-purpose crawlers such as Googlebot, Applebot, and BingBot will be blocked by customers who have selected to block Training (either through the new options to manage AI traffic, or through the legacy Block AI bots service).

Of course, customer choice is paramount: if a website owner wants to opt out of these new default configurations, they can easily mark this in their Security settings any time leading up to September 15, which will confirm that they want no changes on Training crawlers that also crawl for Search purposes. We’ll also continue to notify customers of the upcoming change to defaults as we approach September 15 to ensure that customers who want to choose settings different from the defaults have the opportunity to do so.

BotBase: a new visibility plane for Enterprise customers

We’re also excited to launch a major visibility update as a new feature of Enterprise Bot Management. As Cloudflare’s directory of tracked bots has grown, so has the desire to manage these bots in sensible groupings and to understand more detail about a particular bot. 

Introducing BotBase. BotBase is our new database tracking all known bots, including Verified bots and agents. This database provides a comprehensive, searchable view of our entire directory of bots, directly on the Cloudflare dashboard. We’re tackling visibility first, but, later this year, we’ll expand BotBase to provide a direct control center for known automated content on your website.

With this new view, Enterprise Bot Management customers can see the full catalogue of all Verified bots/agents and where they are classified in this updated taxonomy — a view we’ve never shown dynamically on the Cloudflare dashboard before. Customers who want to precisely target a specific bot can also easily filter for all traffic from this bot, plus copy the detection ID to use in Security rules. All of this is now live within a dedicated page, which can be accessed through the Bot Management configuration card

As we built BotBase, we wanted to account for all of the pieces of information that would allow us to build scalable, powerful insights from bot to bot. One of these pieces is a cornerstone for our updated taxonomy, which is based on what a bot may do on your site — its behavior. We separate these classifications as shared below, and each bot is classified with one or more of these behaviors.

Bot classification

Behaviors and uses

Search

Crawling to scan your site to help it appear in search engine results

Agent

User-directed agents visiting a page on behalf of a human

Training

Crawling to train or fine-tune models

Transact

Checkout actions on behalf of users

Data Collection

Includes price scraping, competitive intelligence gathering, and third-party analytics

Security Testing

Includes vulnerability scanning and penetration testing

SEO

SEO crawling, site auditing, accessibility checks

Ads Verification

Ad placement verification, ad fraud detection

Social / Link Preview

Link previews for social platforms and messaging apps

Feed Fetching

Includes RSS readers, podcast aggregators, and news feed bots

Monitoring & Operations

Includes uptime monitoring, webhooks, and health checks

Bold italicized rows indicate the new configurable options that are available to all customers.

How does a crawler use my content?

Another piece of information we’ve heard is important to our customers is a bot’s content use — what a bot may keep and reshare after it has crawled your content. To address this, we are building capabilities for Bot Management customers to select and block based on the “content use.” This setting can be set to one of three levels, from least to most permissive:

  • immediate — interact, but store and reuse nothing
  • reference (default) — index, excerpt, and link back
  • full — summarize and reproduce

These values can be combined with bot classifications to express nuanced rules, such as “allow all bots that are used for Search, SEO, and Ads Verification, but only up to the reference use level.” This allows website owners to make decisions in sensible groupings rather than manage individual bot-by-bot rules**.**

To further support this, starting today, we're testing a new signal, use, that extends Content Signals and lives in your robots.txt. This extends the three fields of the first version of Content Signals with a fourth, optional field that expresses the same preference as above:

  • use=immediate
  • use=reference
  • use=full

As with all other items listed in the robots.txt file, the values of content use signal a website owner’s preference, rather than issuing blocks directly. We’re now adding support for this extension: all customers who have already enabled managed robots.txt — which prepends the preference to robots.txt that crawling for search is okay, but that crawling for training is not — will now have the additional preference of use=reference added to their robots.txt.

# Cloudflare Managed content with original Content Signals

User-agent: *
Content-Signal: search=yes,ai-train=no
Allow: /

The contents of Cloudflare managed robots.txt with the original Content Signals values.

# Cloudflare Managed content with the new content-use signal

User-agent: *
Content-Signal: search=yes,ai-train=no,use=reference
Allow: /

The contents of Cloudflare managed robots.txt with the added parameter.

We’re also starting to track content uses for every bot in BotBase, and when we discover a bot abusing these signals, it will lose the “Verified” status, resulting in it no longer being allowed. Today, bots that reproduce in full cannot have the Verified status.

What does it mean for a bot to be Verified?

Speaking of “Verified,” the definition of Verified is being updated to reflect the upcoming changes to default allow and block baselines. Previously, all Verified bots were allowed by default, which was reflected in our basic Bot Fight Mode offering to block unwanted automatic traffic and in our rule templates for Enterprise Bot Management customers. 

Starting today, we’re adjusting this to add nuance: non-verified bots are still default blocked, but we are no longer viewing Verified as “default allowed.” Now, the Verified label makes a bot allowable with its relevant category, meaning the allowed category (e.g., allowing Search) will determine what is allowed to access a website.

To balance this change, we’re opening up the process of becoming a Verified bot, and making it more transparent, too. To "Verify" a bot, a bot operator needs to show two things: that you represent yourself honestly, and you don't abuse the access that honesty earns. And to make this easier on bot operators, we’re currently building management tools for bot operators to better ensure they are accurately represented by Cloudflare’s classification system (to be announced in the near future). 

BLOG-3337 4

A preview screenshot of the upcoming platform built directly for bot operators who are part of or want to be a part of BotBase, the next generation of the Cloudflare Bots Directory.

Experimenting with transitive trust

One more piece: The bot (or agent) at your door increasingly isn't run by the company that built it. A platform like Cloudflare’s Developer Platform runs automations for thousands of different operators at once, ranging from enterprises to a developer you've never heard of. You might trust Stripe, but you don't necessarily trust everyone who wired Stripe's tools into a weekend project.

We call the case of (site owner → bot owning company → end user) a matter of transitive trust, and we're proposing to utilize the existing Forwarded header as defined in RFC 7239 that rides along with the request and allows “proxy components to disclose information lost in the proxying process.” 

This is similar to what X-Forwarded-For does for IP addresses, or X-Forwarded-Host does to preserve the original Host header. So when a website owner says, "Allow this operator," that preference will hold, whether the operator comes to you directly or through three layers of intermediaries that are trusted. More details can be found in our documentation, with a brief example to show the format below.

Forwarded: for="openai"

Adding the extension with content-use discussed above, the header addition would look something like the below, specifying how the operator says they will use the content they access:

Forwarded: for="openai";use="reference"

This also lines up the incentive model we want to foster. Losing trusted status across the more than 20% of web domains that sit behind Cloudflare is a deterrent with teeth. Trust becomes something you can carry with you, and something you can lose.

However, as bot traffic blends with human traffic, it’s possible that this system of transitive trust doesn’t carry beyond the users who can afford to be identifiable. The measures we are proposing today help to convey trust, but they won’t fit the entire web for all time. Small sources of traffic need privacy, and companies that want to preserve their own privacy commitments should be able to explore fair building blocks for the future of an agentic Internet, such as private rate limiting.

Set your terms today

These are small changes that move in the same direction: site owners get more control over who uses their content, and how. We believe the new defaults we discussed today and will soon implement are ones that encourage transparency and are more reflective of where the world is going.

Of course, the ebbs and flows of the web will continue shifting under us, and we'll keep adjusting with it. But the direction won't change, because it's the one Cloudflare started with: a web ecosystem built around trust. Where the people who make things can decide how they're used — and one where being honest about what you do earns you more access, not less.

These new options to manage AI traffic are live now, and can be configured by all existing customers in their zone Settings. Not on Cloudflare yet? Start for free to set the traffic controls that you want today.

Happy Content Independence Day.

BLOG-3337 5

The Daily Front Page 4 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Debian’s LLM Debate
article

LLM Usage in Debian: Three Proposals

by zdw·▲ 208 points·208 comments·debian.org ↗
General Resolution: LLM usage in Debian.

Time Line

Discussion Period: 2026-07-24

Proposal A Proposer

Matthias Geiger [werdahias@debian.org] [text of proposal]

Proposal A Seconds

  1. Johannes Schauer Marin Rodrigues [josch@debian.org] [mail]
  2. Antoine Le Gonidec [vv221@debian.org] [mail]
  3. Simon Richter [sjr@debian.org] [mail]
  4. David Bremner [bremner@debian.org] [mail]
  5. Pierre-Elliott Bécue [peb@debian.org] [mail]
  6. Ian Jackson [iwj@debian.org] [mail]
  7. Amin Bandali [bandali@debian.org] [mail]
  8. Thorsten Glaser [tg@debian.org] [mail]

Proposal A

Choice 1

Preamble

This proposal aims to expressly forbid any contributions to Debian written with the use or assistance of large language models (LLMs) or other generative AI tools.

The scope of this GR is (non-exhaustive):

  • Debian source packages
  • Official Debian project software, such as lintian
  • Debian web resources
  • Documentation and translations added by Debian contributors
  • Official communication from Debian

It does not include:

  • Upstream projects using LLMs for development
  • AI-related software
  • Upstream patches/security fixes etc.

Rationale

Debian has a well-earned reputation for stability. This stability is crucial to Debian's position in the free software ecosystem. It is our belief that widespread LLM usage comes from the "move fast, and break things" attitude that, while common in many parts of this industry, is contrary to what makes Debian Debian, and is inappropriate for Debian contributors.

In practical terms, LLM usage raises the following concerns:

1. Copyright

LLM output has very unclear legal status: it may be possible to copyright on its own merits, or not; it may be affected by all of the licenses and copyrights in the training data, or not. Debian Policy and the DFSG require absolute clarity for licensing and copyright[1][2]. Software and other contributions written conventionally by humans with unclear copyright or license status are not allowed in Debian; LLM output should not have a special exception to this.

2. Quality

LLM output has many well-known problems with accuracy.[3][4][5] A LLM can never "know" if its output is correct since it merely produces syntactically likely combinations of the training data. In some environments this is good enough. In Debian, it is not. For instance, in packaging, each Debian source package is unique. Since packaging syntax and best practices have changed over time, a LLM-produced package will have a mixture of contents spanning the age of the archive, with watch files that do not work, overrides out of context, imaginary copyright, and will generally be unfit for upload. A seasoned Debian contributor with packaging expertise may find some limited usefulness here, but a new contributor cannot, and would not know how to fix it. These same quality and accuracy concerns apply clearly to all of the areas listed in the scope of this proposal above. If Debian were a closed organization comprising only domain experts who never leave, this might not be an issue; however,

3. Community

Debian is a project that is more than just code: it is a community built on shared interests in free software and solving technical problems. Debian intentionally grows this community through many means, and new contributors are always encouraged to join. Allowing LLM contributions breaks this. New contributors submitting LLM output for review places an unnecessary strain on the reviewer, which can lead to burnout. Furthermore, LLM-dependent new contributors do not actually learn and understand the details of Debian packaging or processes, so they cannot come to replace a former burned out DD.

4. Ethics

LLM companies directly hurt the free software community as whole by scraping the whole web for training data without any regard for license, copyright, or even established conventions such as robots.txt.[6] This has had a major negative impact on Debian's public web resources, effectively a large scale and perpetual Denial of Service attack on sites that many users rely on. As a consequence parts of our infrastructure were not reachable at all, and JS-based checks had to be enabled. Many other projects were similarly affected. Furthermore, LLM training consumes a staggering amount of resources[7], and the user verification systems that we have been forced to implement as protection waste resources as well. This is blatant disregard for the internet as a public resource, wastes system administrator time, and although individual LLM sessions do not directly use massive resources or DoS the public web, the fact that they can be used at all is a direct result of these unethical behaviours by the LLM companies.

Debian has a Social Contract. [8] Our priorities are our users and free software. Debian is Stable. [9] Users and organizations choose Debian because it is reliable and secure.

Debian is not here to generate as much code as possible requiring manual review by a shrinking number of human volunteers, or to package every piece of software, or to rush new features, but these are what LLMs are used for.

In conclusion, allowing LLM contributions is contrary to the social contract and the common cause of creating a free operating system with a focus on quality and stability.

Proposal

In the interest of not eroding Debian's reputation or further damaging the community, LLM-assisted contributions should be prohibited from inclusion in Debian.

Though our position is that LLM contributions are contrary to documents already ratified by Debian, in order to remove all doubt, we propose the following addition to the Social Contract:

6. Works Created through the use of Large Language Models (LLMs)

We will not
allow direct contributions to Debian written with the use or assistance of large
language models (LLMs) or other generative AI tools. Direct contributions are
defined as packaging, native Debian software like lintian, documentation and
translations written by Debian contributors, and official Debian web resources,
etc. Other categories such as upstream projects written with LLM assistance may
be included at a later date. This ensures that Debian remains a stable, trusted,
and reliable operating system, and protects the interests of the Debian
volunteers who make it possible.

Possible Issues

Other projects exploring similar decisions have elicited a common reply: "How will you enforce a ban on LLM contributions?" While enforcement could be a challenge, this is a statement of intent by the Debian community, and we trust this community to adhere to it in good faith.

Citations

[1] https://www.debian.org/doc/debian-policy/ch-archive.html#copyright-considerations
[2] https://www.debian.org/social\_contract#guidelines
[3] https://web.archive.org/web/20240614004123/https://news.northeastern.edu/2023/11/10/ai-chatbot-hallucinations/
[4] https://web.archive.org/web/20250328154700/https://transformer-circuits.pub/2025/attribution-graphs/biology.html#dives-cot
[5] https://www.marketwatch.com/story/openais-sam-altman-tells-salesforces-marc-benioff-that-ai-hallucinations-are-more-feature-than-bug-1c035c52
[6] https://lwn.net/Articles/1008897/
[7] https://tech-insider.org/ai-data-center-power-crisis-2026/
[8] https://www.debian.org/social\_contract
[9] https://www.debian.org/doc/manuals/debian-reference/pr01.en.html#\_what\_is\_debian

Disclaimers

  • Citations are for background information only and do not reflect an endorsement of specific websites.
  • Some ideas and wording were derived from the sources below.

Sources

GNOME discussion: https://discourse.gnome.org/t/loupe-no-longer-allows-generative-ai-contributions/27327
(CC0) Gentoo AI policy: https://wiki.gentoo.org/wiki/Project:Council/AI\_policy
Codeberg AI policy: https://codeberg.org/Codeberg/org/pulls/1253#issuecomment-19820434

This document was written by Matthias Geiger and Jesse Rhodes with input from Sledge and josch, organically and without language model assistance.

Proposal B Proposer

Lucas Nussbaum [lucas@debian.org] [text of proposal] [text of amendment] [text of amendment]

Proposal B Seconds

  1. Andrey Rahmatullin [wrar@debian.org] [mail]
  2. Christian Kastner [ckk@debian.org] [mail]
  3. Anton Gladky [gladk@debian.org] [mail]
  4. Stefano Zacchiroli [zack@debian.org] [mail]
  5. Simon Quigley [tsimonq2@debian.org] [mail]
  6. Soren Stoutner [soren@debian.org] [mail]
  7. Julian Andres Klode [jak@debian.org] [mail]
  8. Andreas Tille [tille@debian.org] [mail]
  9. Philipp Kern pkern@debian.org] [mail]

Proposal B

Choice 2

Allow AI-Assisted Contributions

Using its power under Constitution section 4.1 (5), the project issues the following statement describing its current position on AI-assisted contributions. This statement describes the position of the project at the time it is adopted. That position may evolve as time passes without the need to resort to future general resolutions. The GR process remains available if the project needs a decision and cannot come to a consensus.

The Debian project recognizes that AI-assisted contributions raise many concerns, e.g. about the technical quality and maintainability of such contributions, and their legal status. AI itself also raises additional concerns, about its impact on society at large, on the IT industry and on Free Software; about its environmental impact; and the aggressive or non-compliant practices of AI scrapers.

Nevertheless, many Debian contributors find AI tools helpful when contributing to Debian, and ultimately for improving Debian.

Given both the benefits and risks of AI assistance, and the controversial discussions within the community, the Debian project finds it necessary to clarify its position on AI-assisted contributions and establish clear guidelines.

The Debian project allows AI-assisted contributions (partially or fully generated by an LLM), provided the following conditions are met:

  1. Tooling Legal Compatibility: Contributors should ensure that the terms and conditions of the generative AI tool do not impose contractual restrictions that conflict with the distribution, modification, or use of the output in the context of Debian.
  2. Licensing and Attribution: If any pre-existing copyrighted materials (including pre-existing code licensed as free software) authored or owned by third parties are included in the AI tool’s output, prior to contributing such output to the project, the contributor should verify they have the right to submit it under the relevant open source license.
  3. Accountability: Contributors assume full responsibility for their contributions, including vouching for the technical merit, security, license compliance, and utility of their submissions. The contributor remains solely accountable for the entirety of these contributions. Contributors should fully understand the proposed changes and be prepared to justify them.
  4. Disclosure: When a significant portion of a contribution is generated or substantially assisted by a tool, contributors should disclose the use of the tool, making it clearly visible to the intended audience. This covers all forms of contribution, including code, mailing list posts, and bug discussions. The form of the disclosure is left to the contributor; one convenient option for commits is a Git trailer such as Generated-By: or Assisted-By:.
  5. Prior Discussion of Bulk or Automated Changes: Similarly to the mass-bug filing process (Developers Reference section 7.1.1), contributors should discuss their intention before submitting bulk or autonomously generated contributions. Any such automated process should be overseen by a human who remains accountable for its behavior and output.
  6. Confidentiality and Privacy: Contributors must not use generative AI tools that transmit data to untrusted providers with non-public or sensitive project information (such as embargoed security reports or private communication), as this may lead to the unintended disclosure of confidential data.

Proposal C Proposer

Ian Jackson [iwj@debian.org] [text of proposal]

Proposal C Seconds

  1. Matthias Geiger [werdahias@debian.org] [mail]
  2. Andrea Pappacoda [tachi@debian.org] [mail]
  3. Simon Richter [sjr@debian.org] [mail]
  4. Antoine Le Gonidec [vv221@debian.org] [mail]
  5. Amin Bandali [bandali@debian.org] [mail]
  6. Bill Blough [bblough@debian.org] [mail]
  7. Enrico Zini [enrico@debian.org] [mail]
  8. Sean Whitton [spwhitton@debian.org] [mail]

Proposal C

Choice 3

Summary: Reject LLMs (generative "AI") as far as practical

BACKGROUND

LLMs have many serious problems, including: undermining the mechanisms of free software community building; environmental damage; exploitation of authors; disruption to open web hosting by aggressive scraping; generation and promulgation of bullshit; polluting the information commons; hazards to users' mental health; economic bubbles; distortion of the computer hardware market; fraud; ownership by horrible people and companies; and so on. Ethical and safe use of this technology is almost impossible.

Ideally, LLM output should not form any part of software that we rely on, nor should LLM output ever take the place of human-written prose.

Unfortunately some of the wider software world, including many of our upstreams, take a different view. Therefore a complete ban on LLM output as part of Debian is currently impractical.

REQUESTS

1. We request that all contributors to Debian avoid the use of LLMs in their Debian work.

2. We request that all decisionmakers within Debian discourage LLM use as much as practical. Practicality is a judgement call and we recognise that it will involve uncomfortable compromises.

3. We request that everyone, even outside Debian, should refrain from using this technology. In particular, the Free Software and Open Source communities should reject LLMs. We recognise that not everyone will heed this call.

REQUIREMENTS (SUPPLEMENT TO THE CODE OF CONDUCT)

4. Within Debian, messages to humans (including for example bug reports, mailing list messages, discussions on Salsa, and blog posts on Planet Debian) must be drafted solely by humans without LLM assistance.

5. Any use of LLMs for Debian work must be disclosed.

6. Individual projects and maintainers may ban LLM contributions completely. Such bans (including by upstream projects) must be respected.

7. Violations of these requirements should be treated as violations of the Code of Conduct and should result in swift but proportionate disciplinary action.

8. Any contributor who feels they cannot write in English without assistance, may write in their native language, and expect readers to use translation tools of their choice. In that case a human-written English summary would be very welcome but is not required. In any case we promise not to shame anyone for any linguistic mistakes.

Proposal D Proposer

Pierre-Elliott Bécue [peb@debian.org] [text of proposal]

Proposal D Seconds

  1. Russ Allbery [rra@debian.org] [mail]
  2. Johannes Schauer Marin Rodrigues [josch@debian.org] [mail]
  3. Jonathan Carter [jcc@debian.org] [mail]
  4. Gunnar Wolf [gwolf@debian.org] [mail]
  5. Tiago Bortoletto Vaz [tiago@debian.org] [mail]
  6. Jeremy Sowden [azazel@debian.org] [mail]

Proposal D

Choice 4

Accept AI contributions for Debian specific work

Debian as a project does not endorse or recommend the use of generative AI assistants for software development, as it raises multiple concerns about ethics, legality, copyright, etc.

Nevertheless, Debian acknowledges that these practices are already in use and here to stay. Rather than banning their use, which seems counter-productive and unenforceable, the project chooses to place responsibility on contributors and therefore defines the following guidelines.

These apply exclusively to code and work done specifically for the Debian project (Debian websites, applications, resources, packages, etc.). They do not apply to any upstream work. In what follows, "work" refers to the contributions done specifically for the Debian project.

  • All code and work assisted by a generative AI agent or tool must comply with the DFSG.

  • The submitter is solely responsible for the submitted work and:

    • they sufficiently evaluated and properly understand the work they intend to submit, and are able to explain and defend it;
    • they put any potential Signed-off-by tag and GPG signatures on the contributions they send to the Debian infrastructure (package, commit, mail, …) themselves;
    • any content uploaded that would end up in production on Debian infrastructure (main git branch, package upload) has been submitted by them explicitely.
  • Work assisted by a generative AI agent or tool should be marked as such in the adapted place (commit message, changelog, …). Some lightweight generative tools, such as tab-completion in Copilot, may be used without the contributor realising they rely on generative AI models; we therefore trust submitters to assess when this rule applies. When in doubt, add such marking;

  • No cloud-based AI shall be used when the data transmitted could either be sensitive to the project (personal data, information under embargo, …) or not public (debian-private discussions, …).


Debian Project Secretary


The Daily Front Page 5 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Training Tomorrow’s Lawyers
article

Rethinking legal education in the AI era

by jjwiseman·▲ 148 points·91 comments·law.uchicago.edu ↗
Rethinking Legal Education in the AI Era.

Overview

Artificial Intelligence is already impacting higher education and the legal profession, and the pace of change appears only to be accelerating. It is thus critical for us to use this moment to carefully reflect on how legal education should adapt. This memo describes the approach we are taking to legal education in the AI era.

The University of Chicago Law School has long been committed to providing our students with the most rigorous legal education possible. This commitment manifests itself through our faculty’s dedication to teaching, our culture of challenging ideas through Socratic questioning and debate, and our grading policies that motivate students to engage with the material and that convey informative assessments of student performance to prospective employers. With AI disrupting higher education, our commitment to rigorous legal education also must mean openness to even rapid adaptation.

A willingness to rethink our practices is consistent with our law school’s long history of innovation. We were the first law school to conceive of legal education as a graduate-level program and award the Juris Doctor degree. We also made pioneering contributions to interdisciplinary legal education that incorporates insights from the humanities, social sciences, and other academic disciplines, and to the creation of legal aid clinics that brought students into real-world cases.

We began the process of reflecting on how we should adjust our teaching in response to AI shortly after OpenAI publicly released ChatGPT at the end of 2022. Our law school formed an AI committee in early 2023 and began releasing policies and guidance on the use of AI. Since then, we have added an AI module into our first-year legal research and writing program, introduced several upper-level courses on AI and the law, founded an AI Lab to teach our students how to develop AI tools to help improve access to justice, and negotiated licenses with leading AI companies so that our students, faculty, and staff have access to the resources used by practicing lawyers.

Over the last year, we embarked on a more ambitious effort to reflect on how we should adapt our curriculum and policies in response to AI. This effort included extensive consultation with our community, including alumni, leaders of law firms, business leaders, legal technology executives, and law firm associates, as well as internal stakeholders at the Law School, including our faculty, staff, and students. We also surveyed the emerging academic and professional literature on how AI is changing legal practice and student learning, and how law schools can and should respond. And we have had steadfast support from our University of Chicago leadership, who have championed a skeptical, ethical, and ambitious approach to AI.

The feedback we have received throughout this process has been consistent: We need to ensure that our students actually learn to think critically, strategically, and independently without relying on AI; but we also must face the reality that AI tools are already widely available to our students, and our graduates will be expected to be prepared to use them in legal practice.

Our Strategic Vision

Based on what we learned from our consultations this year and our experience over the past three years, we have developed a strategic vision of how we should adapt legal education to the AI era. That vision has three themes: 

  1. Developing AI-resilient pedagogy and assessment; 
  2. Elevating the “essential human” skills that distinguish excellent lawyers; and 
  3. Teaching the responsible, effective, and ethical use of AI.

 

First, rather than attempting to ban AI or to ignore its risks to learning, our pedagogy and assessment should be designed to ensure our students learn how to think critically and solve legal problems with sound professional judgment. We thus need to ensure that our students do not rely on AI-provided shortcuts that help them produce easy answers but stunt intellectual growth. This requires rethinking the technology we allow in our classrooms, the tasks we assign our students, and the way we assess our students’ performance.

This requires developing AI-resilient pedagogy, by which we mean modes of classroom interaction and performance evaluation that reward students’ effortful and sustained engagement with the material, and discourage the offloading of work to AI tools. AI-resilient pedagogy does not mean trying to prevent all student use of AI. We do not want to deter uses of AI that can increase students’ effort and engagement, such as asking AI to clarify background concepts while reading before class or asking AI to generate practice problems while studying.

Second, AI may transform the legal profession, but many aspects of legal practice are likely to remain the domain of humans, not merely because humans are good at them, but because clients, employers, judges, and society will want humans to perform them. Among others, these aspects of legal practice are likely to include oral advocacy, strategic judgment, critical thinking, and developing and maintaining relationships with clients and stakeholders. To be sure, there are ways in which AI can assist humans with these tasks. But legal education should renew its focus on training students for the aspects of legal practice for which humans are likely to remain essential.

Third, AI is already a pervasive part of legal practice and will become more so. It is simply unrealistic to think that students and lawyers will not use AI. But legal technology is changing rapidly, and there is no guarantee that the specific AI tools or techniques that are ascendant today will be useful when current students enter practice. Thus, AI skills training requires more than producing students who can use the tools that are currently part of legal practice. Law schools must give students the analytical skills and theoretical toolkit to adapt as technologies change. This is how we will ensure that our students learn how to use AI responsibly, effectively, and ethically.

We believe that these three components of our strategic vision are consistent with the Law School’s longstanding commitment to rigorous legal training and with emerging scholarship on the role of AI in education. They are also consistent with the University of Chicago’s broader goal of ensuring that we teach our students to think with, without, and about AI.

Putting the Vision into Practice

The Law School is implementing this vision through policies that apply to the major elements of the law school curriculum: required 1L core courses, 1L legal research and writing, elective courses, upper-level writing requirements, and clinical education. We outline these policies below.

Required 1L Core Courses. The 1L year is a crucial, formative period for law students. It lays the foundation for the development of critical thinking, legal writing skills, and strategic judgment throughout law school and in professional life. During the 1L year, the value of effortful struggle, even with concepts that are second nature to experienced lawyers, is paramount. Student expertise at judging the quality of AI output is at its nadir. The need for faculty to work together to create a consistent, AI-resilient approach to pedagogy is especially acute in this setting. For this reason, the Law School is adopting rules that set consistent norms for AI-resilient teaching and assessment across all 1L sections in all 1L core courses (Civil Procedure, Torts, Elements of the Law, Contracts, Property, Criminal Law, Constitutional Law, Statutory Interpretation, and Transactional Lawyering).

We will be piloting a coordinated approach to classroom and examination policies for the core 1L curriculum during the 2026–2027 academic year. Across all 1L sections, we will prohibit the use of electronic devices such as laptops, tablets, and phones in the classroom. There will be some limited exceptions to this policy. For instance, professors can designate classroom “scribes” who can use electronic devices to take notes for the class, professors can authorize electronic device use for specific tech-enabled activities (such as interactive in-class polling), and we will ensure that disabilities are accommodated in conformity with applicable law, as appropriate for the program of study. Additionally, examinations will be in-class without access to the internet, electronic files, or apps. And most of all, we will continue our longstanding tradition of emphasizing the Socratic Method as part of these courses. This coordinated approach reflects our experience, and an emerging scholarly consensus, that active, in-person engagement is conducive to learning. Reliance on devices to take notes or assist with answering questions tends to inhibit reflection and reasoning.

1L Legal Research and Writing. We are taking a different approach to 1L Legal Research and Writing (LRW). Many (if not most) students will spend their 1L summers in professional environments where they will be expected to use AI tools for research and writing tasks. Thus, the LRW curriculum must also include instruction in the responsible, effective, and ethical use of AI. At the same time, even AI skills training must itself be AI-resilient. By the end of their 1L year, our students should have the ability to review, assess, and improve the output of AI tools. Developing these skills requires human interaction as much as it requires AI tools. Hands-on work with AI tools in classroom settings and individualized feedback on writing must be components of instruction on the use of AI in legal research and writing.

We will thus be piloting a new structure to the LRW curriculum during the 2026–2027 academic year. Our approach will treat writing without AI as the foundation and will layer writing with AI onto it. Throughout the year, students will write without AI, while also using AI for research, revision, iterating on drafts, and preparation for oral argument. Students and their instructors will review together both their writing and their use of AI. In this way, students will develop their own writing skills independent of generative AI tools while also developing their ability to supervise AI and critique its output.

Elective Courses. As students progress from their foundational training in required 1L classes to elective courses, the need for coordination across sections abates, and the benefits of heterogeneity increase. Thus, in the upper-level curriculum and the elective courses that 1Ls take in the Spring, the goal of our AI policies shifts to providing guidance and fostering experimentation. For these courses, the use of the Socratic Method, no-device policies, and in-class, no-access exams will remain, but as default rules rather than required policies.1 

In all elective courses, we will encourage instructors to experiment with pedagogy. Some methods of teaching and evaluation have enhanced value as AI-resilient methods. These include modes of formative assessment such as midterms, group projects, oral presentations, and peer-to-peer feedback. At the same time, AI itself creates opportunities for new forms of teaching and assessment. Our faculty have already begun experimenting with tools such as custom chatbots that serve as study aids, AI-generated practice problems, and the like. None of these particular modes of teaching and evaluation is required, but we will support experimentation with modes such as these.

Finally, as students advance in their legal training, the need for classes that explore responsible, effective, and ethical use of AI grows. To this end, we have already added a number of courses that explicitly focus on the use of AI (and even the creation of AI tools for legal work). These courses are all offered as electives, so that students can select how many AI-focused classes, and which of these classes, they wish to take. We plan to continue to grow our upper-level offerings on AI.

Upper-Level Writing Requirements. Writing is crucial to lawyering. Not only do most lawyers do a considerable amount of writing, but the practice of writing cultivates the practice of deep, sustained, and critical thought. Yet the primary modes of writing in the upper-level curriculum—research papers and reaction papers—are under stress in a world in which AI can produce plausible and substantial academic papers without the kind of human input that paper-writing is intended to involve.

This is a particularly difficult challenge. Some responses, such as requiring writing to take place in supervised, in-class settings, address the need to cultivate practice while ensuring original human effort. Yet such requirements lose an important aspect of the exercise of writing, which is sustained and independent effort across hours, days, and weeks to create a significant piece of work. This is a valuable formative exercise for thinkers and lawyers, and we cannot wholly abandon it in favor of writing methods that are easier to administer in AI-resilient ways.

Thus, we are developing an approach that introduces elements of AI-resilient design while sustaining the project of training students to undertake ambitious and independent writing projects. The rule-based element is a change to our “SRP” (substantial research paper) requirements, which will take effect beginning with this year’s class of rising 2Ls. Writing an SRP is a requirement for completion of a JD degree, and the criteria for an SRP already include (roughly) substantial length, independent research, original ideas, incorporation of feedback from faculty, and iteration across drafts. These requirements incorporate elements of serious academic and legal writing. We will be adding one additional requirement, which is that all students will be required to engage in an oral discussion of their SRP with their supervising professor, in an in-person setting. This discussion will occur after a complete draft (or final version) of the paper has been submitted to the professor. The discussion could take place one-on-one, or as a class presentation in the style of an academic workshop. Either way, the oral exchange will involve the student answering questions that probe the reasoning of the paper and the implications of its arguments.

The motivation for this new requirement is twofold. First, this requirement makes the SRP more AI-resilient by providing a test of a student’s thinking about what they have written in a setting where they cannot lean on technology. It does so without sacrificing any of the unsupervised, independent effort that must be a part of serious, sustained writing.

Second, this requirement imparts valuable skill-building for both legal practice and academic life that is justified on its own merits, without regard to AI. We are training students for a profession where they will be called upon to explain and defend their ideas in person and in real time, whether in the courtroom, during negotiations, when counseling clients, or when working in collaboration with other lawyers. Indeed, one of the hallmarks of intellectual life at the University of Chicago Law School is our robust workshop culture, in which faculty regularly present their own research paper drafts and address questions, suggestions, and criticisms from colleagues. An oral discussion component to the SRP is a way to formalize a piece of that workshop culture for all students.

Experimentation is the other element of our approach to upper-level writing assignments. For all upper-level writing, we encourage faculty to experiment with different modes of structuring writing assignments in AI-resilient ways. Some examples could include: 

• In-class student workshops of their papers; 
• Writing reaction papers or portions of longer papers in supervised, in-class settings; 
• Group presentations or panel discussions by students with related reaction paper topics; and 
• Students, individually or in groups, leading portions of class discussion relevant to their reaction paper topics; and 
• One-on-one discussion of a research paper with the professor outside of class. 

Many other possibilities exist, of course, and the point of experimentation is to find the options that best ensure our students learn writing and critical thinking skills. 

Clinical Education. The role that clinical education plays in preparing students for practice will be even more important in the AI era. Client needs and expectations will create pressure for graduates to be able to immediately use AI responsibly, effectively, and ethically. As a practical matter, this means that for many graduates, law school clinics are the best opportunity to receive close and careful supervision of their use of AI tools. 

Further, clinical work often goes to the heart of what is essentially human about lawyering: advocating to judges, juries, and policy makers; understanding and being present to clients; strategizing about both the doctrinal and the practical aspects of a case; and developing rapport with counterparties and adversaries. These are all skills that our students learn from our clinical faculty. Thus, clinics are a crucial site for learning with, without, and about AI. 

To these ends, we have been obtaining access to AI tools for our clinics. These include general-purpose legal AI tools and tools that are specifically designed for transactional work, immigration work, and litigation discovery. We are also in the process of procuring additional tools. Further, the clinics are each developing their own policies, tailored to their practice areas, for appropriate use of AI and to safeguard against AI-created errors in court filings and other work product. As these tools and policies are implemented, the goal is to ensure that all students in clinics are working both with AI and without AI for real clients in real practice settings. 

Final Thoughts

Beyond these changes to our curriculum, three additional principles will inform our thinking.

First, we will strive to ensure that our AI policies are transparent, explicit, and explained. To that end, each instructor must state their AI policy explicitly in the course syllabus and describe the policy in class. And, as we confront new circumstances that require changes to our policies, we will communicate those clearly to our students.

Second, we will try to stay up to date on new AI tools and the best practices for how to use them. We have already been working with alumni and other employers to understand how legal practice is incorporating AI, and we have been partnering with technology firms to secure access for our students, faculty, and staff to the AI tools that leading firms are using. We have also been having regular conversations among our faculty to share information about the use of AI. We are committed to continuing to do what we can to stay current in this time of rapid change. 

Finally, we will regularly reconsider these policies with ongoing input from our faculty, students, and alumni. We recognize that no statement of an AI strategy or vision can be final. Technology is changing too fast. Thus, all the changes we are currently making will be subject to review, reconsideration, and revision as we learn more and as both technology and the practice of law evolve.


1We will continue to offer courses that are cross-listed with other units, and students will still be allowed to enroll in courses offered by other units. Depending on the rules of the other units, these courses may have different policies than the electives offered solely within the Law School. We will work with instructors and other units to navigate any conflicts that may arise in the policies of different units.

The Daily Front Page 6 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Jobs in the Age of Agents
article

What is happening to jobs? Separating AI hype from reality

by pod_krad·▲ 258 points·338 comments·siepr.stanford.edu ↗
AI’s effects on overall employment is likely small.

Key takeaways

  • AI’s effects on overall employment is likely small, though a tough job market for new graduates may be partly due to AI.
  • AI’s impact on worker productivity is mixed but generally positive.
  • Firm adoption has accelerated but unevenly across the economy.
  • Early evidence is hardly the last word on the future of work in an AI world.

View this Policy Brief

Advances in AI models have sparked fears that rapid disruption of labor markets is imminent, if not already underway. A steady drumbeat of media articles has forecast a grim future for white-collar work due to AI.[1] Fears of an “AI jobs apocalypse” are often amplified by AI leaders themselves. For example, Dario Amodei, CEO of Anthropic, has predicted that AI could wipe out half of white-collar jobs and push unemployment to 20 percent. A labor market upheaval of this magnitude would cause enormous suffering for many households and pose a significant challenge for policymakers.

While the public has been debating AI’s potential impact on firms and their workers, research and data have been catching up. Our goal in this brief is to synthesize the fast-growing body of research on AI’s impact for policymakers and others eager to understand how AI is affecting the labor market right now. For ease of exposition, we organize this empirical evidence into a set of stylized facts, as follows:

  1. AI’s impact on aggregate employment is likely small right now.
  2. A tough market for recent graduates may be partly due to AI.
  3. AI’s impact on worker productivity is mixed but generally positive.
  4. Firm adoption has accelerated but unevenly across the economy.
  5. xEarly evidence is hardly the last word on AI’s impacts.

We will now explore each of these points in greater depth.

AI’s impact on labor market conditions is likely small right now.

No one can predict the future, but there is little evidence that AI is causing significant job losses right now. Unemployment among workers in occupations most exposed to AI-driven disruption is rising, but not faster than among those least exposed.[2] As shown in Figure 1, the unemployment rate for the top quintile of AI-exposed workers has risen by 0.77 percentage points since 2022, while the unemployment rate for the least-exposed workers rose slightly more, by 0.85 percentage points over the same period. These aggregate trends suggest a broadly softening labor market, rather than one characterized by AI-driven job losses.

Figure 1: Unemployment rate by AI-exposure quintile, 2015-2026 (quarterly)

Source: IPUMS-CPS data. Updates and extends Eckhardt and Goldschlag (2025) using replication code. 
Note: AI exposure from Felten, Raj, and Seamans (2021).[3] Quintile 1 = least exposed, 5 = most exposed; dashed line is the U.S. total.

There is also little evidence of AI depressing employment or job postings in the most highly exposed occupations. Employment trends in occupations with high exposure to AI are fairly stable.[4] While employment growth in coding-heavy occupations has slowed somewhat, it remains positive.[5] There is no evidence that AI adoption has negatively impacted firms’ job postings.[6] Indeed, online job postings for software developers — a very highly exposed occupation — have been growing faster than for other occupations over the last year.[7] Among firms that adopted enterprise AI, employment grew by 10 percent in the two years following adoption, an effect driven by firms with the highest per capita AI spending.[8]

What about the companies announcing layoffs, increasingly citing AI as a driving factor?[9] Both industry leaders and labor economists express some healthy skepticism about these claims.[10] While some narrow layoffs may be connected to AI-related automation, others appear driven by a desire to free up cash flow for AI investments or to reduce headcount after pandemic-era over-hiring. Human resource executives say the impacts of AI are more evident in role consolidation and hiring avoidance in roles where AI can automate many tasks.[11]

This doesn’t mean AI isn't having any negative impact on some workers; rather, the effects so far are more nuanced than an imminent “AI jobs apocalypse” would suggest. AI could be creating pockets of disruption that aren’t easily visible in aggregate economic data. In particular, there is some evidence that AI may be negatively affecting demand for young white-collar workers, as we discuss next.

A tough labor market for young workers may be partly due to AI

Recent graduates are facing the most challenging job market in years, with unemployment rates for new grads reaching 5.6 percent in early 2026, up 1.6 percentage points from three years earlier.[12] This rise has fueled concerns that AI is replacing many of the jobs recent graduates once sought. Junior roles often involve routine research, analysis, and writing tasks that can now largely be done with AI. Consistent with this intuition is empirical evidence that AI may be dampening demand for new hires.

In a widely discussed paper, Brynjolfsson, Chandar, and Chen report a notable decline in employment among early-career workers in AI-exposed occupations, notably software developers and customer service representatives, since ChatGPT’s launch in 2022. As shown in Figure 2, by contrast, employment among older workers in those same occupations remained relatively stable or continued to grow. The authors liken these young workers to “canaries in the coal mine,” the first to experience labor market disruption from AI.[13] Other researchers have since identified similar negative effects on the hiring of young AI-exposed workers in the U.S. and the U.K., beginning in 2022.[14]

Figure 2: Employment by age in two AI-exposed occupations, 2021–2026

(a) Customer service representatives

Source: Stanford Digital Economy Lab and ADP Research. 
Note: Employment index by age group, from a five-year balanced sample of firms using ADP payroll services; 100 = each group's employment at the November 2022 launch of ChatGPT (Brynjolfsson, Chandar, and Chen 2025).

(b) Software developers

Source: Stanford Digital Economy Lab and ADP Research. 
Note: Employment index by age group, from a five-year balanced sample of firms using ADP payroll services; 100 = each group's employment at the November 2022 launch of ChatGPT (Brynjolfsson, Chandar, and Chen 2025).

The timing of this impact is somewhat surprising. AI model capabilities were very limited in 2022. Other factors may be at play, including rising interest rates, pandemic over-hiring, and remote work. For example, in response to rising inflation, the Federal Reserve aggressively hiked interest rates beginning in March 2022 — several months before ChatGPT’s public release the following November. Two new papers find that hiring in AI-exposed occupations began to decline after the Fed’s monetary policy shift but before ChatGPT’s debut.[15] Also, the rapid shift to remote work during the pandemic, which can slow on-the-job learning, erodes the value of hiring younger workers. New evidence suggests hiring in remote-friendly occupations began to skew toward more experienced workers after the pandemic.[16]

In response to concerns about these and other possible confounding effects, Brynjolfsson and coauthors added new controls. In these results, employment declines among entry-level workers are not notable until 2024.[17] It thus seems plausible that factors other than AI are driving declines in hiring young workers around 2022. However, by 2024, both AI adoption and model capabilities had advanced significantly, making the direct impacts of AI more plausible.

Hiring of entry-level workers in AI-exposed occupations clearly declined markedly around 2022. What is harder to determine is whether this decline constitutes clear evidence of AI’s impact on demand for young workers, now or in the future. Given other macroeconomic shocks to labor demand around this time, isolating AI’s impact is empirically challenging. This remains an open and active area of research.

The impact of AI on worker productivity is mixed but generally positive.

In experimental settings, generative AI tools — such as chatbots and coding tools — have often been found to disproportionately improve the performance of less experienced and poorer performing workers. In one such study, researchers analyzed the impact of a generative AI assistant on customer support agents in a large call center.[18] The assistant increased overall productivity by 15 percent, with gains highly concentrated among novice and less-skilled workers, who saw a 30 percent improvement in the number of issues resolved per hour. There was no performance improvement among highly skilled customer service agents, whose response quality fell slightly.

Other studies also show that AI tools generally speed up task completion, though the effects vary by task, context, and skill level. Figure 3 summarizes research findings on the impact of AI tools on speed across a variety of tasks. In software development, the use of GitHub Copilot — an AI tool that suggests code and functions — allowed tasks to be completed 56 percent faster, with gains concentrated among less-experienced programmers.[19] More modest impacts on software development were found in a separate paper, with effects ranging from 10 percent to 30 percent, depending on the firm where it was deployed.[20] In writing tasks, access to ChatGPT was found to reduce writing time for workers of all abilities and improve writing quality among low-ability writers.[21] Among young lawyers, the use of an AI tool was found to increase the speed of legal work, such as drafting contracts.[22] In medical settings, an assessment of AI scribes found some evidence that they increase the speed of medical note-taking, although the effects are small and occasional inaccuracies require physician oversight.[23]  

Figure 3: Experimental estimates of generative AI's effect on task speed

Source: Authors' compilation of randomized and field experiments. 
Note: Each point is the estimated effect of giving workers access to generative AI on the time taken to complete representative tasks, colored by the AI model generation used. Choi et al. (2023) is shown as a range (the span across tasks). Cui et al. (2025) and Brynjolfsson et al. (2023) are converted from output-per-hour measures, and Brynjolfsson et al. is a staggered-rollout, quasi-experiment rather than a randomized trial. Estimates are task-level and short-horizon, and tasks differ across studies, so magnitudes are not strictly comparable.

In many workplace settings, employees must determine when and how to deploy AI assistance, which complicates matters considerably. Dell’Acqua and coauthors document that AI’s capabilities are “jagged,” meaning performance can be strongly positive or negative depending on the specific task to which it is deployed.[24] Determining whether AI output is useful or needs further refinement can require skilled judgment. For example, when researchers deployed an AI assistant to help Kenyan entrepreneurs, less-skilled entrepreneurs posted lower revenues and profits from using an AI tool than those who grew their businesses without it.[25] Examining why, the researchers found that less-skilled entrepreneurs were more likely to act on generic advice from the AI tool that was detrimental to their specific situations, while better performers extracted suggestions more tailored to their business needs.

While AI can make some employees more individually productive, it can also narrow the range of creative ideas. The use of AI in creative writing tasks was found to increase the quality of stories produced by less-skilled writers, but AI-generated stories were much more similar to one another than human-generated ones.[26] Hao and coauthors found that, while scientists who adopt AI tools publish more papers, AI adoption reduces the total number of topics studied and scientists’ engagement with one another.[27]

AI clearly has the potential to improve worker performance across many settings. However, several frictions could prevent the gains observed in experimental studies from showing up in aggregate productivity measures yet. The share of tasks that AI can profitably speed up may be small relative to the total number of economic tasks.[28] Bottlenecks in processes where AI cannot currently assist could limit productivity gains for a time.[29] Also, when firms adopt new technologies, measured productivity can initially fall because firms need to divert resources to reorganize functions and make complementary investments.[30] While AI is likely to boost productivity eventually, given the current pace of firm adoption, it may not be visible in aggregate statistics yet.

Firm adoption has accelerated but unevenly across the economy.

If AI is truly a transformative technology, whether its sweeping effects occur over three years or 20 years fundamentally changes the policy problem. A swift transformation could displace many workers at once, while a slower one gives policymakers and workers more time to adapt. One of the most important indicators of the timing and distribution of AI’s potential economy-wide effects is the pace of firm adoption.

Figure 4: AI adoption at work, 2023-2026

Source: U.S. Census Bureau, Business Trends and Outlook Survey (BTOS); Stanford Digital Economy Lab/Real-Time Population Survey (Bick, Blandin & Deming, 2024); Federal Reserve Bank of Atlanta, Survey of Business Uncertainty; Ramp AI index. The Census revised the BTOS instrument in November 2025 to ask firms about all AI use at the firm; previously, the survey asked whether AI was used ‘to produce goods and services’. 
Note: Series are not directly comparable. BTOS = unweighted share of firms using AI (biweekly; the break marks the November 2025 change in question wording). Bick, Blandin, and Deming = the share of individuals using generative AI at work. Survey of Business Uncertainty = employment-weighted share of firms using any AI. Ramp = the share of businesses on its platform (not a representative sample) purchasing AI services.

Surveys of firms show a wide range of adoption rates, but all show rapid growth in the use of AI. Figure 4 shows several widely cited measures of firm adoption. The most conservative estimate of firm adoption is the Census Bureau’s Business Trends and Outlook Survey (BTOS), a nationally representative survey of businesses, which currently estimates that about 20 percent of firms use AI. Other measures of AI use in the workplace generally show higher rates of firm adoption; at the high end, a survey of executives in U.S. firms found that over 80 percent of employees use AI at work.[31] Ramp, a fintech expense management company, estimates that over 50 percent of its clients are spending money on AI tools and AI vendors. A nationally representative survey of households finds that over 40 percent of employed respondents use AI at work.

Variance in adoption rates across measures is largely due to differences in methodology and sample composition. Larger firms are more likely to adopt AI, so employment-weighted adoption rates are higher than simple firm counts would suggest. Ramp clearly states in the methodology for its AI index that firms using its platform are not a representative sample of businesses and are skewed toward technology firms, which have higher rates of AI adoption. In a review of the surveys on AI adoption, a recent Federal Reserve research report noted good reasons to favor the relatively low Census estimates, which use a national sampling frame, and showed how differences in adoption rates across surveys narrow considerably when each is weighted by employment.[32] 

The distribution of firms that are adopting AI tells us where AI’s effects will likely happen first. Currently, the survey evidence suggests AI adoption is concentrated among technology firms and information-intensive sectors like finance. The most common AI deployments are in sales and marketing, IT, strategy, finance, and accounting. Even among adopters, AI use remains narrow rather than broad, limited either to one or two business functions or to low-frequency use across a broader set of tasks, with comprehensive AI integration the exception rather than the rule.[33] A recent McKinsey survey found that most businesses remain in the experimentation and piloting phase of AI, with only a few large companies leading the way in scaling beyond pilots. [34]

AI adoption also appears to have had little effect on employment. In the Census data, only 5 percent of firms report any employment impact, with equal numbers reporting gains and losses. Eighty percent of executives surveyed by the Federal Reserve Bank of Atlanta said AI investments have yet to alter their headcount or improve productivity. A large-scale Danish study linking data on AI adoption by workers to firms found that AI adoption results in the restructuring of worker tasks and time, but that these changes are not yet affecting worker employment, hours or earnings.[35] A recent study found that among firms adopting AI, employment grew by 10 percent in the two years following adoption.[36]

Taken together, the data suggest firm AI adoption is moving quickly but unevenly throughout the economy, with many firms still in the experimentation stage and only the more advanced adopters deploying AI more broadly.

Conclusion: Existing evidence is hardly the last word.

Technological advances typically take years, even decades, to transform businesses and labor markets. In 1987, economist Robert Solow famously quipped, “You can see the computer age everywhere but the productivity statistics.” This lag between innovation and growth occurs because firms' adoption of new technologies is often uneven and slow. Firms, for instance, invested heavily in PC technology in the 1980s. But leveraging the advantages of the PC required additional investments — in enterprise software, worker retraining, and organizational restructuring. These adoption frictions were a major reason measurable economic returns from the computer revolution did not appear until the late 1990s.

It is always difficult to predict what will come next. In Silicon Valley, opinion is sharply divided between those who see AI as a “normal technology” — transformative but gradual, akin to past technologies — and those who predict world-altering effects as early as 2027. The strongest case for AI as a normal technology is history. Previous technological revolutions have eliminated or reduced labor demand in some occupations while simultaneously creating new jobs and industries. As a result, aggregate employment increased, even as the nature of work and the economy changed.

In contrast, those who believe that AI is fundamentally different point to the uniquely fast pace of AI adoption, AI’s disproportionate impact on cognitive work, and the cross-cutting nature of AI, threatening not just one occupation but much white-collar work. Regardless of which camp is correct, technological revolutions can be deeply disruptive to individuals, making it crucial to better understand — as quickly as possible — AI's evolving impact on the labor market and prepare potential policy responses.

Endnotes

[1] Meg Short, “Banks Lay the Groundwork for Mass Workforce Cuts as AI Takes Hold,” Bloomberg, June 7, 2026. Lindsay Ellis, Owen Tucker-Smith, and Allison Pohle, “Tens of Thousands of White-Collar Jobs Disappearing as AI Starts to Bite,” Wall Street Journal, October 28, 2025. Kevin Roose, “For Some Recent Graduates, the AI Job Apocalypse May Already Be Here,” The New York Times, May 30, 2025. Derek Thompson, “Something Alarming is Happening to the Job Market,” The Atlantic, April 30, 2025.

[2] Eckhardt, Sarah, and Nathan Goldschlag. AI and Jobs: The final word (until the next one). Economic Innovation Group, August 2025. Massenkoff, Maxim, and Peter McCrory. Labor Market Impacts of AI: A new measure and early evidence. Anthropic Research, March 2026.

[3] Felten, Edward, Manav Raj, and Robert Seamans. 2021. “Occupational, industry, and geographic exposure to artificial intelligence: A novel dataset and its potential uses.” Strategic Management Journal 42(12): 2195-2217.

[4] Gimbel, Martha, Molly Kinder, Joshua Kendall, and Maddie Lee. Evaluating the Impact of AI on the Labor Market: Current State of Affairs. Yale Budget Lab, October 2025. Chandar, Bharat. Tracking Employment Changes in AI-Exposed Jobs. SSRN, June 2025.

[5] Crane, Leland, and Paul Soto. “AI and Coder Employment: Compiling the Evidence.” FEDS Working Paper. No. 2026-018. Board of Governors of the Federal Reserve System, March 2026.

[6] Liu, Jessica, and Douglas Webber. “AI Adoption and Firms’ Job-Posting Behavior,” FEDS Notes, Board of Governors of the Federal Reserve System, March 27, 2026.

[7] Gallacher, Guillermo. “AI and Job Postings: From Job Destruction to Creation?” Indeed Hiring Lab, July 2026.

[8] Kharazian, Ara, Lisa Simon, and Ryan Stevens. A New Look at AI’s Impact on Jobs: Firm-Level AI Spending and Workforce Adjustment. Ramp Economics Lab, June 2026.

[9] Rebecca Bellan and Connie Loizos, “Every Major Tech Layoff in 2026 That Has Name-Checked AI.” TechCrunch, July 6, 2026.

[10]Lucy Hodgman, “Sam Altman thinks tech companies are ‘AI-washing’ their layoffs,” San Francisco Chronicle, February 21, 2026. Jake Angelo, “Marc Andreessen says AI layoffs are a farce: Companies are 75% overstaffed, and AI is the ‘silver bullet excuse’ to clean house,” Fortune, March 31, 2026. Satyam Mishra, “AI Overtakes All Other Reasons for Job Cuts as Layoffs Surge in 2026.” Outlook Business, June 7, 2026.

[11] Roy Maurer. “The AI Layoffs Narrative: Real Transformation, or Scapegoat?SHRM, May 18, 2026.

[12]]  Federal Reserve Bank of New York, The Labor Market for Recent College Graduates, 2026 Q1 data.

[13] Brynjolfsson, Erik, Bharat Chandar, and Ruyu Chen. “Canaries in the Coal Mine? Six Facts about the Recent Employment Effects of Artificial Intelligence.” Stanford Digital Economy Lab, November 2025.

[14] Hosseini Maasoum, Seyed Mahdi, and Guy Lichtinger. Generative AI as Seniority-Biased Technological Change: Evidence from U.S. Resume and Job Posting Data. SSRN, November 2025. Teeselink, Bouke. Generative AI and Labor Market Outcomes: Evidence from the United Kingdom. SSRN, December 2025. Tucker, Lee. “You’re Not Hired: Artificial Intelligence and Early Career Hiring in the Quarterly Workforce Indicators,” CES Working Paper CES-26-27, April 2026.

[15] Iscenko, Zanna, and Fabien Curto Millet. Looking for the Ladder: Is AI Impacting Entry-Level Jobs?  Economic Innovation Group, January 2026. Frank, Morgan, Alireza Sabet, Lisa Simon, Sarah Bana, and Renzhe Yu. “AI-Exposed Jobs Deteriorated Before ChatGPT.” arXiv, January 2026.

[16] Lambert, Peter and Yannick Schindler. “The Broken Ladder: AI, Remote Work, and Early-Career Hiring.” SSRN, May 2026.

[17]Brynjolfsson, Erik, Bharat Chandar, and Ruyu Chen. “Canaries, Interest Rates, and Timing: More on the Recent Drivers of Employment Changes for Young Workers.” Stanford Digital Economy Lab, February 2026.

[18] Brynjolfsson, Erik, Danielle Li, and Lindsey Raymond. 2025. “Generative AI at Work.” Quarterly Journal of Economics. 140(2): 889-942.

[19] Peng, Sida, Eirini Kalliamvakou, Peter Cihon, and Mert Demirer. “The Impact of AI on Developer Productivity: Evidence from GitHub Copilot.” arXiv, Feb 2023.

[20] Cui, Zheyuan, Mert Demirer, Sonia Jaffe, Leon Musolff, Sida Peng, & Tobias Salz. “The Effect of Generative AI on High-Skilled Work: Evidence from Three Field Experiments.” SSRN, August 2025.

[21] Noy, Shakked, and Whitney Zhang. 2023. “Experimental Evidence on the Productivity Effects of Generative Artificial Intelligence.” Science. 381:187-192.

[22] Choi, Jonathan, Amy Monahan, and Daniel Schwarcz. 2024. “Lawyering in the Age of Artificial Intelligence.Minnesota Law Review. pp 147-218.

[23] Lukac, Paul, William Turner, Sitaram Vangala, Aaron Chin, Joshua Khalili, Ya-Chen Shih, Catherine Sarkisian, Eric Cheng, John Mafi. “Ambient AI Scribes in Clinical Practice: A Randomized Trial.” NEJM AI 2025, 2(12).

[24] Dell’Acqua, Fabrizio, Edward McFowland III, Ethan Mollick, Hila Lifshitz-Assaf, Katherine Kellogg, Saran Rajendran, Lisa Krayer, Francois Candelon, and Karim Lakhani. “Navigating the Jagged Technological Frontier: Field Experimental Evidence of the Effects of AI on Knowledge Worker Productivity and Quality.” Harvard Business School Working Paper, No. 24-013. September 2023.

[25] Otis, Nicholas, Rowan Clarke, Solene Delecourt, David Holtz, and Rembrand Koning. “The Uneven Impact of Generative AI on Entrepreneurial Performance: Evidence from a Field Experiment in Kenya.” Harvard Business School Working Paper, No. 24-042. October 2025.

[26] Doshi, Anil, and Oliver Hauser. “Generative AI Enhances Individual Creativity but Reduces the Collective Diversity of Novel Content.” Science Advances. July 12, 2024. Moon, Kibum, Kostadin Kushlev, Andrew Bank, Ben Lira Luttges, Indre Viskontas, James Kaufman, Dan Johnson, Angela Duckworth, and Adam Green. “The Creative Link Between Words and Ideas is Weakening in the AI Era.” Available at ResearchGate. February 2026.

[27] Hao, Qianyue, Fengli Xu, Yong Li, and James Evans. 2026. “Artificial Intelligence Tools Expand Scientists’ Impact but Contract Science’s Focus.” Nature. 649: 1237-1243.

[28] Acemoglu, Daron. “The Simple Macroeconomics of AI.” NBER Working Paper 32487. 2024.

[29] Jones, Charles. “AI and Our Economic Future.” NBER Working Paper 34779. 2026.

[30] Brynjolfsson, Erik, Daniel Rock, and Chad Syverson. (2021). “The Productivity J-Curve: How Intangibles Complement General Purpose Technologies.” American Economic Journal: Macroeconomics. 13(1): 333-372.

[31] Yotzov, Ivan, Jose Maria Barrero, Nicholas Bloom, Philip Bunn, Steven Davis, Kevin Foster, Aaron Jalca, Brent Meyer, Paul Mizen, Michael Navarrete, Pawel Smietanka, Greg Thwaites, and Ben Zhe Wang. “Firm Data on AI.” Federal Reserve Bank of Atlanta Working Paper. March 2026.

[32] Crane, Leland, Michael Green, and Paul Soto. “Measuring AI Uptake in the Workplace.” FEDS Notes. Board of Governors of the Federal Reserve System. February 2025.

[33] Bonney, Kathryn, Cory Breaux, Emin Dinlersoz, Lucia Foster, John Haltiwanger, and Aditya Pande. “The Microstructure of AI Diffusion: Evidence from Firms, Business Functions, and Worker Tasks.” NBER Working Paper 35141. 2026.

[34] McKinsey. “The state of AI in 2025: Agents, innovation, and transformation.” November 5, 2025.

[35] Humlum, Anders and Emile Vestergaard. “Still Waters, Rapid Currents: Early Labor Market Transformation under Generative AI.” NBER Working Paper 33777. 2025.

[36] Kharazian, Ara, Lisa Simon, and Ryan Stevens. A New Look at AI’s Impact on Jobs: Firm-Level AI Spending and Workforce Adjustment. Ramp Economics Lab, June 2026.

The Daily Front Page 7 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — The Token Underground
article

The relay market powering token resellers and fraud

by mlenhard·▲ 188 points·113 comments·vectoral.com ↗
Companies are losing millions of dollars to abuse each day.

My Story

I’ve spent a lot of time thinking about token fraud, a problem I first stumbled upon while working as a software engineer on an AI gateway.

We faced constant abuse. At first it was free-credit abuse, where users spun up accounts en masse. Then it was our support chatbot. I started talking to friends about it and hearing stories of companies losing millions of dollars to abuse each day.

The abuse took a number of different shapes, and the abusers were relentless. I came to realize that the problem was much bigger than us. A new form of fraud had emerged, and it had become endemic to the token economy.

While researching where the abuse was coming from, I stumbled onto a Chinese forum where operators openly discussed the relays and their methods. My notes on the industry and its players are below.

So What Is a Relay?

A relay — or “transfer station” — is essentially a service that proxies traffic to U.S. models, often at a deep discount. For example, one operator’s price-comparison site listed a package that bought the equivalent of $3,333 worth of official Anthropic credit for 425 RMB — roughly $0.13 of usage per $1 spent.

Effective rate

$0.13

of official usage per $1 spent

Sample package

$3,333

of Anthropic credit for just 425 RMB

Top discount

97.8%

off official pricing, at the cheapest relay

To make that concrete, here’s how far below official pricing the relays we track actually run, ranked by discount:

ProviderMedian discount from official list price

  • 01Now Coding

    97.8%

  • 02I Code Easy

    97.1%

  • 03Claude ZZ

    96.6%

  • 04Doro

    96.4%

  • 05UoCode

    96.3%

  • 06ZeroCode

    94.9%

  • 07AiYa

    94.9%

  • 08HongMaCC

    94.2%

  • 09Right Code

    94.1%

  • 10BUZZ

    94.1%

How the Market Works

The ecosystem runs four layers deep, from the merchants sourcing raw accounts down to the developers buying cheap tokens:

01 Upstream 卡商 / 号商

Card & account merchants — virtual credit cards built to pass U.S. and European billing checks, plus bulk-registered accounts.

02 Midstream 账号池

Account pools — aggregate hundreds of upstream accounts, manage tokens and rate limits, handle failover, and expose a single API.

03 Downstream 中转站

Relays / transfer stations — wrap the pool's API in a clean, billed, Chinese-language product and compete on price.

04 End users

Developers, startups, and SaaS chasing cheap inference — plus commercial buyers running model distillation.

Upstream

Sitting at the top are the card merchants (卡商) and account merchants (号商). They sell virtual credit cards designed to pass U.S. and European billing checks, along with bulk-registered accounts.

Midstream

In the middle sit the account pools (账号池). A pool aggregates dozens or hundreds of upstream accounts, manages their authentication tokens and rate limits, handles failover when accounts get flagged, and exposes a single API surface that downstream relays can consume.

The inventory isn’t only model-lab accounts. Alongside direct OpenAI, Anthropic, and Google credentials are accounts harvested from the application layer.

Much of the forum’s activity centers on “reverse-engineered” access to tools like Kiro and antigravity, which are consumer products, not lab APIs. To a pool, it makes no difference whether a token comes from a lab or from an app built on one; anything that resells or exposes a model is a target.

Downstream

Downstream sit the relay / transfer stations themselves — the consumer-facing layer. They wrap the pool’s API in a clean Chinese-language product, handle billing and invoicing, run customer-support WeChat groups, and compete on price.

End users

At the bottom are individual Chinese developers, small startups, and mid-sized SaaS companies hunting for cheap inference — as well as some larger commercial buyers using the infrastructure for model distillation.

In practice these layers blur. Many operators run both the pool and the relay, and the forum’s own participants often use “pool” and “transfer station” interchangeably.

The Software Behind the Relays

Almost every relay I’ve looked at runs on one of two open-source projects: one-api or new-api.

Both are OpenAI-compatible gateways. An operator deploys the panel and adds a set of channels (渠道). Each channel represents a provider plus a pool of API keys. The panel exposes a single endpoint that matches the OpenAI API, so buyers just point their existing SDK at the relay’s URL. On every request it pulls a key from the pool, forwards it upstream, returns the response, and deducts quota priced by usage times a multiplier (倍率). It manages the users, tokens, pricing tiers, logs, and billing.

new-api is a more actively developed fork of one-api, and the difference is mostly commerce: it ships with self-service payment and recharge, plus image, video, and audio models. Across the relays we track, one-api turns up roughly four times as often as new-api; the original base is the more widespread of the two, even if new-api is the one built to sell.

There’s nothing inherently illicit about the software. one-api and new-api are neutral, legitimate tools. Plenty of companies self-host them to put their own accounts behind a single gateway with team quotas and spend tracking. A relay crosses the line when its channels are stocked with stolen, leaked, or pooled keys instead of the operator’s own, and when it resells that access against the providers’ terms.

The Methods

  • Free-trial abuse. Abusers automate account creation en masse to claim free credits, then proxy that traffic back to their own end users.
  • Chargeback attacks. Abusers charge back their spend after the usage period ends to recoup their costs — or use stolen cards from the start.
  • Prepaid cards. Abusers fund accounts with prepaid cards capped at a set limit.
  • Open inference. Any support chatbot without strict guardrails is ripe for having traffic proxied through it.
  • Denial of wallet. Not strictly a relay technique, but an emerging form of abuse I’ve been tracking: attackers fire off a flood of concurrent requests purely to burn a provider’s spend. It can be facilitated by any of the methods above. The difference is there’s no financial motive.

Who Are the Buyers?

The three main use cases seem to be cheap tokens, getting around geo-restrictions and model distillation. A few relevant quotes from the forum:

Diagram of the model distillation pipeline, where cheap relayed access to frontier models is used to train domestic models

From the V2EX thread · translated

@v2exgo · reply #38 V2EX

“Distillation uses Claude/CodeX models to train domestic models. There are intermediaries that specialize in distillation and can provide relevant evidence, but I can’t name specific domestic companies. Anyway, companies with strong programming capabilities are all distilling Claude; it’s a multi-billion RMB industry chain, and many big players earn hundreds of thousands a day.”

Forum discussion claiming distillers in the industry have made millions

From the V2EX thread · translated

@v2exgo · reply #129 V2EX

“It’s not just true — many distillers in the industry have made millions.”

A Growing and Maturing Market

@v2exgo · reply #82 V2EX

“I got 20TB on my first day online.” (And: “You might get 100TB of traffic as soon as you go online.”)

I was surprised by how mature the market already is. There are price-comparison sites for the relays, affiliate programs, and even gateway products. On the forums, consumer demand looks just as strong. And these aren’t fringe operations: the ten highest-traffic relays we track pull a combined 3.6 million visits a month between them.

My hunch is that things get worse for the application layer from here. As Anthropic and others roll out KYC controls and identity verification, the abuse won’t disappear, it will just move somewhere else.

They’re Raffling Off Keys Now

A clear sign of how normalized this has become is that one of the relay directories runs a daily lottery for API keys.

The site — hvoy.ai, which otherwise bills itself as a relay authenticity checker and price-comparison tool, gives away fifty $100 API keys every single day. You earn entry credits from a daily check-in, spend 20 credits per ticket, and can buy up to three tickets a round. On the day I looked, 258 people had entered 401 tickets for the fifty keys.

Screenshot of the hvoy.ai daily lottery page, showing a prize pool of 50 $100 API keys with 258 participants and 401 tickets entered

hvoy.ai · daily draw for 50 × $100 API keys

Every day

50

$100 API keys given away per round

Cost to enter

20 pts

earned free from a daily check-in

A recent round

1,150

tickets competing for 50 keys

The part that got me is the fairness theater. The draw is provably fair — the same cryptographic scheme legitimate crypto-gambling sites use to prove they didn’t rig the result. The random seed is the hash of the latest Bitcoin block, winners are picked with a Partial Fisher-Yates shuffle, and the full list of entries is published as a snapshot before the draw.

How Providers Can Defend Themselves

I’ve talked to many companies facing this, and the truth is that there’s no clean fix. Fraud is a constant cat-and-mouse game. What follows isn’t a silver bullet — it’s the set of things that I’ve seen work and that others are doing, roughly in the order that abuse travels: from account creation, to detection and then to damage control.

  • Raise the cost of entry. Make accounts hard to create in bulk and cap what a fresh one can spend. Check for browser-based automation signals. Any client-side detection can be bypassed, but every bit of friction raises the attacker’s cost.
  • Watch the money. Flag prepaid cards, virtual cards, mismatched billing info, and small card-testing charges.
  • Watch the behavior. Look for patterns no real user produces: time from registration to first token, the model selected, prompt relevance (where you can measure it), account age, and IP signals (proxy, VPN, country).
  • Cluster the accounts. Watch for IP sybils and shared device fingerprints that tie supposedly-separate accounts back to one operator.
  • Monitor for cost anomalies. Setup monitos and alerts on AI spend as a failsafe, so that you can flip things off if abuse does start.

Assume some abuse gets through anyway, and limit what it can cost you:

  • Enforce spend caps, spend locks, and concurrency limits per account.
  • Reserve budget for every in-flight request, so concurrent calls can’t blow past your limit.
  • Start new accounts with low caps; let them earn higher limits with age and a verified card.
  • If an account’s risk rises mid-session, add friction like a CAPTCHA or an additional form of identity verification.

And when you do catch someone, throttle quietly. A clean error just tells the attacker which signal to fix before they come back.

None of this stops the abuse for good. But if you make attacking your own service expensive enough that the numbers stop working, they’ll try to find an easier target.

Sources

All quotes are translated from a V2EX thread in the site’s Programmers section, “A comprehensive guide to AI transfer station jargon,” started by the user v2exgo (who operates the relay at terminal.pub). The thread ran from March 5 to June 23, 2026 and drew roughly 35,000 views and 190 replies. Reply numbers below refer to that thread.

  • Primary source — V2EX thread: https://www.v2ex.com/t/1196011
  • Price-comparison aggregator: getcheapai.com.
  • Daily API-key lottery: hvoy.ai/free-tokens/lottery — 50 $100 keys drawn per day, settled with a Bitcoin-block-hash seed and a Partial Fisher-Yates shuffle.
  • Operator’s relay: terminal.pub
  • Effective-rate derivation: reply #50, where milkleeeeee computes the 425 RMB package as equal to the official $3,333 — about $0.13 per $1 of official usage.
The Daily Front Page 8 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Agents, Infra and the Future
article

Turn And Face The Strange

by subarctic·▲ 225 points·151 comments·fly.io ↗
Turn and face the strange.

A cool old Annie illustration involving some kind of portal situation

Image by Annie Ruygt

We’re Fly.io, a public cloud platform that is both our favorite way to put an app on the Internet and our favorite way to safely let a frontier agent coding harness cook. This is a post about our company, the future, and Sprites, which are computers for agents that you can check out right now.

This is a complicated post. So I need you to promise me something: if you read past this introduction, you’ll read the whole rest of the way through. It’s an honor thing.

A couple months back, Theo Browne ran a video rating the “best place to host a new application in 2026”. Theo tends to say nice things about us. He did this time too. But then he concluded by saying that of all the providers he pays attention to, we were the one he was least confident would be around by the end of the year.

Well, fuck.

Theo startled us, because we’re in the middle of a run of strong quarters that have included the best financial months in the company’s history. But that take has been rattling around in my brain. It whacked me right on a raw nerve, about what we’re doing and where we’re going as a company.

Honestly, I should’ve seen this coming. Fly.io has been motoring along this year, but I’ve coasted a bit, letting the company smolder in an unresolved identity crisis.

I’m going to overshare some more in a second, but I won’t leave you hanging. So: we’ve raised a bunch more money. We’re launching a new iteration of Sprites, and focusing the company on them and the problem they solve. And I’m tagging in Scott Johnston as CEO.

Product-Market Fit

I started Fly.io with two clear principles that probably don’t matter anymore.

The first is that Internet applications work best when they’re fast, and that happens when they’re deployed close to users. I learned this over many years of working at Ars Technica, and started Fly.io in part to scratch an itch. It was our mantra over the first several years of the company.

The second is that cloud infrastructure is too complicated. Developers need platforms with the flexibility of AWS and the ergonomics of Heroku. When we started Fly.io, you couldn’t get both things at the same time, and now you can, here and elsewhere.

You read this and say, “no shit, of course these things are important.” But I’m here to tell you they’re less important than you think, for an obvious reason — the only reason anybody talks about anymore. AI has transmogrified software development. The dingo has truly eaten our baby.

† For the past 18 months, every time I’ve said these words, they’ve gotten even truer.

I don’t think it’s fully sunk in yet[†]. We’re still trying to integrate coding agents into our profession like they’re sufficiently smart compilers. But AI isn’t like the difference between shipping C code and shipping Ruby. It’s much bigger.

Everybody forgets that before Dan Bricklin invented the spreadsheet, every “Excel document” in the world was a computer program, built by a computer programmer. In just a matter of years, every business professional became a programmer, using the world’s most important programming language, spreadsheet formulas. AI is like that, but bigger. Almost anybody will probably be able to build almost any kind of computer program.

Now consider conventional public cloud infrastructure. We take fixed-function applications built to rigorous standards on fussy CI/CD rails and ship them to audiences of millions of people. But a computer program with an audience of millions will soon be like a spreadsheet with an audience of a million readers. They exist! But they’re not the norm.

Betting on an opinionated public cloud design from 2020 is the same as betting against personalized, adaptive software. I don’t think that’s a good bet. And even if I did, I wouldn’t want to make it. I want a world where my friends and family can make computers do exactly what they want, without waiting for me to build everything for them.

What Agents Want

That brings us to our second founding principle, which is that serious cloud infrastructure is too hard for developers to use well. And: still true! But this even more obviously doesn’t matter anymore.

† It is in fact possible that it’s now worse to have a carefully curated human developer experience with opinionated defaults. Agents work best when things are explicit.

To a first approximation, nobody reads documentation anymore. They’re not picking up new CLIs and figuring out how to use them by trial and error, either[†]. That’s what agents are for. An agent can one-shot a Fly.io deployment: just build a site locally and say “now get this working on Fly.io”, and it’ll work great. But an agent can also one-shot an AWS deployment. What are we doing here? What’s going on?

I wrote about this last year, in a post about how our fastest-growing customers were all robots. Then I stopped retconning what we’d already built and got to work figuring out what the robot customers actually want. Here’s what I came up with.

Thing 1: Coding agents expect to run on developer workstations.

Thing 2: Even in a sandbox that you trust, running an agent on your physical dev laptop is annoying, because your laptop stops running when you close the lid. Raise your hand if you’ve walked up or down a flight of stairs with your MacBook open this year. Is your hand down? I’d guess your home doesn’t have stairs. And so people all end up running their agent sandboxes in the cloud.

Thing 3: Public clouds are an irritating place to run agents. We divide servers into “pets” or “cattle”, but for agents, even a herd cow is too much commitment. You want, I don’t know, a semi-disposable cow, a cow that comes into existence exactly when you want it to and sticks around for exactly as long as you want and doesn’t cost very much — and this is why analogies are hard to write.

Earlier this year, our team made what I believe is a breakthrough in systems engineering and perhaps all of computer science: we launched the semi-disposable cow. We call them Sprites.

Sprites take an odd shape that comes from shrink-wrapping them around what I think the robots are looking for. You can create hundreds or thousands of them quickly, but all of them have 100GB durable disk drives. Like everything in the cloud, they have metered utility billing, but the meter doesn’t run when they’re not doing anything, and they’re smart about figuring out when they’re idle. And you can host an app on them and share it with your coworkers, over the Internet.

This grab-bag of features adds up to a proposition about agents. The industry obsesses over sandboxes. But robots don’t want sandboxes. They want computers. That’s what our semi-disposable cow is: a computer for an agent.

You Can Go Make A Sprite Right Now

It’ll take, like, a minute  ✨

Go!→

Computers For Agents

I’m happy with how the Sprites launch played out. But honestly, Sprites were a skunkworks project. We didn’t even host them on the main Fly.io website! A weird move. I’m not rationalizing it. We were in an identity crisis. But the clouds have parted, and Computers for Agents are, going forward, the focus of our company.

† (to get a flavor of how true that is: a git blame of the whole codebase shows my name more than any other)

Fly Machines and our Platform As A Service features aren’t going anywhere. But Sprites was the product of a tiny skeleton crew inside of Fly.io[†], and now it isn’t.

Ordinarily we’d spend thousands of words on deep-dive technical content about how we built any new product we launched. We’ll do that for Sprites too. But I’m pretty deep into this post already and I have other stuff to share. So for now, I’m going to keep it brief.

In addition to behind-the-scenes work we’ve done on scaling and orchestration, nu-Sprites introduces two big subsystems that get us to a place I’d finally consider “feature-complete” for what we’re trying to do.

The first is the Sprite Block Device (SBD). The original Sprites storage stack was a goblin contraption I personally derived from JuiceFS and wired into our system using Ben Johnson’s Litestream. You should be glad to hear that Ben and Tim Newsham tore that whole stack down to the studs and rebuilt it. It’s faster, more reliable, and still does instant checkpoint-and-restore.

More importantly, SBD enables drive forking: you can create a template Sprite, and then efficiently clone millions of times.

The other big new thing in Sprites is Connectors. Connectors build on work we did to secure our core platform: they let Sprites make authenticated requests to other systems, without giving agents anything useful to exfiltrate. Connectors have fun security properties, but are also much more pleasant to use than manually managing accounts and API keys.

These are our most requested features. They’re the reason so many agent companies are still using Fly Machines many months after we launched a product specifically for them. So I’m confident enough to bet: unless some new space alien technology arrives that does something even weirder to computer science than what Transformer models have done, Sprites are the right fit for our future customers, and a very large portion of our existing ones. Which brings us to:

Fancy Sprite Beta

You want a weird beta Sprite that can clone itself? I can get you a toe. ✨

Sign up for the beta →

I Quit

This has been a little while coming, but for the stage Fly.io is at, I think it’s extracted most of the good stuff out of me being CEO. So I’m going to stop doing that.

For the first several years of a startup, you’re running a science project, an experiment-driven search for product-market fit. As anybody who’s worked here can attest, we tried dozens of things, from unmanaged Postgres (never do this) to global CDNs to user-mode WireGuard. Deeper into the company fabric, we built a bottom-up engineering org, avoided product roadmaps, and recruited an all-remote team with members in over a dozen countries.

Some experiments paid off, and others were learning opportunities. Running them has been my whole life over the last 8 years. But Fly.io doesn’t need these kinds of science projects anymore.

For the past many months, stretching way back into 2025, I’ve been talking to Scott Johnston about what Fly.io would look like if he was calling the plays. Scott was the CEO of Docker, and led that through a really challenging time that began with Docker’s own enterprise-vs.-developer identity crisis and ended with them blowing the doors off the business. As a shareholder of Fly.io, for this stage in Fly.io’s lifecycle, I liked his playbook better than mine. As the CEO of Fly.io, I liked the prospect of him doing all this work more than I liked the prospect of me doing it. We took a lot of time to work this out, and ultimately the board and I convinced him to take the job.

This is the paragraph in these kinds of posts where I’m supposed to tell you why Scott is a perfect fit for Fly.io and recount all his past adventures. And he is, and they were majestic. But you already know what I’m going to say here, which makes it boring, and Scott can introduce himself just fine when he wants to. He’s not shy.

Meanwhile, I’m going to do what all the smart spent founder CEOs do, and move to an advisor role parachuting randomly into product design discussions (the fun part of my job) while using my board seat to annoy Scott as he executes what (for me) was the unfun part of my job better than I could have.

One of the things I’m sure Scott will break down is the fundraise we just did. That’s another thing Theo Browne called out in his video (I’m not mad, do I sound mad?) — that we hadn’t announced a raise in several years. The answer to that is: we raised a fuckload of money and didn’t need more. We’ve been operating on the threshold of never needing more, if we stuck to our original plans, and if AI didn’t cause the ground to open up and swallow us all whole. But obviously, that’s no longer the game plan.

Ch-ch-changes

Look, I know how this is going to go over. I could write this post without pissing anybody off, but I don’t know how to do that and still have it be worth reading.

We’re making a very specific and probably polarizing bet on the future of the industry: that agents, within a few years, are going to determine how almost all software is built and shipped. That software is going to become much more personal, with smaller audiences, and much more flexible and slippery.

I’m excited about all of this. I’m a little taken aback that I get to work in this field during a shift like this. But I’d have to be oblivious not to see how uncomfortable that shift makes other professionals in the field.

By the middle of the year, we could have gone one of two ways:

  • On the first path: keep investing our energy in exactly what we’ve been scaling out and refining, a platform for fixed-function full-stack applications designed by humans.
  • On the second path: dial in and nail a product that fits an agent-driven near future of software.

Five of the most dangerous words in startups are “¿Por qué no los dos?”. We do one thing or the other. We don’t limp in on both. And if we fail, we fail with our full asses. Though, I guess only most of mine going forward.

It’s a hell of a thing, steering a team, a company, a base of customers at this stage in Fly.io’s life. We’d been putting off a big decision about priorities for several months. Theo, you picked up on that. Good note! I could have decided quicker, and more clearly; instead, I shipped Sprites. Sprites answer the question that’s been facing us. I’m glad I spent the time building them, and I’m glad we’ve recruited Scott to turn them into our core business.

The Daily Front Page 9 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Shellcraft: The Humble Colon
article

A shell colon does nothing. Use it anyway

by olexsmir·▲ 387 points·169 comments·refp.se ↗
A shell colon does nothing. Use it anyway.

A shell colon does nothing. Use it anyway.

I've written more shell scripts than I can count, but I still stumble upon tricks that honestly blow my mind far too often than I care to admit. Latest thing that blew my head clean off? The shell colon.

Table of Contents

In a land far-far away..

... there was once a far too cold cup of coffee next to a freshly brewed far-too-hot one. Four different terminals where three could have been closed an hour ago, and a shell script which I really (really) did not want to write.

Who would have thought a single colon would be the one to save the day night?

Note: Want your mind blown straight away? See more colons in the limelight.

Checking for required arguments

This is a familiar dance, it's pretty much muscle memory by this point. You have a script, it takes a few arguments, and some of them are mandatory; alright, an if-statement like so many times before:

if [ -z "$1" ]; then
   echo "missing argument, aborting." 1>&2
   exit 1
fi

echo "Hello $1!"

Though.. what if I told you the above four lines could be replaced by just... one?

: "${1:?missing argument, aborting.}"
echo "Hello $1!"
$ bash example.sh
example.sh: line 1: 1: missing argument, aborting.

$ bash example.sh refp
Hello refp!

And look what happens if we refer to a variable with a proper name — it's the same behavior as previously but easier to spot; the diagnostic includes the name of our variable!

: "${GREET_NAME:?missing argument, aborting.}"
echo "Hello $GREET_NAME!"
$ bash greet.sh
greet.sh: line 1: GREET_NAME: missing argument, aborting.

Parameter expansion and the story of :?

There are two things going on in the previous snippet, and you are correct in identifying that one part is using parameter expansion:

  • The syntax ${name:?diagnostic} checks whether $name is unset or empty — if it is, the diagnostic is printed to stderr and the shell exits with a non-zero status, otherwise;
  • if the variable is set, it is equivalent to $name.

That.. other colon

So that's one colon, but what about that other one, the one who sits alone at the beginning of the line?

  • : is the null-command — a builtin that does nothing but evaluate its arguments and discard the result.
  • : is old — it goes all the way back to the 1971 Thompson shell where it doubled as a label and Unix's very first comment marker.
  • : two eyes staring at you in the dark, with love.

More colons in the limelight

Perhaps we have already established that there is more to : than meets the eye, but to prove the real magic of the null-command — here are a few usages that blew my mind.

: "${DATA_DIR:=/var/data}"       # set defaults, : swallows the result
: "${RETRIES:=3}"                # instead of running it as a command
: > error.log                    # truncate error.log
: > error.log > access.log       # truncate both error.log and access.log
( : < dataset.json ) && echo YES # is dataset.json readable?
( : >> result.json ) && echo YES # is result.json writable?
trap : INT                       # trap requires a command
sleep 60                         # sleep is interruptible
set -u                           # error on unset variables
: "$DEPLOY_ENV" "$HOST"          # check DEPLOY_ENV and HOST
if some-command; then
    :                            # command required
else
    echo "command failed"
fi

Con-colon-sion

So, if you are like me and prefer less typing (gotta go fast) — the null-command and parameter expansion are a pair worth studying before your coffee goes cold.

And also.. isn't this — magic?

set  : : : : : : : : : : : : : : : : : : : : :

while : colons are more than "${1:?magic}"; do
    echo "$*" && shift
done

Note: The above example is safe to run locally, try it!

Frequently Asked Questions

After reading a few comments online, it seems I skipped over some things worth explaining. I will keep this section updated as questions come up.

  • Why do I need the null-command? Doesn't the expansion happen without the colon?

    The parameter expansion will happen regardless, but without a null-command or similar usage the shell will treat the resulting string as a command to run.

    % ${HELLO:=123}
    zsh: command not found: 123
    

    If we prefix our parameter-expansion with the null-command, the result is discarded, but the expression is still evaluated (setting HELLO to 123).

    % : ${HELLO:=123}
    % echo $HELLO
    123
    
  • Why use the null-command when I could do VAR=${VAR:-default-value}?

    This at its core boils down to personal preference, but using our beloved colon we can shrink the number of potential typos to one (rather than two):

    : "${DATA_DIR:=/var/data}"           # <- DATA_DIR mentioned once (1)
    DATA_DIR="${DATA_DRI:-/var/data}"    # <- oops                    (2)
    
  • Why would I do any of these when it hurts readability?

    There are a few common misconceptions about the — very contrived — examples presented in this article, questions regarding their usefulness, or if one should ever-ever use these things at all.

    Let's take the below as an example.

    if some-command; then
        :                            # command required
    else
        echo "command failed"
    fi
    

    Of course you can use if ! some-command and get rid of the branching, but that isn't the point of the snippet — the point is to show that the null-command can be used in places where a command is required, but nothing is desired.

    It was never meant to be read as "oh, if I have an if-statement I should use null-command", it is intended to be read as "oh, if I am in a context which requires a command, I can use the null-command to no-op it".

    Clever real-world usage of : in the wild

    User ifphilipe posted what follows as a comment on news.ycombinator.com, which is the direct equivalent of needing a command which does absolutely nothing.

    I use the colon as EDITOR with Git when I want to do an interactive rebase combined with auto squash without having to edit the todo list. I have an alias[1] for that which I call a quick interactive rebase:

    riq = -c sequence.editor=: rebase --interactive

    Could this article have used the above rather than the so much debated if-statement? If I was clever enough to come up with it — yes.. but would it be as comprehensible in terms of understanding what : really is? I strongly suspect not.

The Daily Front Page 10 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Go’s Analysis Framework
article

Go Analysis Framework: modular static analysis by go team

by AbuAssar·▲ 200 points·58 comments·pkg.go.dev ↗
A static analysis inspects a package and reports diagnostics.

Package analysis defines the interface between a modular static analysis and an analysis driver program.

Background

A static analysis is a function that inspects a package of Go code and reports a set of diagnostics (typically mistakes in the code), and perhaps produces other results as well, such as suggested refactorings or other facts. An analysis that reports mistakes is informally called a "checker". For example, the printf checker reports mistakes in fmt.Printf format strings.

A "modular" analysis is one that inspects one package at a time but can save information from a lower-level package and use it when inspecting a higher-level package, analogous to separate compilation in a toolchain. The printf checker is modular: when it discovers that a function such as log.Fatalf delegates to fmt.Printf, it records this fact, and checks calls to that function too, including calls made from another package.

By implementing a common interface, checkers from a variety of sources can be easily selected, incorporated, and reused in a wide range of driver programs including command-line tools (such as vet), text editors and IDEs, build and test systems (such as go build, Bazel, or Buck), test frameworks, code review tools, code-base indexers (such as SourceGraph), documentation viewers (such as godoc), batch pipelines for large code bases, and so on.

Analyzer

The primary type in the API is Analyzer. An Analyzer statically describes an analysis function: its name, documentation, flags, relationship to other analyzers, and of course, its logic.

To define an analysis, a user declares a (logically constant) variable of type Analyzer. Here is a typical example from one of the analyzers in the go/analysis/passes/ subdirectory:

package unusedresult

var Analyzer = &analysis.Analyzer{
	Name: "unusedresult",
	Doc:  "check for unused results of calls to some functions",
	Run:  run,
	...
}

func run(pass *analysis.Pass) (interface{}, error) {
	...
}

An analysis driver is a program such as vet that runs a set of analyses and prints the diagnostics that they report. The driver program must import the list of Analyzers it needs. Typically each Analyzer resides in a separate package. To add a new Analyzer to an existing driver, add another item to the list:

import ( "unusedresult"; "nilness"; "printf" )

var analyses = []*analysis.Analyzer{
	unusedresult.Analyzer,
	nilness.Analyzer,
	printf.Analyzer,
}

A driver may use the name, flags, and documentation to provide on-line help that describes the analyses it performs. The doc comment contains a brief one-line summary, optionally followed by paragraphs of explanation.

The Analyzer type has more fields besides those shown above:

type Analyzer struct {
	Name             string
	Doc              string
	Flags            flag.FlagSet
	Run              func(*Pass) (interface{}, error)
	RunDespiteErrors bool
	ResultType       reflect.Type
	Requires         []*Analyzer
	FactTypes        []Fact
}

The Flags field declares a set of named (global) flag variables that control analysis behavior. Unlike vet, analysis flags are not declared directly in the command line FlagSet; it is up to the driver to set the flag variables. A driver for a single analysis, a, might expose its flag f directly on the command line as -f, whereas a driver for multiple analyses might prefix the flag name by the analysis name (-a.f) to avoid ambiguity. An IDE might expose the flags through a graphical interface, and a batch pipeline might configure them from a config file. See the "findcall" analyzer for an example of flags in action.

The RunDespiteErrors flag indicates whether the analysis is equipped to handle ill-typed code. If not, the driver will skip the analysis if there were parse or type errors. The optional ResultType field specifies the type of the result value computed by this analysis and made available to other analyses. The Requires field specifies a list of analyses upon which this one depends and whose results it may access, and it constrains the order in which a driver may run analyses. The FactTypes field is discussed in the section on Modularity. The analysis package provides a Validate function to perform basic sanity checks on an Analyzer, such as that its Requires graph is acyclic, its fact and result types are unique, and so on.

Finally, the Run field contains a function to be called by the driver to execute the analysis on a single package. The driver passes it an instance of the Pass type.

Pass

A Pass describes a single unit of work: the application of a particular Analyzer to a particular package of Go code. The Pass provides information to the Analyzer's Run function about the package being analyzed, and provides operations to the Run function for reporting diagnostics and other information back to the driver.

type Pass struct {
	Fset         *token.FileSet
	Files        []*ast.File
	OtherFiles   []string
	IgnoredFiles []string
	Pkg          *types.Package
	TypesInfo    *types.Info
	ResultOf     map[*Analyzer]interface{}
	Report       func(Diagnostic)
	...
}

The Fset, Files, Pkg, and TypesInfo fields provide the syntax trees, type information, and source positions for a single package of Go code.

The OtherFiles field provides the names of non-Go files such as assembly that are part of this package. Similarly, the IgnoredFiles field provides the names of Go and non-Go source files that are not part of this package with the current build configuration but may be part of other build configurations. The contents of these files may be read using Pass.ReadFile; see the "asmdecl" or "buildtags" analyzers for examples of loading non-Go files and reporting diagnostics against them.

The ResultOf field provides the results computed by the analyzers required by this one, as expressed in its Analyzer.Requires field. The driver runs the required analyzers first and makes their results available in this map. Each Analyzer must return a value of the type described in its Analyzer.ResultType field. For example, the "ctrlflow" analyzer returns a *ctrlflow.CFGs, which provides a control-flow graph for each function in the package (see golang.org/x/tools/go/cfg); the "inspect" analyzer returns a value that enables other Analyzers to traverse the syntax trees of the package more efficiently; and the "buildssa" analyzer constructs an SSA-form intermediate representation. Each of these Analyzers extends the capabilities of later Analyzers without adding a dependency to the core API, so an analysis tool pays only for the extensions it needs.

The Report function emits a diagnostic, a message associated with a source position. For most analyses, diagnostics are their primary result. For convenience, Pass provides a helper method, Reportf, to report a new diagnostic by formatting a string. Diagnostic is defined as:

type Diagnostic struct {
	Pos      token.Pos
	Category string // optional
	Message  string
}

The optional Category field is a short identifier that classifies the kind of message when an analysis produces several kinds of diagnostic.

The Diagnostic struct does not have a field to indicate its severity because opinions about the relative importance of Analyzers and their diagnostics vary widely among users. The design of this framework does not hold each Analyzer responsible for identifying the severity of its diagnostics. Instead, we expect that drivers will allow the user to customize the filtering and prioritization of diagnostics based on the producing Analyzer and optional Category, according to the user's preferences.

Most Analyzers inspect typed Go syntax trees, but a few, such as asmdecl and buildtag, inspect the raw text of Go source files or even non-Go files such as assembly. To report a diagnostic against a line of a raw text file, use the following sequence:

content, err := pass.ReadFile(filename)
if err != nil { ... }
tf := fset.AddFile(filename, -1, len(content))
tf.SetLinesForContent(content)
...
pass.Reportf(tf.LineStart(line), "oops")

Modular analysis with Facts

To improve efficiency and scalability, large programs are routinely built using separate compilation: units of the program are compiled separately, and recompiled only when one of their dependencies changes; independent modules may be compiled in parallel. The same technique may be applied to static analyses, for the same benefits. Such analyses are described as "modular".

A compiler’s type checker is an example of a modular static analysis. Many other checkers we would like to apply to Go programs can be understood as alternative or non-standard type systems. For example, vet's printf checker infers whether a function has the "printf wrapper" type, and it applies stricter checks to calls of such functions. In addition, it records which functions are printf wrappers for use by later analysis passes to identify other printf wrappers by induction. A result such as “f is a printf wrapper” that is not interesting by itself but serves as a stepping stone to an interesting result (such as a diagnostic) is called a Fact.

The analysis API allows an analysis to define new types of facts, to associate facts of these types with objects (named entities) declared within the current package, or with the package as a whole, and to query for an existing fact of a given type associated with an object or package.

An Analyzer that uses facts must declare their types:

var Analyzer = &analysis.Analyzer{
	Name:      "printf",
	FactTypes: []analysis.Fact{new(isWrapper)},
	...
}

type isWrapper struct{} // => *types.Func f “is a printf wrapper”

The driver program ensures that facts for a pass’s dependencies are generated before analyzing the package and is responsible for propagating facts from one package to another, possibly across address spaces. Consequently, Facts must be serializable. The API requires that drivers use the gob encoding, an efficient, robust, self-describing binary protocol. A fact type may implement the GobEncoder/GobDecoder interfaces if the default encoding is unsuitable. Facts should be stateless. Because serialized facts may appear within build outputs, the gob encoding of a fact must be deterministic, to avoid spurious cache misses in build systems that use content-addressable caches. The driver makes a single call to the gob encoder for all facts exported by a given analysis pass, so that the topology of shared data structures referenced by multiple facts is preserved.

The Pass type has functions to import and export facts, associated either with an object or with a package:

type Pass struct {
	...
	ExportObjectFact func(types.Object, Fact)
	ImportObjectFact func(types.Object, Fact) bool

	ExportPackageFact func(fact Fact)
	ImportPackageFact func(*types.Package, Fact) bool
}

An Analyzer may only export facts associated with the current package or its objects, though it may import facts from any package or object that is an import dependency of the current package.

Conceptually, ExportObjectFact(obj, fact) inserts fact into a hidden map keyed by the pair (obj, TypeOf(fact)), and the ImportObjectFact function retrieves the entry from this map and copies its value into the variable pointed to by fact. This scheme assumes that the concrete type of fact is a pointer; this assumption is checked by the Validate function. See the "printf" analyzer for an example of object facts in action.

Some driver implementations (such as those based on Bazel and Blaze) do not currently apply analyzers to packages of the standard library. Therefore, for best results, analyzer authors should not rely on analysis facts being available for standard packages. For example, although the printf checker is capable of deducing during analysis of the log package that log.Printf is a printf wrapper, this fact is built in to the analyzer so that it correctly checks calls to log.Printf even when run in a driver that does not apply it to standard packages. We would like to remove this limitation in future.

Testing an Analyzer

The analysistest subpackage provides utilities for testing an Analyzer. In a few lines of code, it is possible to run an analyzer on a package of testdata files and check that it reported all the expected diagnostics and facts (and no more). Expectations are expressed using "// want ..." comments in the input code.

Standalone commands

Analyzers are provided in the form of packages that a driver program is expected to import. The vet command imports a set of several analyzers, but users may wish to define their own analysis commands that perform additional checks. To simplify the task of creating an analysis command, either for a single analyzer or for a whole suite, we provide the singlechecker and multichecker subpackages.

The singlechecker package provides the main function for a command that runs one analyzer. By convention, each analyzer such as go/analysis/passes/findcall should be accompanied by a singlechecker-based command such as go/analysis/passes/findcall/cmd/findcall, defined in its entirety as:

package main

import (
	"golang.org/x/tools/go/analysis/passes/findcall"
	"golang.org/x/tools/go/analysis/singlechecker"
)

func main() { singlechecker.Main(findcall.Analyzer) }

A tool that provides multiple analyzers can use multichecker in a similar way, giving it the list of Analyzers.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Validate

func Validate(analyzers []*Analyzer) error

Validate reports an error if any of the analyzers are misconfigured. Checks include: that the name is a valid identifier; that the Doc is not empty; that the Run is non-nil; that the Requires graph is acyclic; that analyzer fact types are unique; that each fact type is a pointer.

Analyzer names need not be unique, though this may be confusing.

Types

type Analyzer

type Analyzer struct {
	// The Name of the analyzer must be a valid Go identifier
	// as it may appear in command-line flags, URLs, and so on.
	Name string

	// Doc is the documentation for the analyzer.
	// The part before the first "\n\n" is the title
	// (no capital or period, max ~60 letters).
	Doc string

	// URL holds an optional link to a web page with additional
	// documentation for this analyzer.
	URL string

	// Flags defines any flags accepted by the analyzer.
	// The manner in which these flags are exposed to the user
	// depends on the driver which runs the analyzer.
	Flags flag.FlagSet

	// Run applies the analyzer to a package.
	// It returns an error if the analyzer failed.
	//
	// On success, the Run function may return a result
	// computed by the Analyzer; its type must match ResultType.
	// The driver makes this result available as an input to
	// another Analyzer that depends directly on this one (see
	// Requires) when it analyzes the same package.
	//
	// To pass analysis results between packages (and thus
	// potentially between address spaces), use Facts, which are
	// serializable.
	Run func(*Pass) (any, error)

	// RunDespiteErrors allows the driver to invoke
	// the Run method of this analyzer even on a
	// package that contains parse or type errors.
	// The [Pass.TypeErrors] field may consequently be non-empty.
	RunDespiteErrors bool

	// Requires is a set of analyzers that must run successfully
	// before this one on a given package. This analyzer may inspect
	// the outputs produced by each analyzer in Requires.
	// The graph over analyzers implied by Requires edges must be acyclic.
	//
	// Requires establishes a "horizontal" dependency between
	// analysis passes (different analyzers, same package).
	Requires []*Analyzer

	// ResultType is the type of the optional result of the Run function.
	ResultType reflect.Type

	// FactTypes indicates that this analyzer imports and exports
	// Facts of the specified concrete types.
	// An analyzer that uses facts may assume that its import
	// dependencies have been similarly analyzed before it runs.
	// Facts must be pointers.
	//
	// FactTypes establishes a "vertical" dependency between
	// analysis passes (same analyzer, different packages).
	FactTypes []Fact
}

An Analyzer describes an analysis function and its options.

func (*Analyzer) String

func (a *Analyzer) String() string

type CycleInRequiresGraphError

type CycleInRequiresGraphError struct {
	AnalyzerNames map[string]bool
}

func (*CycleInRequiresGraphError) Error

func (e *CycleInRequiresGraphError) Error() string

type Diagnostic

type Diagnostic struct {
	Pos      token.Pos
	End      token.Pos // optional
	Category string    // optional
	Message  string

	// URL is the optional location of a web page that provides
	// additional documentation for this diagnostic.
	//
	// If URL is empty but a Category is specified, then the
	// Analysis driver should treat the URL as "#"+Category.
	//
	// The URL may be relative. If so, the base URL is that of the
	// Analyzer that produced the diagnostic;
	// see https://pkg.go.dev/net/url#URL.ResolveReference.
	URL string

	// SuggestedFixes is an optional list of fixes to address the
	// problem described by the diagnostic. Each one represents an
	// alternative strategy, and should have a distinct and
	// descriptive message; at most one may be applied.
	//
	// Fixes for different diagnostics should be treated as
	// independent changes to the same baseline file state,
	// analogous to a set of git commits all with the same parent.
	// Combining fixes requires resolving any conflicts that
	// arise, analogous to a git merge.
	// Any conflicts that remain may be dealt with, depending on
	// the tool, by discarding fixes, consulting the user, or
	// aborting the operation.
	SuggestedFixes []SuggestedFix

	// Related contains optional secondary positions and messages
	// related to the primary diagnostic.
	Related []RelatedInformation
}

A Diagnostic is a message associated with a source location or range.

An Analyzer may return a variety of diagnostics; the optional Category, which should be a constant, may be used to classify them. It is primarily intended to make it easy to look up documentation.

All Pos values are interpreted relative to Pass.Fset. If End is provided, the diagnostic is specified to apply to the range between Pos and End.

type Fact

type Fact interface {
	AFact() // dummy method to avoid type errors
}

A Fact is an intermediate fact produced during analysis.

Each fact is associated with a named declaration (a types.Object) or with a package as a whole. A single object or package may have multiple associated facts, but only one of any particular fact type.

A Fact represents a predicate such as "never returns", but does not represent the subject of the predicate such as "function F" or "package P".

Facts may be produced in one analysis pass and consumed by another analysis pass even if these are in different address spaces. If package P imports Q, all facts about Q produced during analysis of that package will be available during later analysis of P. Facts are analogous to type export data in a build system: just as export data enables separate compilation of several passes, facts enable "separate analysis".

Each pass (a, p) starts with the set of facts produced by the same analyzer a applied to the packages directly imported by p. The analysis may add facts to the set, and they may be exported in turn. An analysis's Run function may retrieve facts by calling Pass.Import{Object,Package}Fact and update them using Pass.Export{Object,Package}Fact.

A fact is logically private to its Analysis. To pass values between different analyzers, use the results mechanism; see Analyzer.Requires, Analyzer.ResultType, and Pass.ResultOf.

A Fact type must be a pointer. Facts are encoded and decoded using encoding/gob. A Fact may implement the GobEncoder/GobDecoder interfaces to customize its encoding. Fact encoding should not fail.

A Fact should not be modified once exported.

type Module added in v0.24.0

type Module struct {
	Path      string       // module path
	Version   string       // module version ("" if unknown, such as for workspace modules)
	Replace   *Module      // replaced by this module
	Time      *time.Time   // time version was created
	Main      bool         // is this the main module?
	Indirect  bool         // is this module only an indirect dependency of main module?
	Dir       string       // directory holding files for this module, if any
	GoMod     string       // path to go.mod file used when loading this module, if any
	GoVersion string       // go version used in module (e.g. "go1.22.0")
	Error     *ModuleError // error loading module
}

A Module describes the module to which a package belongs.

type ModuleError added in v0.43.0

type ModuleError struct {
	Err string // the error itself
}

ModuleError holds errors loading a module.

type ObjectFact

type ObjectFact struct {
	Object types.Object
	Fact   Fact
}

ObjectFact is an object together with an associated fact.

type PackageFact

type PackageFact struct {
	Package *types.Package
	Fact    Fact
}

PackageFact is a package together with an associated fact.

type Pass

type Pass struct {
	Analyzer *Analyzer // the identity of the current analyzer

	// syntax and type information
	Fset         *token.FileSet // file position information; Run may add new files
	Files        []*ast.File    // the abstract syntax tree of each file
	OtherFiles   []string       // names of non-Go files of this package
	IgnoredFiles []string       // names of ignored source files in this package
	Pkg          *types.Package // type information about the package
	TypesInfo    *types.Info    // type information about the syntax trees
	TypesSizes   types.Sizes    // function for computing sizes of types
	TypeErrors   []types.Error  // type errors (only if Analyzer.RunDespiteErrors)

	Module *Module // the package's enclosing module (possibly nil in some drivers)

	// Report reports a Diagnostic, a finding about a specific location
	// in the analyzed source code such as a potential mistake.
	// It may be called by the Run function.
	Report func(Diagnostic)

	// ResultOf provides the inputs to this analysis pass, which are
	// the corresponding results of its prerequisite analyzers.
	// The map keys are the elements of Analysis.Required,
	// and the type of each corresponding value is the required
	// analysis's ResultType.
	ResultOf map[*Analyzer]any

	// ReadFile returns the contents of the named file.
	//
	// The only valid file names are the elements of OtherFiles
	// and IgnoredFiles, and names returned by
	// Fset.File(f.FileStart).Name() for each f in Files.
	//
	// Analyzers must use this function (if provided) instead of
	// accessing the file system directly. This allows a driver to
	// provide a virtualized file tree (including, for example,
	// unsaved editor buffers) and to track dependencies precisely
	// to avoid unnecessary recomputation.
	ReadFile func(filename string) ([]byte, error)

	// ImportObjectFact retrieves a fact associated with obj.
	// Given a value ptr of type *T, where *T satisfies Fact,
	// ImportObjectFact copies the value to *ptr.
	//
	// ImportObjectFact panics if called after the pass is complete.
	// ImportObjectFact is not concurrency-safe.
	ImportObjectFact func(obj types.Object, fact Fact) bool

	// ImportPackageFact retrieves a fact associated with package pkg,
	// which must be this package or one of its dependencies.
	// See comments for ImportObjectFact.
	ImportPackageFact func(pkg *types.Package, fact Fact) bool

	// ExportObjectFact associates a fact of type *T with the obj,
	// replacing any previous fact of that type.
	//
	// ExportObjectFact panics if it is called after the pass is
	// complete, or if obj does not belong to the package being analyzed.
	// ExportObjectFact is not concurrency-safe.
	ExportObjectFact func(obj types.Object, fact Fact)

	// ExportPackageFact associates a fact with the current package.
	// See comments for ExportObjectFact.
	ExportPackageFact func(fact Fact)

	// AllPackageFacts returns a new slice containing all package
	// facts of the analysis's FactTypes in unspecified order.
	// See comments for AllObjectFacts.
	AllPackageFacts func() []PackageFact

	// AllObjectFacts returns a new slice containing all object
	// facts of the analysis's FactTypes in unspecified order.
	//
	// The result includes all facts exported by packages
	// whose symbols are referenced by the current package
	// (by qualified identifiers or field/method selections).
	// And it includes all facts exported from the current
	// package by the current analysis pass.
	AllObjectFacts func() []ObjectFact
}

A Pass provides information to the Run function that applies a specific analyzer to a single Go package.

It forms the interface between the analysis logic and the driver program, and has both input and an output components.

As in a compiler, one pass may depend on the result computed by another.

The Run function should not call any of the Pass functions concurrently.

func (*Pass) ReportRangef

func (pass *Pass) ReportRangef(rng Range, format string, args ...any)

ReportRangef is a helper function that reports a Diagnostic using the range provided. ast.Node values can be passed in as the range because they satisfy the Range interface.

func (*Pass) Reportf

func (pass *Pass) Reportf(pos token.Pos, format string, args ...any)

Reportf is a helper function that reports a Diagnostic using the specified position and formatted error message.

func (*Pass) String

func (pass *Pass) String() string

type Range

type Range interface {
	Pos() token.Pos // position of first character belonging to the node
	End() token.Pos // position of first character immediately after the node
}

The Range interface provides a range. It's equivalent to and satisfied by ast.Node.

type RelatedInformation

type RelatedInformation struct {
	Pos     token.Pos
	End     token.Pos // optional
	Message string
}

RelatedInformation contains information related to a diagnostic. For example, a diagnostic that flags duplicated declarations of a variable may include one RelatedInformation per existing declaration.

type SuggestedFix

type SuggestedFix struct {
	// A verb phrase describing the fix, to be shown to
	// a user trying to decide whether to accept it.
	//
	// Example: "Remove the surplus argument"
	Message   string
	TextEdits []TextEdit
}

A SuggestedFix is a code change associated with a Diagnostic that a user can choose to apply to their code. Usually the SuggestedFix is meant to fix the issue flagged by the diagnostic.

The TextEdits must not overlap, nor contain edits for other packages. Edits need not be totally ordered, but the order determines how insertions at the same point will be applied.

type TextEdit

type TextEdit struct {
	// For a pure insertion, End can either be set to Pos or token.NoPos.
	Pos     token.Pos
	End     token.Pos
	NewText []byte
}

A TextEdit represents the replacement of the code between Pos and End with the new text. Each TextEdit should apply to a single file. End should not be earlier in the file than Pos.

Source Files

View all Source files

  • analysis.go
  • diagnostic.go
  • doc.go
  • validate.go

Directories

Path Synopsis

analysistest

Package analysistest provides utilities for testing analyzers.

Package analysistest provides utilities for testing analyzers.

checker

Package checker provides an analysis driver based on the golang.org/x/tools/go/packages representation of a set of packages and all their dependencies, as produced by packages.Load.

Package checker provides an analysis driver based on the golang.org/x/tools/go/packages representation of a set of packages and all their dependencies, as produced by packages.Load.

internal

analysisflags

Package analysisflags defines helpers for processing flags (-help, -json, -fix, -diff, etc) common to unitchecker and {single,multi}checker.

Package analysisflags defines helpers for processing flags (-help, -json, -fix, -diff, etc) common to unitchecker and {single,multi}checker.

checker

Package internal/checker defines various implementation helpers for the singlechecker and multichecker packages, which provide the complete main function for an analysis driver executable based on go/packages.

Package internal/checker defines various implementation helpers for the singlechecker and multichecker packages, which provide the complete main function for an analysis driver executable based on go/packages.

multichecker

Package multichecker defines the main function for an analysis driver with several analyzers.

Package multichecker defines the main function for an analysis driver with several analyzers.

passes

appends

Package appends defines an Analyzer that detects if there is only one variable in append.

Package appends defines an Analyzer that detects if there is only one variable in append.

asmdecl

Package asmdecl defines an Analyzer that reports mismatches between assembly files and Go declarations.

Package asmdecl defines an Analyzer that reports mismatches between assembly files and Go declarations.

assign

Package assign defines an Analyzer that detects useless assignments.

Package assign defines an Analyzer that detects useless assignments.

atomic

Package atomic defines an Analyzer that checks for common mistakes using the sync/atomic package.

Package atomic defines an Analyzer that checks for common mistakes using the sync/atomic package.

atomicalign

Package atomicalign defines an Analyzer that checks for non-64-bit-aligned arguments to sync/atomic functions.

Package atomicalign defines an Analyzer that checks for non-64-bit-aligned arguments to sync/atomic functions.

bools

Package bools defines an Analyzer that detects common mistakes involving boolean operators.

Package bools defines an Analyzer that detects common mistakes involving boolean operators.

buildssa

Package buildssa defines an Analyzer that constructs the SSA representation of an error-free package and returns the set of all functions within it.

Package buildssa defines an Analyzer that constructs the SSA representation of an error-free package and returns the set of all functions within it.

buildtag

Package buildtag defines an Analyzer that checks build tags.

Package buildtag defines an Analyzer that checks build tags.

cgocall

Package cgocall defines an Analyzer that detects some violations of the cgo pointer passing rules.

Package cgocall defines an Analyzer that detects some violations of the cgo pointer passing rules.

composite

Package composite defines an Analyzer that checks for unkeyed composite literals.

Package composite defines an Analyzer that checks for unkeyed composite literals.

copylock

Package copylock defines an Analyzer that checks for locks erroneously passed by value.

Package copylock defines an Analyzer that checks for locks erroneously passed by value.

ctrlflow

Package ctrlflow is an analysis that provides a syntactic control-flow graph (CFG) for the body of a function.

Package ctrlflow is an analysis that provides a syntactic control-flow graph (CFG) for the body of a function.

deepequalerrors

Package deepequalerrors defines an Analyzer that checks for the use of reflect.DeepEqual with error values.

Package deepequalerrors defines an Analyzer that checks for the use of reflect.DeepEqual with error values.

defers

Package defers defines an Analyzer that checks for common mistakes in defer statements.

Package defers defines an Analyzer that checks for common mistakes in defer statements.

defers/cmd/defers command

The defers command runs the defers analyzer.

The defers command runs the defers analyzer.

directive

Package directive defines an Analyzer that checks known Go toolchain directives.

Package directive defines an Analyzer that checks known Go toolchain directives.

errorsas

Package errorsas defines an Analyzer that checks that the second argument to errors.As is a pointer to a type implementing error.

Package errorsas defines an Analyzer that checks that the second argument to errors.As is a pointer to a type implementing error.

fieldalignment

Package fieldalignment defines an Analyzer that detects structs that would use less memory if their fields were sorted.

Package fieldalignment defines an Analyzer that detects structs that would use less memory if their fields were sorted.

fieldalignment/cmd/fieldalignment command

findcall

Package findcall defines an Analyzer that serves as a trivial example and test of the Analysis API.

Package findcall defines an Analyzer that serves as a trivial example and test of the Analysis API.

findcall/cmd/findcall command

The findcall command runs the findcall analyzer.

The findcall command runs the findcall analyzer.

framepointer

Package framepointer defines an Analyzer that reports assembly code that clobbers the frame pointer before saving it.

Package framepointer defines an Analyzer that reports assembly code that clobbers the frame pointer before saving it.

gofix

Package gofix defines an Analyzer that checks "//go:fix inline" directives.

Package gofix defines an Analyzer that checks "//go:fix inline" directives.

hostport

Package hostport defines an analyzer for calls to net.Dial with addresses of the form "%s:%d" or "%s:%s", which work only with IPv4.

Package hostport defines an analyzer for calls to net.Dial with addresses of the form "%s:%d" or "%s:%s", which work only with IPv4.

httpmux

httpmux/cmd/httpmux command

The httpmux command runs the httpmux analyzer.

The httpmux command runs the httpmux analyzer.

httpresponse

Package httpresponse defines an Analyzer that checks for mistakes using HTTP responses.

Package httpresponse defines an Analyzer that checks for mistakes using HTTP responses.

ifaceassert

Package ifaceassert defines an Analyzer that flags impossible interface-interface type assertions.

Package ifaceassert defines an Analyzer that flags impossible interface-interface type assertions.

ifaceassert/cmd/ifaceassert command

The ifaceassert command runs the ifaceassert analyzer.

The ifaceassert command runs the ifaceassert analyzer.

inline

Package inline defines an analyzer that inlines calls to functions and uses of constants marked with a "//go:fix inline" directive.

Package inline defines an analyzer that inlines calls to functions and uses of constants marked with a "//go:fix inline" directive.

inline/cmd/inline command

The inline command applies the inliner to the specified packages of Go source code.

The inline command applies the inliner to the specified packages of Go source code.

inspect

Package inspect defines an Analyzer that provides an AST inspector (golang.org/x/tools/go/ast/inspector.Inspector) for the syntax trees of a package.

Package inspect defines an Analyzer that provides an AST inspector (golang.org/x/tools/go/ast/inspector.Inspector) for the syntax trees of a package.

internal/gofixdirective

Package gofixdirective searches for and validates go:fix directives.

Package gofixdirective searches for and validates go:fix directives.

loopclosure

Package loopclosure defines an Analyzer that checks for references to enclosing loop variables from within nested functions.

Package loopclosure defines an Analyzer that checks for references to enclosing loop variables from within nested functions.

lostcancel

Package lostcancel defines an Analyzer that checks for failure to call a context cancellation function.

Package lostcancel defines an Analyzer that checks for failure to call a context cancellation function.

lostcancel/cmd/lostcancel command

The lostcancel command applies the golang.org/x/tools/go/analysis/passes/lostcancel analysis to the specified packages of Go source code.

The lostcancel command applies the golang.org/x/tools/go/analysis/passes/lostcancel analysis to the specified packages of Go source code.

modernize

Package modernize provides a suite of analyzers that suggest simplifications to Go code, using modern language and library features.

Package modernize provides a suite of analyzers that suggest simplifications to Go code, using modern language and library features.

modernize/cmd/modernize command

The modernize command suggests (or, with -fix, applies) fixes that clarify Go code by using more modern features.

The modernize command suggests (or, with -fix, applies) fixes that clarify Go code by using more modern features.

nilfunc

Package nilfunc defines an Analyzer that checks for useless comparisons against nil.

Package nilfunc defines an Analyzer that checks for useless comparisons against nil.

nilness

Package nilness inspects the control-flow graph of an SSA function and reports errors such as nil pointer dereferences and degenerate nil pointer comparisons.

Package nilness inspects the control-flow graph of an SSA function and reports errors such as nil pointer dereferences and degenerate nil pointer comparisons.

nilness/cmd/nilness command

The nilness command applies the golang.org/x/tools/go/analysis/passes/nilness analysis to the specified packages of Go source code.

The nilness command applies the golang.org/x/tools/go/analysis/passes/nilness analysis to the specified packages of Go source code.

pkgfact

The pkgfact package is a demonstration and test of the package fact mechanism.

The pkgfact package is a demonstration and test of the package fact mechanism.

printf

Package printf defines an Analyzer that checks consistency of Printf format strings and arguments.

Package printf defines an Analyzer that checks consistency of Printf format strings and arguments.

reflectvaluecompare

Package reflectvaluecompare defines an Analyzer that checks for accidentally using == or reflect.DeepEqual to compare reflect.Value values.

Package reflectvaluecompare defines an Analyzer that checks for accidentally using == or reflect.DeepEqual to compare reflect.Value values.

reflectvaluecompare/cmd/reflectvaluecompare command

The reflectvaluecompare command applies the reflectvaluecompare checker to the specified packages of Go source code.

The reflectvaluecompare command applies the reflectvaluecompare checker to the specified packages of Go source code.

scannererr

Package scannererr defines an analyzer for uses of bufio.Scanner in which the user has forgotten to check Scanner.Err.

Package scannererr defines an analyzer for uses of bufio.Scanner in which the user has forgotten to check Scanner.Err.

shadow

Package shadow defines an Analyzer that checks for shadowed variables.

Package shadow defines an Analyzer that checks for shadowed variables.

shadow/cmd/shadow command

The shadow command runs the shadow analyzer.

The shadow command runs the shadow analyzer.

shift

Package shift defines an Analyzer that checks for shifts that exceed the width of an integer.

Package shift defines an Analyzer that checks for shifts that exceed the width of an integer.

sigchanyzer

Package sigchanyzer defines an Analyzer that detects misuse of unbuffered signal as argument to signal.Notify.

Package sigchanyzer defines an Analyzer that detects misuse of unbuffered signal as argument to signal.Notify.

slog

Package slog defines an Analyzer that checks for mismatched key-value pairs in log/slog calls.

Package slog defines an Analyzer that checks for mismatched key-value pairs in log/slog calls.

sortslice

Package sortslice defines an Analyzer that checks for calls to sort.Slice that do not use a slice type as first argument.

Package sortslice defines an Analyzer that checks for calls to sort.Slice that do not use a slice type as first argument.

sqlrowserr

Package sqlrowserr defines an analyzer for uses of sql.Rows in which the user has forgotten to check Rows.Err.

Package sqlrowserr defines an analyzer for uses of sql.Rows in which the user has forgotten to check Rows.Err.

stdmethods

Package stdmethods defines an Analyzer that checks for misspellings in the signatures of methods similar to well-known interfaces.

Package stdmethods defines an Analyzer that checks for misspellings in the signatures of methods similar to well-known interfaces.

stdversion

Package stdversion reports uses of standard library symbols that are "too new" for the Go version in force in the referring file.

Package stdversion reports uses of standard library symbols that are "too new" for the Go version in force in the referring file.

stringintconv

Package stringintconv defines an Analyzer that flags type conversions from integers to strings.

Package stringintconv defines an Analyzer that flags type conversions from integers to strings.

stringintconv/cmd/stringintconv command

The stringintconv command runs the stringintconv analyzer.

The stringintconv command runs the stringintconv analyzer.

structtag

Package structtag defines an Analyzer that checks struct field tags are well formed.

Package structtag defines an Analyzer that checks struct field tags are well formed.

testinggoroutine

Package testinggoroutine defines an Analyzerfor detecting calls to Fatal from a test goroutine.

Package testinggoroutine defines an Analyzerfor detecting calls to Fatal from a test goroutine.

tests

Package tests defines an Analyzer that checks for common mistaken usages of tests and examples.

Package tests defines an Analyzer that checks for common mistaken usages of tests and examples.

timeformat

Package timeformat defines an Analyzer that checks for the use of time.Format or time.Parse calls with a bad format.

Package timeformat defines an Analyzer that checks for the use of time.Format or time.Parse calls with a bad format.

unmarshal

The unmarshal package defines an Analyzer that checks for passing non-pointer or non-interface types to unmarshal and decode functions.

The unmarshal package defines an Analyzer that checks for passing non-pointer or non-interface types to unmarshal and decode functions.

unmarshal/cmd/unmarshal command

The unmarshal command runs the unmarshal analyzer.

The unmarshal command runs the unmarshal analyzer.

unreachable

Package unreachable defines an Analyzer that checks for unreachable code.

Package unreachable defines an Analyzer that checks for unreachable code.

unsafeptr

Package unsafeptr defines an Analyzer that checks for invalid conversions of uintptr to unsafe.Pointer.

Package unsafeptr defines an Analyzer that checks for invalid conversions of uintptr to unsafe.Pointer.

unusedresult

Package unusedresult defines an analyzer that checks for unused results of calls to certain pure functions.

Package unusedresult defines an analyzer that checks for unused results of calls to certain pure functions.

unusedresult/cmd/unusedresult command

The unusedresult command applies the golang.org/x/tools/go/analysis/passes/unusedresult analysis to the specified packages of Go source code.

The unusedresult command applies the golang.org/x/tools/go/analysis/passes/unusedresult analysis to the specified packages of Go source code.

unusedwrite

Package unusedwrite checks for unused writes to the elements of a struct or array object.

Package unusedwrite checks for unused writes to the elements of a struct or array object.

usesgenerics

Package usesgenerics defines an Analyzer that checks for usage of generic features added in Go 1.18.

Package usesgenerics defines an Analyzer that checks for usage of generic features added in Go 1.18.

waitgroup

Package waitgroup defines an Analyzer that detects simple misuses of sync.WaitGroup.

Package waitgroup defines an Analyzer that detects simple misuses of sync.WaitGroup.

singlechecker

Package singlechecker defines the main function for an analysis driver with only a single analysis.

Package singlechecker defines the main function for an analysis driver with only a single analysis.

suite

fix

The fix package defines the suite of analyzers used by cmd/fix, the default analysis tool run by "go fix".

The fix package defines the suite of analyzers used by cmd/fix, the default analysis tool run by "go fix".

vet

The vet package defines the suite of analyzers used by cmd/vet, the default analysis tool run by "go vet".

The vet package defines the suite of analyzers used by cmd/vet, the default analysis tool run by "go vet".

unitchecker

The unitchecker package defines the main function for an analysis driver that analyzes a single compilation unit during a build.

The unitchecker package defines the main function for an analysis driver that analyzes a single compilation unit during a build.

The Daily Front Page 11 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Big Lint Energy
article

Ruff v0.16.0 – Significant new updates – 413 default rules up from 59

by vismit2000·▲ 340 points·224 comments·astral.sh ↗
Ruff is an extremely fast Python linter and formatter, written in Rust.

Ruff v0.16.0

Ruff v0.16.0 is available now! Install it from PyPI, or with your package manager of choice:

uv tool install ruff@latest

As a reminder: Ruff is an extremely fast Python linter and formatter, written in Rust. Ruff can be used to replace Black, Flake8 (plus dozens of plugins), isort, pydocstyle, pyupgrade, and more, all while executing tens or hundreds of times faster than any individual tool.

Migrating to v0.16

Ruff v0.16 has a small number of breaking changes, allowing most users to update without significant changes to code or configuration. The main exception is described below.

Better default rule set

Ruff now enables 413 rules by default, up from 59 in previous versions.

Since Ruff's default rule set was last modified in v0.1.0, the number of rules in Ruff has grown from 708 to 968. Many of these rules catch severe issues, including syntax errors and immediate runtime errors but were not previously enabled by default. With the new rule set, Ruff will bring these issues and many others to your attention without any Ruff configuration. Even if you're already using select or extend-select, we hope that this will draw your attention to helpful rules that you previously hadn't discovered.

The full listing of enabled rules is too long to include here, but you can find it on our new Default Rules page in the documentation. A few of the highlights include rules from the popular flake8-bugbear (B) and pyupgrade (UP) linters, as well as rules from our own RUF category.

If you want to revert to the old default set, you can easily select the old rules with this configuration:

[lint]
select = ["E4", "E7", "E9", "F"]

We view this work as closely tied to our longstanding goal of rule recategorization, so look forward to upcoming developments in this area.

New features in v0.16

Ruff v0.16 also includes several newly stabilized features that are highlighted below.

Markdown code block formatting

Ruff can now format Python code blocks embedded in Markdown files.

In these files, Ruff v0.16 will format fenced code blocks with a python, py, python3, py3, pyi, or pycon info string. pyi blocks are formatted like stub files, pycon blocks as REPL sessions, and the others use normal Python file formatting. For example:

# README

Here's an example:

```py
import ruff

ruff_binary = (
    ruff.find_ruff_bin()
)
```

will be reformatted when running ruff format:

Ruff formatter output showing a Markdown code block that would be reformatted

This can also be used to format Quarto notebooks because Ruff still recognizes the language when surrounded by curly braces (e.g. ```{python}). Note that you may need to configure your extension mapping if your Quarto files use the .qmd extension.

If you need to suppress formatting, you have several options. If you want the suppression comments to appear in the code block, you can use normal fmt: off and fmt: on comments within the code block itself, or you can use similar HTML comments to disable formatting for a whole region of the document:

# README

<!-- fmt: off -->

```py
x =      "this will be suppressed"
```

<!-- fmt: on -->

To suppress Markdown formatting entirely, you can use the normal extend-exclude setting to exclude all Markdown files with a glob like *.md.

See the full documentation for more details.

ruff: ignore comments offer new suppression features

Ruff now has its own suppression comment format that can be used on its own line.

In v0.15, the Ruff linter gained a range suppression mechanism through paired ruff: disable and ruff: enable comments, much like the fmt: off and fmt: on pair described above:

# ruff: disable[N803]
def foo(
    legacyArg1,
    legacyArg2,
    legacyArg3,
    legacyArg4,
): ...
# ruff: enable[N803]

Ruff v0.16 builds on this to add two additional ruff suppression comments. ruff: ignore can be used to suppress a diagnostic on the same line, like noqa, or on the following logical line:

import math  # ruff: ignore[F401]

# ruff: ignore[N803]
def foo(
    legacyArg1,
    legacyArg2,
    legacyArg3,
    legacyArg4,
): ...

In this case, the logical line spans the whole function header (from def to the colon), so the ruff: ignore suppresses all of the same N803 diagnostics as the disable/enable pair above.

ruff: file-ignore comments can be used to suppress diagnostics for the entire file, just like ruff: noqa comments:

# ruff: file-ignore[F401] Allow unused imports in this file

import foo
import bar
import baz

As this example also shows, each of these comment kinds can have an associated "reason" explaining why they were added, in this case Allow unused imports in this file.

ruff: ignore comments can be added automatically with the new --add-ignore CLI flag, and in preview, all of these ruff suppression comments support rule names instead of codes:

❯ echo 'import math' > try.py
❯ uvx ruff@latest check --preview --add-ignore try.py
Added 1 ignore comment.
❯ cat try.py
import math  # ruff: ignore[unused-import]

You can find the full specification for all of these comments in the documentation.

Fixes are now shown in check and format --check output

Ruff now shows the diff for linter and formatter fixes when rendering diagnostics.

Both the check and format subcommands have long supported the --diff flag for showing the changes introduced by applying fixes with check --fix or format, but this was separate from the normal output and suppressed the accompanying explanatory diagnostics. In v0.16, available fixes are now shown as part of the default full output format, rendered below the help subdiagnostic:

Ruff check output showing the diff for an unused-import fix

The same is true for format --check. Given the following input:

# example.py

if True:
    pass
elif     False:
    pass

The formatter produces:

Ruff formatter output showing the diff for an unformatted file

format --check also now supports the full selection of output formats supported by the linter. You can use this to obtain machine-readable JSON output or produce the formats expected by GitHub and GitLab to render annotations in CI, as just a couple of examples. See the CLI help or documentation for the full list of supported formats.

One final note on output formats is that there is a small breaking change to the JSON output in v0.16. The filename, location, end_location, fix.edits[].location, and fix.edits[].end_location fields may now be null rather than defaulting to the empty string and row 1, column 1, respectively. This should affect very few of Ruff's existing diagnostics but better reflects the internal diagnostic representation and may become more common in future rules.

Rule stabilizations

The following rules have been stabilized and are no longer in preview:

Other behavior stabilizations

This release also stabilizes some additional behavior, previously only available in preview mode:

Thank you!

Thank you to everyone who provided feedback regarding the changes included in Ruff's preview mode and to our contributors. It's an honor building Ruff with you!


View the full changelog on GitHub.

Read more about Astral — the company behind Ruff.

Thanks to Zanie Blue, David Peter, Micha Reiser, and Alex Waygood who contributed to this blog post.

The Daily Front Page 12 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Django, Delightfully Pragmatic
article

Some more things about Django I've been enjoying

by surprisetalk·▲ 153 points·94 comments·jvns.ca ↗
Some more things about Django I’ve been enjoying.

Hello! I’m on a funny journey right now where I’m trying to learn how to make websites in a sort of 2010 style, where I have an SQL database and render some HTML on the backend.

It’s kind of an interesting journey because it doesn’t necessarily feel “easy” to me to make websites in this way: I never learned how to do it in the 2000s or 2010s, and there’s a lot I need to learn.

So here are some Django features that make building this kind of site feel more achievable than when I was trying and failing to use Go’s standard library or Flask. And I’ll talk about a couple of issues with Django I’ve run into.

why learn to make websites like it’s 2010?

Previously the toolkit I felt confident with for making websites was:

  • static site generators (like for this blog)
  • static sites that do some fun stuff with Javascript (like this sql playground)
  • simple Vue.js single page apps with either a Lambda as a backend or a Go backend (like mess with dns)

I really liked this frontend-heavy approach for these super simple applications but when I started thinking about making something with a lot of different pages (instead of literally just one page), I didn’t feel so excited about the options I saw that involved a lot of frontend code. So I figured I’d try the backend.

Writing a backend-focused site that uses as little JS as possible feels the same to me in a way as writing a single-page JS website that does as little on the backend as possible, even though they might seem like opposites. In both cases I’m just trying to keep as much of the logic as possible in one place.

Now for some thoughts about Django!

I’m enjoying query builders

I learned that I can define a “query set” class in Django with a bunch of methods with different WHERE statements I might want to use while constructing a query:

Here’s how I use it in my view code once I’ve defined what all the methods mean:

Events.objects.approved()
  .for_tab(tab)
  .with_festivals(tab_params.festival_slugs)
  .is_free(tab_params.free)
  .is_outdoors(tab_params.outdoors)

and here’s how I define the methods:

class EventQuerySet(SearchableQuerySetMixin, models.QuerySet):
    def approved(self):
        return self.filter(approved_at__isnull=False)

    def future(self):
        today = timezone.localdate()
        return self.filter(end__gt=self._midnight(today))

    def with_tags(self, tags):
        if tags:
            return self.filter(tags__name__in=tags).distinct()
        return self

The syntax for defining the filters isn’t my favourite, but I spend most of my time just using the methods, and it feels super readable and nice to use, and it makes me want to look into other query builder libraries in the future. In the past I thought “I know SQL, who needs a query builder?”, but this kind of structure does make it really nice to read.

I found an example of someone who wrote their own small query builder in Python that I want to read later to think about whether I would enjoy using a more minimal version of this.

the template filters are awesome

There are a bunch of little quality of life filters available in Django templates that are super useful for generating HTML. The ones I’ve used so far are:

  • translating plain text URLs into links, or line breaks into <br> ({{ event.description|urlize|linebreaksbr }})
  • formatting dates ({{ row.date|date:"M j" }})
  • json_script, which takes a Python dictionary and automatically converts it to JSON and inserts it into the HTML as a <script> tag in a safe way

These are all small things individually but I feel like it makes a big difference somehow to just have them available.

querystring is cool

I think my favourite template filter is querystring: in this site sometimes we use filters like ?date=2026-06-01 to decide what’s displayed. querystring that will make a link to the same query string with one change, like this to link to the previous date:

<a href="{% querystring date=nav.prev_date%}">

Or to remove the outdoors parameter:

<a href="{% querystring outdoors=None %}">

automatic database migrations are still great

I still really love Django’s automatic database system. It’s amazing to be able to just edit a model to add a new field or whatever, and then Django automatically generates the migration.

So far we have done 19 database migrations and I think there will probably be more! It makes a huge difference for me to be able to just easily change the database as my understanding of the problem changes.

I do not want to organize my code with inheritance

Django’s documentation sometimes offers the option of using class-based views and inheritance to organize the code in your views. For example I have four views that share a lot of code, and I could use inheritance to manage that by defining some kind of parent class and then having my other views inherit from it.

I tried it out and I did not enjoy the experience of using inheritance to share code between views. I switched to using functions instead, sort of how this post advocates, and that was a lot more straightforward. I’ve never had a good experience using inheritance in Python and I don’t think I’ll try to use it again.

But I don’t mind using inheritance to use the interfaces Django itself provides: for example if I want to define a query set I need to write something like class EventQuerySet(SearchableQuerySetMixin, models.QuerySet). I don’t think too hard about it and it seems to work.

(as a meta comment: I’ve been working on talking about my programming opinions by just saying “THING does not feel good to me, I prefer OTHER THING instead”. That post I linked to says that function-based views are the “right way”. I’m not very invested in whether it’s “right”, but it’s validating to know that other people feel similarly to me about inheritance)

I don’t know how to think about Django performance

At some point the LLM scrapers discovered our site, and started sending us maybe 10 requests per second. I blocked them which is working for now, but it made me think about what the site’s capacity is. I’m used to writing Go backends where the performance situation is pretty straightforward (usually everything is just fast enough), and a Django site is very different.

Some light load testing (with (ab -n 1000 -c 1) shows that right now we can serve about 2-3 requests per second (on a ~$10/month VM).

It’s tempting for me to go down a rabbit hole where I do a bunch of profiling to figure out what’s slow and try to make it faster (there’s py-spy for that, and py-spy is great and super easy to use, and profiling is fun!) But I really don’t understand what I should expect in terms of performance from a Django site and how I should be thinking about at a higher level.

Some things I haven’t figured out yet:

  • If I have a site that’s going to be getting occasional bursts of traffic, do I want to be able to scale up?
  • Do I want to design the site so that more things can be cached? (and do I really have to? caches are so annoying to get right!)
  • The django performance docs say that Jinja is faster for templating, do I want to think about switching templating systems?
  • Those docs also say “{% block %} is faster than using {% include %}”, I wonder if it’s a big difference and if so why

template caching might be important

I think one thing I’m learning about Django is that because it’s a Framework (tm), it’s easy to accidentally misconfigure it. For example, when I was thinking about why my site was slow just now, I read the django performance docs and I noticed a comment saying:

Enabling the cached template loader often improves performance drastically, as it avoids compiling each template every time it needs to be rendered.

When I’d done CPU profiling I’d noticed that it was spending a lot of time rendering templates! Maybe this could help me!

Clicking through the link, I saw that the cached template loader was supposed to be on by default, but I’d turned it off by accident while trying to do something else. I think this “I turned off the cached template loader by default” things is an example of how I still find the django settings file to be pretty confusing and difficult. I guess I should just be careful when I go in there.

After turning on template caching, it seems like the site can now pretty easily handle 12 requests per second or so without using all of the CPU. I have not carefully benchmarked the before and after but it seems like it’s made a pretty big difference.

One thing that’s been surprising to me about Django performance is that I’ve always heard the advice “if you have a performance problem, check your database queries! Maybe add an index!”. But I’ve been running into a variety of performance issues (like this template caching thing) that are not because of slow queries, so instead it’s been more useful for me so far to start by running a CPU profile. And since I’m using SQLite, any slow database query problem will show up on the CPU profile anyway.

Anyway I don’t want to get too far into site performance. Like I said it’s easy for me to get interested in profiling, but actually I know a lot about profiling and it’s not the most important thing for me to learn about.

that’s all for now!

I might say more about what I’m enjoying (or having a hard time with!) about Django later. Trying to write some shorter blog posts recently.

The Daily Front Page 13 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Sentence and Sense
article

How to write English prose (2023)

by geneticdrifts·▲ 111 points·53 comments·thelampmagazine.com ↗
If you must choose between elegance and perfect clarity, choose elegance.

The Ideals

There are few if any passages in the works of Sir Thomas Browne that I do not find thoroughly delightful; but two afford me particularly intense pleasure. One is the opening paragraph from his essay “On Dreams”:

Half our dayes wee passe in the shadowe of the earth, and the brother of death exacteth a third part of our lives. A good part of our sleepes is peeced out with visions, and phantasticall objects wherin wee are confessedly deceaved. The day supplyeth us with truths, the night with fictions and falsehoods, which uncomfortably divide the natural account of our beings. And therefore having passed the day in sober labours and rationall enquiries of truth, wee are fayne to betake ourselves unto such a state of being, wherin the soberest heads have acted all the monstrosities of melancholy, and which unto open eyes are no better then folly and madnesse.

And the other is the final paragraph from the second chapter of the fifth book of the immense, glorious, and shamefully neglected miscellany Pseudodoxia Epidemica, entitled “Of the Picture of Dolphins”:

And thus also must that picture be taken of a Dolphin clasping an Anchor: that is, not really, as is by most conceived out of affection unto man, conveighing the Anchor unto the ground: but emblematically, according as Pierius hath expressed it, The swiftest animal conjoyned with that heavy body, implying that common moral, Festina lentè: and that celerity should always be contempered with cunctation.

To my mind, each is in its own way a perfect, exquisitely faceted gem of English prose from an especially glorious literary epoch. The music of the one has haunted me for most of my life; the gleeful perversity of the other has lost none of its power to make me laugh in nearly four decades. And, however great the joy I take in either of these passages in isolation, it is as nothing compared to the idiot bliss I derive from their juxtaposition. Taken together, they ideally illustrate the two extremes of the great man’s voice: on the one hand, its glowing beauty and spacious sonority; on the other, its anfractuous density and heedless flamboyance.

One really would have to have a miserly spirit not to love both. Browne’s prose is a magnificent Baroque palace, by dizzying turns grandiose or lyrical, opulent or elegant, monstrous or precious, inordinate or harmonious, carelessly vast or pedantically exact—and always magnificent. All its outlandish and scintillating mannerisms are just so many volutes and modillions, Solomonic columns and gilded cornices, quadrature and mirrored halls. And all of it is a monument to a brief enchanted period in the seventeenth century when English had achieved the whole range of its expressive powers, and when its greatest writers were not yet burdened by any bad conscience about employing those powers to their fullest. Never again would English letters enjoy such a state of innocent sophistication (or sophisticated innocence).

And yet, of course, it was also the age of the King James Bible, which is so often praised for exhibiting precisely the opposite virtues: simplicity, clarity, plain diction. All of which is true enough, admittedly: the King James is perhaps the greatest feat of pellucid phrasing in the history of English letters. But is that the whole story?

Remember now thy Creator in the days of thy youth, while the evil days come not, nor the years draw nigh, when thou shalt say, I have no pleasure in them; While the sun, or the light, or the moon, or the stars, be not darkened, nor the clouds return after the rain: In the day when the keepers of the house shall tremble, and the strong men shall bow themselves, and the grinders cease because they are few, and those that look out of the windows be darkened, And the doors shall be shut in the streets, when the sound of the grinding is low, and he shall rise up at the voice of the bird, and all the daughters of musick shall be brought low; Also when they shall be afraid of that which is high, and fears shall be in the way, and the almond tree shall flourish, and the grasshopper shall be a burden, and desire shall fail: because man goeth to his long home, and the mourners go about the streets: Or ever the silver cord be loosed, or the golden bowl be broken, or the pitcher be broken at the fountain, or the wheel broken at the cistern. Then shall the dust return to the earth as it was: and the spirit shall return unto God who gave it. Vanity of vanities, saith the preacher; all is vanity.

This, from Ecclesiastes, is definitely a grand and gradual music, luminously clear in many respects; but it is not exactly austere; it is also quite complex in its cadences and syntax. True, its gleaming paratactic flow contrasts strikingly with Browne’s massy hypotactic architectonics. And yet the difference is nowhere near so absolute as one might initially be tempted to think. Read once again the first few sentences of the passage from “On Dreams” above. When one places the King James alongside the prose not only of Browne but of all the great English writers of the period, over a period of a few generations—John Florio (1552–1625), Lancelot Andrewes (1555–1626), John Donne (1572–1631), Robert Burton (1577–1640), Thomas Hobbes (1588–1679), Izaak Walton (1593–1683), John Dryden (1631–1700), Thomas Traherne (c. 1636–1674), Joseph Addison (1672–1719), and so on—one cannot help but feel that one is moving back and forth along a single continuum: running not, as we tend to think of it today, from the gaudily ostentatious to the unpretentiously plain, but rather from the beautiful to the sublime, in the classical, pre-Kantian sense of those terms. Taken in that way, the “beautiful” is a style that abounds in sparkling ornamentation while the “sublime” is a style of majestic restraint, pitched “below the threshold” (sub limine) of the temple, a plangent bareness whose rhetorical power somehow exceeds that of the most spectacular oratorical adornments.

Every great national prose, in just about any tongue, reaches its high meridian only by way of a prolonged and constant negotiation of just this tension between beauty and sublimity—between the decorative and the august, or between the splendid and the lucid. And this comes only at the end of long epochs of development. To be able to balance expressiveness and reticence, or to know when to cast that balance away, requires tact and ingenuity and taste on the part of writers; but it also requires a language of sufficient maturity. This is why prose of any consequence invariably arrives far later in a culture’s history than does great poetry. Poetry entered the world almost as early as words did; it is the first flowering of language’s intrinsic magic—its powers of invocation and apostrophe, of making the absent present and the present mysterious, of opening one mind to another. It comes most naturally to languages in their first dawn, when something elemental—something somehow pre-linguistic and not quite conscious—is still audible in them. Prose, however, evolves only when that force has been subdued by centuries upon centuries of refinement, after unconscious enchantment has been largely mastered by conscious artistry, and when the language has acquired a vocabulary of sufficient richness and a syntax of sufficient subtlety, and has fully discovered its native cadences. In English, as in French, this happened in the early modern period, beginning in the late fifteenth century and reaching an unsurpassable zenith in the seventeenth.

By then, moreover, English had amassed the most varied, magnificently farraginous hoard of words in any European tongue, full of Teutonic thunder and purling Latinity, but also enriched with every other verbal plunder it could seize from abroad. No other language could achieve so deep a range of organ-tones, or boast so enormous a collection of pipes and stops, or command so huge an acoustic space. Certainly no other could have produced the sort of wild polyphony and gorgeously wanton dissonances one hears in Macbeth’s

Will all great Neptune’s ocean wash this blood

Clean from my hand? No; this my hand will rather

The multitudinous seas incarnadine,

Making the green one red.

This is the peculiar genius of the English language: this clash and chaos of radically different tones and textures, and this inexhaustible store of ever more exotic words, with all their ever finer distinctions of association and connotation.

All the great prose stylists of the next couple centuries of English letters would avail themselves freely of this extraordinary instrument’s capacities. Fashions would shift over time. The sensibility of the eighteenth century for a time moved more toward the Latinical registers, that of the nineteenth for a time more toward the Anglo-Saxon; but in every generation writers of any significance understood the magnitude of the musical forces at their disposal. And—well, here, take a typical passage from Thomas De Quincey in Confessions of an English Opium-Eater:

The ocean, in everlasting but gentle agitation, and brooded over by a dove-like calm, might not unfitly typify the mind and the mood which then swayed it. For it seemed to me as if then first I stood at a distance, and aloof from the uproar of life; as if the tumult, the fever, and the strife, were suspended; a respite granted from the secret burthens of the heart; a sabbath of repose; a resting from human labours. Here were the hopes which blossom in the paths of life, reconciled with the peace which is in the grave; motions of the intellect as unwearied as the heavens, yet for all anxieties a halcyon calm: a tranquillity that seemed no product of inertia, but as if resulting from mighty and equal antagonisms; infinite activities, infinite repose.

I do not know exactly why, in the twentieth century, the dominant fashions in English prose moved relentlessly in the direction of ever greater simplification and aesthetic minimalism. I do not even entirely regret it. Tastes change, and some of the change has been a corrective of certain excesses of the past. But, on the whole, the result has been a kind of official dogma in favor of a prose so denuded of nuance, elegance, intricacy, and originality as to be often little better than infantile, not only in vocabulary but also in artistry and expressive power—a formula, that is, for producing writers whose voices are utterly anonymous in their monotonous ordinariness. Most of the fiction one reads today in literary journals is atrociously written, as are most of the essays, principally because writers have been indoctrinated in a style so rigid, barren, brutal, dry, and idiotically naïve that the best it can elicit from them is competent dullness. And who can tell one author from another?

Simplicity is difficult, after all, no less than complexity. Both require taste and skill. Neither is less artificial or more natural than the other. Both are necessary for good writing. And when either becomes a forced regimen, exclusive of the other, the results can be only hideous. Good writing is produced not by forsaking the beautiful for the sublime or the exorbitant for the restrained, but by finding new ways of orchestrating the interplay between them. Now, all the authorities of the age seem to concur, the literary performer should treat the organ’s console as a collection of decadent temptations to be resisted; he or she should confine the performance to a single manual, played with two fingers, with no stops pulled and the pedals never so much as brushed by an errant shoe-tip.

The Exemplars

I could, had I but world enough and time, fill volumes with passages from hundreds of masters of English prose to illustrate my notion of what the best writing looks and sounds like. Here, though, I have chosen five authors whose writing I especially love. To keep myself from expressing too much of my own idiosyncratic tastes, however, I have in each case reproduced a passage that I know others have also praised and excerpted in the past. I will add only that, if you wish for an accelerated tutelage in good writing, you could do far worse than to take these five for your teachers.

Robert Louis Stevenson:

It was by this time about nine in the morning, and the first fog of the season. A great chocolate-colored pall lowered over heaven, but the wind was continually charging and routing these embattled vapors; so that as the cab crawled from street to street, Mr. Utterson beheld a marvelous number of degrees and hues of twilight; for here it would be dark like the black end of evening; and there would be a glow of a rich, lurid brown, like the light of some strange conflagration; and here for a moment, the fog would be quite broken up and a haggard shaft of daylight would glance in between the swirling wreaths. The dismal quarter of Soho seen under these changing glimpses, with its muddy ways, and slatternly passengers, and its lamps, which had never been extinguished or had been kindled afresh to combat this mournful reinvasion of darkness, seemed, in the lawyer’s eyes, like a district of some city in a nightmare. (Dr Jekyll and Mr Hyde)

Sylvia Townsend Warner:

It was not beauty at all that she wanted, or depressed though she was, she would have bought a ticket to somewhere or other upon the Metropolitan railway and gone out to see the recumbent autumnal graces of the country-side. Her mind was groping after something that eluded her experience, a something that was shadowy and menacing, and yet in some way congenial; a something that lurked in waste places, that was hinted at by the sound of water gurgling through deep channels and by the voices of birds of ill-omen. Loneliness, dreariness, aptness for arousing a sense of fear, a kind of ungodly hallowedness—these were the things that called her thoughts away from the comfortable fireside. (Lolly Willowes)

J.A. Baker:

The valley sinks into mist, and the yellow orbital ring of the horizon closes over the glaring cornea of the sun. The eastern ridge blooms purple, then fades to inimical black. The earth exhales into the cold dusk. Frost forms in hollows shaded from the afterglow. Owls wake and call. The first stars hover and drift down. Like a roosting hawk, I listen to the silence and gaze into the dark. (The Peregrine)

Patrick Leigh Fermor:

Scattered with poppies, the golden-green waves of the cornfields faded. The red sun seemed to tip one end of a pair of scales below the horizon, and simultaneously to lift an orange moon at the other. Only two days off the full, it rose behind a wood, swiftly losing its flush as it floated up, until the wheat loomed out of the twilight like a metallic and prickly sea. (Between the Wood and Water)

Vladimir Nabokov:

I recall one particular sunset. It lent an ember to my bicycle bell. Overhead, above the black music of telegraph wires, a number of long, dark-violet clouds lined with flamingo pink hung motionless in a fan-shaped arrangement; the whole thing was like some prodigious ovation in terms of color and form! It was dying, however, and everything else was darkening, too; but just above the horizon, in a lucid, turquoise space, beneath a black stratus, the eye found a vista that only a fool could mistake for the square parts of this or any other sunset. It occupied a very small sector of the enormous sky and had the peculiar neatness of something seen through the wrong end of a telescope. There it lay in wait, a family of serene clouds in miniature, an accumulation of brilliant convolutions, anachronistic in their creaminess and extremely remote; remote but perfect in every detail; fantastically reduced but faultlessly shaped; my marvelous tomorrow ready to be delivered to me. (Speak, Memory)

The Rules

To propose a list of rules for writers is probably a very presumptuous thing to do. The only authority it can possibly have is one’s own example, and so offering it to the world is something of a gamble. One has to assume that one’s own writing is impressive enough to most readers to provide one with the necessary credentials for the task. If one is wrong on this score, issuing those rules will invite only ridicule. I mean, for goodness’ sake, Steven Pinker (of all people) published a book on style. How can anyone take that seriously?

Not that being a good writer is a guarantee that one has any great gift for instructing others in the art. E.B. White was an absolutely splendid stylist; he produced a prose so limpid that he was able to fool even himself that it was a triumph of simple diction rather than of (as was actually the case) very subtle intricacy. But he was also the chief perpetrator of Strunk and White’s Elements of Style, by far the most influential and most pernicious book of its kind in English: a total congeries of fatuous advice and grammatical ignorance. Similarly, George Orwell was a perfectly competent (if rather boring) stylist; and yet his celebrated essay “Politics and the English Language,” which was intended as a rebuke of obscurantist jargon, endures now mostly as a manifesto of literary provincialism. Had either White or Orwell followed his own turgid counsels with any fidelity, neither would be nearly as fondly remembered as he is.

Anyway, taking all things into account, I offer the following only to those who like my writing, or who at least think it accomplished enough to make me a credible authority on these matters. These are, if nothing else, the rules to which I adhere and that best express my literary tastes. The first three arise, in fact, from my own direct encounters with editors and critics.

Vocabulary:

  1. Always use the word that most exactly means what you wish to say, in utter indifference to how common or familiar that word happens to be. A writer should never fret over what his or her readers may or may not know, and should worry only about underestimating them. As Nabokov said, a good reader always comes prepared with a dictionary and never resents being introduced to a new term. I call this the “ultracrepidarian rule,” simply because an editor once tried unsuccessfully to dissuade me from writing about a certain “polemicist who stumbles across unseen disciplinary boundaries in an ultracrepidarian stupor.” The editor lost that argument because there is absolutely no other word in the English language that so exactly means what I wanted to say.

  2. Always use the word you judge most suitable for the effect you want to produce, in terms both of imagery and sound, as well as of the range of connotations and associations you want to evoke. This I call the “hyaline rule” on account of a sentence that appeared in a book of mine entitled The Doors of the Sea: “At the shorelines, the lovely glistening hyaline waters were all at once polluted with the silt and débris and murk of the ocean’s bed, and rose with such terrifying suddenness that very few—even as far away as Sri Lanka—had sufficient time to flee.” An indignant reader complained that I might just as easily have used the word “glassy” instead, as any decent unpretentious soul would have done. But I had chosen “hyaline” for very particular reasons: it is a precise word, meaning “glassy” in the sense principally of crystalline translucency; it had exactly the right sound for the sentence—three syllables, the lovely long-i vowel sounds, the equally lovely liquid “l” and smoothly glistening “n,” all of which gave it a glassy and watery feel on the tongue; and it was the perfect word in the context of that book because it echoes the book of Revelation’s thalassa hyalinē, “the sea of glass like unto crystal” before God’s throne, as well as Milton’s “On the clear hyaline, the glassy sea . . .” Perhaps no reader is likely to be aware of all of that; but I knew what I was doing, and so any other word would have been a craven capitulation to the ordinary.

  3. When the occasion presents itself for using an outlandishly obscure but absolutely precise and appropriate word, use it. I call this the “pogonotrophy rule,” because I once wrote a review in the Times Literary Supplement of a book by Rowan Williams, at that time Archbishop of Canterbury, after a dreadfully stupid journalist had suggested that his reputation as an intellectual was a consequence only of his lavish beard. This gave me an opportunity to use that wonderful word, which I had long been holding in reserve for just the proper moment. Such an opportunity would certainly have never come again; if I had let it pass unexploited, I should have carried the grief of it to my grave.

  4. Never use a word simply because it is obscure, but never hesitate to use a word on account of its obscurity either. If you show off by being punctiliously precise, as per rule one above, all the grand rococo ornamentation you could ever wish for your prose will spring up all on its own.

  5. Do not use a thesaurus. Lists of putative synonyms do not give you a sense of any word’s most proper meaning and use. If you are trying to recall a word you know that inexplicably refuses to surface in your memory, maybe you will find it in such a volume; and perhaps, if you happen to be writing humorous verse and have come up against an intractable problem of scansion, you might find something suitable there. Otherwise, learn the meanings and uses of words by reading widely (with that dictionary that Nabokov recommends within reach).

  6. The exotic is usually more delightful than the familiar. Be kind to your readers and give them exotic things when you can. In general, life is rather boring, and a writer should try to mitigate that boredom rather than contribute to it.

Style:

  1. Sometimes less is more. More often, more is more and less is less. Sometimes more is the very least one can do for one’s readers.

  2. If you must choose between elegance and perfect clarity, allow yourself a period of decorously agonized indecision, and then always choose elegance.

  3. Never squander an opportunity for verbal cleverness. I once related in print the notorious tale of Schopenhauer throwing an old washerwoman down a flight of stairs, describing him at one point as seizing her by her “wizened weasand.” Self-indulgent, no doubt, but such moments as those make one feel that one has lived to a purpose.

  4. In “Politics and the English Language,” George Orwell proposes six rules, the first of which is a sound admonition against using hackneyed metaphors, but the second of which is “Never use a long word when a short one will do.” This is an idiotic maxim, one that concentrates almost every kind of philistinism in itself. What he should have written was “Never prefer a short word because it is short or a long word because it is long, but always use the word that to your mind best combines sense, felicity, connotation, wit, and sound, without worrying about whether your readers are likely to recognize it.”

  5. Orwell also decrees: “If it is possible to cut a word out, always cut it out.” No great writer in the history of any tongue has ever observed this rule, and no aspiring writer should follow it. The correct counsel would be “If a word is so excessive as to mar the effect of a sentence, remove it; but never remove a word simply because it is possible to do so.”

  6. Orwell then commands: “Never use the passive where you can use the active.” This is perhaps the worst rule of style ever proposed by anyone. All of literary history proclaims its imbecility. Instead: “Avoid the passive voice when the active works better and vice versa.” After all, in life we sometimes act and sometimes are acted upon. The causal dialectic between agency and patiency, to use the scholastic terms, is intrinsic to finitude.

  7. Orwell’s next dictate is “Never use a foreign phrase, a scientific word, or a jargon word if you can think of an everyday English equivalent.” All that can be salvaged from this trite and parochial balderdash is “Avoid jargon.” Feel free to use a foreign phrase when it is apt or pleasing to do so, and always do so when it expresses an idea with greater elegance or aphoristic economy than any English equivalent could (for instance, the phrase l’esprit d’escalier). English is a gloriously mongrel tongue, and it has always pillaged other languages for glittering trinkets. Moreover, always—always—employ precise scientific terms in contexts where they are germane.

  8. Orwell’s final injunction is “Break any of these rules sooner than say anything outright barbarous.” Since, however, following his rules would produce barbarous prose roughly half the time, he ought instead to have written, “Ignore these rules, except for the one about hackneyed metaphors and the bit about jargon.”

  9. Strunk and White’s Elements of Style decrees: “Keep related words together.” This is vacuous. Awkward ruptures of sense are obviously to be avoided. Taken as a principle, however, this little axiom is not only bad advice; it is a renunciation of language as such. As any decent student of linguistics knows, one of the chief differences between actual linguistic meaning (on the one hand) and mere ostensive noises and gestures (on the other) is the former’s reliance upon structural rather than spatial proximities. The capacity to qualify a predicative phrase by the interpolation of a subordinate clause (for example) is one of those precious attainments that distinguish us from baboons.

  10. The same book advises: “Write with nouns and verbs, not with adjectives and adverbs.” That is moronic. Better not to write at all than attempt to heed so obscene a piece of puritanical nonsense. Write with every kind of word that serves your ends.

  11. In fact, if you own a copy of The Elements of Style, just destroy the damned thing. It is a pestilential presence in your library. Most of the rules of style it contains are vacuous, arbitrary, or impossible to obey, and you are better off without them in your life. And the materials on grammar and usage are frequently something worse. Some of them are simply inherited fake rubrics—“however” must always be a postpositive, “which” must not be used for a restrictive relative clause, and other nonsense of that kind—all of which are belied by the whole canon of English literature. Others, however, are evidence of surprising ignorance. It is bad enough that the manual insists that one must on principle prefer the active to the passive voice; but it is far worse that it then adduces several supposed examples of sentences in the passive voice that are in fact nothing of the sort. One of them—“There were a great number of dead leaves lying on the ground”—seems to have been chosen simply because “lying” about sounds like a passive sort of thing to do. That neither Strunk nor White knew the difference between a passive construction and an active intransitive verb in the imperfect past tense—or, as the book also demonstrates, the difference between the passive and an active past perfect, or the difference between the passive and an adjectival past participle without an auxiliary verb—is genuinely shocking. It does, however, impart a useful lesson: never mistake a tone of authority for evidence of actual expertise.

  12. All these vapidly doctrinaire injunctions—urging you to write only plain declarative sentences stripped of modifiers and composed solely of words familiar to the average ten-year-old and demanding that you always prefer charcoal-gray to sumptuous purple—are expressions of everything spiritually deadening about late modernity and its banausic values. They reflect an epoch in which the mysterious, the evocative, and the beautifully elliptical have been systematically suppressed and nearly extinguished in the name of the efficient, the practical, the mechanical, and the starkly unambiguous—in short, in the name of everything that makes existence uninviting and life boring. They are reflections of an age of bloodless capitalist economism, the reign of brutally common sense, the barbarian triumph of function over form, a spare, Spartan civic architecture of featureless glass and steel and plastic, a consumerist society that lives on the ceaseless production and disposal of intrinsically graceless conveniences. Learn to detest all of these things and you will be a better writer for having done so.

  13. Always read what you have written aloud. No matter how elaborate your prose, it must flow; it must feel genuinely continuous. This is not to say one must imitate natural speech; it is only to say that one must try to capture its rhythms. If what you have written is awkward on your tongue, then it is awkward on the page.

Models:

  1. Bad writing is rarely mistaken for good by the discerning, but it can often be mistaken for great. Keep this in mind when considering the work of authors you are tempted to emulate.

  2. Truly great writing is often inimitable, simply because the better a writer is, the more distinctive his or her voice tends to be. Keep this also in mind when considering the work of authors you are tempted to emulate.

  3. If you have ever taken a course in “creative writing,” try to remember as vividly as possible the kind of prose you were encouraged by your teacher to write, and then do your very best to avoid writing that way.

  4. If you were told in school that Hemingway’s Old Man and the Sea is a specimen of good writing, disabuse yourself of this folly. It is in fact an excruciating specimen of bad schoolboy prose, written by a man who by that point had, alas, been too often drunk, too often concussed, and too often praised.

  5. For American writers in particular, and especially young American writers, and most especially young male American writers: There is on these shores an indigenous tradition of the “American Sublime”—though in many cases it might better be called “American Fustian.” One encounters it at its worst in William Faulkner and Thomas Wolfe when they are at their worst, as well as in a number of other authors whose names I here omit. We as a people like to strive for grand effects, often vastly in excess of any plausible occasion for doing so. Whether this is because of the presence of our magnificent landscape or because of the absence of a long cultural history I cannot guess. I would not say that you must resist the lures of this style altogether. It is there also to be found in the best of our literature—in Melville and Emerson, Muir and Thoreau, and so on—and there it is often glorious. Still, yield to it only to the degree that you can control the forces you set loose. Otherwise, you will lapse into inadvertent parody.

Punctuation:

  1. A writer who disdains the semicolon is a fool. In fact, hostility to this most delicate and lyrical of punctuation marks is a sure sign of a deformed soul and a savage sensibility. Conscious life is not a brute concatenation of discrete units of experience; it is often fluid, resistant to strict divisions and impermeable partitions, punctuated by moments of transition that are neither exactly terminal nor exactly continuous in character. Meaning, moreover, is often held together by elusive connections, ambiguous shifts of reference, mysterious coherences. And art should use whatever instruments it has at its disposal to express these ambiguous eventualities and perplexing alternations. To master the semicolon is to master prose. To master the semicolon is to master language’s miraculous capacity for capturing the shape of reality.

  2. Second only to the semicolon in subtlety, fluent beauty, and whimsy is the dash. Cherish it. Use it with abandon.

Readers:

  1. Those who read only to be informed and never to delight in the words on the page have every right to do so. But do not write for them.

  2. The only book reviewers of any significance are themselves distinguished writers. Cultivate critical intelligence in yourself and try to read your own work with impartiality; but studiously ignore criticism from the unaccomplished.

  3. Do not write down to what you presume to be the level of your readers (unless you are writing specifically for very small children). To do so is an injustice both to them and to you. Even if your suppositions regarding them are correct, you should do them the honor of assuming they know what you know, or can learn it, or are at least willing to try. True, some readers become indignant at their own inability to follow prose of any complexity or to recognize words any more obscure than those they are accustomed to using when talking to their dogs. Invariably they will blame the author rather than themselves. You owe them absolutely nothing. If you attempt always to descend to the lowest common denominator, you will never hit bottom, but you will certainly end up losing the interest of better readers. Ours is, sadly, an age of declining literacy and attention spans, and the situation grows worse by the year. You simply must not make any concessions to that reality, unless you are prepared in the end to give up on writing altogether.

The Last Things:

  1. Memento mori. One day you will die and go to your long home and your voice will fall silent. You have only so much time to make the treasures of your mind and soul manifest. Do not waste the little span allotted to you producing only work intended for the moment rather than for posterity.

  2. Know the names of things and the names of places. Both are a kind of poetry and both contain mysteries. It is an ancient intuition that to possess something’s proper name is to possess power over it; it is, if nothing else, to share in that thing’s form—its unique manner, that is, of making being’s inexhaustible richness manifest. This is because language is magic.

  3. Language is magic. It is invocation and conjuration. With words, we summon the seas and the forests, the stars and distant galaxies, the past and the future and the fabulous, the real and the unreal, the possible and the impossible. With words, we create worlds—in imagination, in the realm of ideas, in the arena of history. With words, we disclose things otherwise hidden, including even our inward selves. And so on. When you write, attempt to weave a spell. If this is not your intention, do not write.

  4. As you near your life’s end, you will be able to look back over your work with some satisfaction if there have been moments in your prose when you have achieved precisely what you hoped to achieve. Keep an inventory of these in your mind, so that you can return to them when you find yourself depressed, uninspired, or suffering self-doubt. I offer two of my own such moments in parting, not because either is in any sense the best thing I have written, but only because each happened (almost miraculously) to have exactly the form and effect that I wanted it to have before I began to write it.

The first is not even a complete sentence, but only a sequence of fragmentary impressions in a story called “A Voice from the Emerald World”:

The light, palely golden in the fluttering leaves, and between the slowly swaying culms . . . and, when I look up, that great eye of soft luminous blue, fringed by the mercurial sparkle of green and silver leaves . . . that blank, quietly menacing, mysterious gaze . . . .

The second is a short passage from near the end of a novel entitled Kenogaia:

He could even see Kenopolis from here, no longer under a pall of storm-clouds, ringed by the mild aqueous shimmer of the moonlit harbor and bay and sea; now, though, it all looked poignantly diminutive, like a chaotically turreted sandcastle among shallow tidal pools, waiting for the rising surf to break it down, or like a frayed cardboard diorama in a neglected corner of the nursery. Why, he mused, had they ever felt it necessary to flee from something so quaint and ephemeral?

Only I can ever really know what it is about each of them that I find so perfectly pleasing; but, believe me, that knowledge makes all the hard work of writing seem more than worthwhile.

The Daily Front Page 14 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — LLM on an $8 ESP32
repository

Running a 28.9M parameter LLM on an $8 microcontroller

by boveyking·▲ 276 points·69 comments·github.com ↗
★ 1,499⑂ 155 forks Python

28.9M-parameter LLM running on an ESP32-S3

This is a 28.9 million parameter language model that generates text on an ESP32-S3, a microcontroller that costs about $8. It runs on the chip itself, with nothing sent to a server, and it writes each word to a small screen wired to the chip at roughly 9 tokens per second. The last language model people ran on a chip like this had 260 thousand parameters, so this one holds about a hundred times more. It fits because most of the model lives in flash instead of RAM, using an idea from Google's Gemma models called Per-Layer Embeddings.

The numbers

Parameters 28.9M stored (25M of them in a flash lookup table)
Chip ESP32-S3, about $8, with 512KB SRAM, 8MB PSRAM and 16MB flash
Speed about 9.5 tok/s end to end (9.7 tok/s of pure compute)
Connectivity none, everything runs on the device
Model size 14.9MB at 4-bit

Why it is hard, and how it fits anyway

A microcontroller has very little fast memory. The ESP32-S3 gives you 512KB of SRAM. Normally the whole model has to be reachable from there, which keeps you stuck with tiny models, and that is why the previous model on a chip like this had only 260 thousand parameters.

The way around it is to stop putting the model in fast memory at all. Most of a language model's parameters sit in an embedding table, which the model reads from rather than computes on. So you can leave that 25 million row table in slow flash and pull only the few rows each token needs, about 450 bytes, while the small part that does the actual work stays in fast memory. The large model then costs almost nothing to run, because you never load most of it. It just sits in flash and gets sampled a little at a time.

That idea is Google's Per-Layer Embeddings, from Gemma 3n and Gemma 4. Here it runs on the memory layout of a microcontroller instead of a phone or a GPU. As far as I can tell, nobody had tried it on a chip this small.

  SRAM  (fast, tiny)   the "thinking" core, used on every token
  PSRAM (medium)       the output head and working memory
  FLASH (huge, slow)   the 25M-param table, about 6 rows read per token (~450 B)

What it does, and what it does not

The model was trained on TinyStories, so it writes short, simple stories and mostly keeps them coherent. It will not answer questions, follow instructions, write code, or know facts. That limit comes from the small part of the model that does the reasoning, and the memory trick does not change it. What is interesting here is the architecture, fitting a large model onto a tiny chip, rather than what a 28.9 million parameter model can say.

Running it yourself

The firmware, the wiring, and the flashing steps live in firmware/esp32_llm/README.md. The training, ablation, and quantization code is in src/ and experiments/. The full method, the ablations, and the on-chip measurements are written up in RESULTS.md.

Credit

TinyStories is the dataset this trains on: short synthetic stories simple enough that a small model can still learn to write coherently (Ronen Eldan and Yuanzhi Li, Microsoft Research, arXiv:2305.07759). The other half is Per-Layer Embeddings, Google's design from the Gemma models, which is what lets a big model fit on a small chip.

Andrej Karpathy's llama2.c is why a lot of people, me included, believe you can train a tiny language model and run it in plain C at all. This grew out of that.

How this actually went

I left the messy history in the repo on purpose. That includes a bug I found in my own parameter accounting, which had inflated an early number, and the corrected result that followed once I fixed it. The commit history and RESULTS.md show where the numbers moved and why.

The Daily Front Page 15 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — An Album-Sized Streamer
article

I learned PCB design, 3D printing and C just to listen to music

by interfeco·▲ 202 points·41 comments·pentaton.app ↗
I learned PCB design, 3D printing and C just to listen to music.

I always liked to see and touch the cover artwork of the CDs and LPs I bought in the past, but in the end the convenience of digital streaming won me over and I accepted no (or stamp-sized) artwork. Lately I’ve been missing this more and more and ultimately decided to try to do something about it. So I built a streamer.

The hardware

I wanted a device that looks and feels like a vinyl sleeve put on display. Around 12”x 12”, and as thin as possible. I sourced the only display matching my needs: a 17” industrial IPS LCD with a 1920x1920 resolution. This came with an embedded DisplayPort connector and I needed to find a single-board computer in a compute module form that has this connector. Not a lot to choose from, but I managed to find the Radxa CM3.

Since I wanted the device to be thin, I needed to design a carrier board for the CM3 with as little height-above-the-board as possible. I never did this before so there was plenty to learn, from basic electronics and magnetics to high-speed signal routing. Took me four revisions to get to state where everything works as expected.

The board is powered via USB-C PD, has another USB-C for an external DAC, a Gigabit Ethernet port and a 12V trigger in the shape of a 3.5mm jack. The compute module also has Wifi and Bluetooth.

I designed an enclosure in FreeCAD which was also new to me. Turns out parametric CAD and smooth, curved surfaces don’t work well together. I had to write a custom macro to generate the shape I was after. Once I had the design, I tried to 3D print it on my brand new 3D printer, which turned out to be a disaster. Took me some time to learn more about designing for manufacturing, especially for 3D printing.

In the end, I had a small computer looking like an LP sleeve.

The software

I use AirPlay in my home exclusively, so I needed this streamer to support that. I went for the most popular open-source implementation called shairport-sync and made it work on a very scaled down Alpine linux image. I had to learn quite some things about booting single-board computers, device-trees, compiling kernels, controlling display backlight, power management and more.

This gave me a good base but no artwork just yet. So I made a small application that powers the display based on the metadata that is coming from the AirPlay client. It only shows the artwork and nothing more. This was surprisingly challenging, even though this is the area I am most familiar with. Turns out, cross-fading two 4MP images at 60 frames per second on a moderately powerful single board computer is not so easy. I needed to use GPU acceleration to make it happen and look smooth.

Funnily enough, AirPlay itself only transmits low-resolution artwork (around 500x500 pixels), no wonder why large-display streamers don’t exist. Thankfully, I was able build an out-of-band protocol extension into my audio player app, so that full resolution artwork can be sent and displayed in all its glory.

Usage

The device is always on. When the screen is off, it consumes less than 2 watts. It wakes up automatically when streaming starts and turns on the screen and my amplifier via the 12V trigger. At full brightness it needs about 24 watts. I power it using a generic USB-C charger and have my Pro-Ject Amp Box SE connected via a FiiO KA17 DAC. AirPlay does CD quality lossless which is plenty of quality for me.

What’s Next

I am considering creating a Kickstarter for Pentaton LP if there is enough interest. If you don’t want to miss it, sign up for updates here: https://pentaton.app/lp/

The Daily Front Page 16 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Private Eyes, Local Drives
show hn

Show HN: CheapSecurity – Lightweight, Self-Hosted CCTV for Linux SBCs

by zeldone·▲ 126 points·26 comments·github.com ↗
Privacy-first: your video data never leaves your network.

This project provides a lightweight, self-hosted CCTV solution designed for Linux-based single-board computers (SBCs) and standard USB webcams. It offers an affordable, privacy-focused alternative for home monitoring by keeping your video data entirely under your control.

Project Philosophy

  • Privacy-First: By storing all footage locally, this system eliminates the need for third-party cloud subscriptions and ensures your data never leaves your network.
  • Cost-Effective: Leverage existing hardware—such as a spare Linux board and a USB webcam—to build a fully functional surveillance system without recurring fees.
  • Minimalist Architecture: The software is optimized to run efficiently on low-power devices, ensuring high performance even on entry-level hardware.

Key Features

  • Hardware Agnostic: Highly compatible with a wide range of standard USB webcams.
  • Resource Efficient: Optimized specifically for Linux-based boards (e.g., Raspberry Pi, Orange Pi, or similar SBCs).
  • Data Sovereignty: Full control over your storage path, retention policies, and access methods.
  • Simple Deployment: Designed for quick setup and easy maintenance.

Features

  • Live MJPEG stream with a web dashboard
  • Motion detection with frame differencing
  • Automatic recording with a pre-motion buffer
  • Email alerts with a snapshot picture when motion starts
  • Telegram integration:
    • Automatic video upload after motion is recorded
    • Bot commands: /snapshot, /video <seconds>, /help
  • Night mode low-light enhancement (software CLAHE + brightness/contrast boost)
  • Recordings bulk actions: select all, send to Telegram, download ZIP, delete
  • Storage cleanup by age, total size, and emergency low-disk cleanup
  • systemd autostart ready
  • Licensed under GNU AGPLv3

Requirements

  • Python 3.10 or newer
  • OpenCV with V4L2 support (see installation options below)
  • A USB webcam (/dev/video0 by default)
  • Optional: a Telegram bot token for Telegram notifications
  • Optional: SMTP credentials for email alerts

Quick start

  1. Verify OpenCV is available:

    python3 -c "import cv2; print(cv2.__version__)"
    

    If OpenCV is not installed, you have two options:

    • Option A — use a system OpenCV package (recommended on ARM boards such as the Odroid XU4, where a pre-built system package is usually optimized for the board).
    • Option B — install OpenCV via pip (convenient on x86/amd64 machines or when no system package is available). Use opencv-python-headless because the app does not need a GUI.
  2. Create and activate the virtual environment and install the app:

    Option A: system OpenCV (recommended for ARM)

    This creates a venv that can see the system site-packages, so the system cv2 is available inside the venv.

    cd $HOME/CheapSecurity
    python3 -m venv venv --system-site-packages
    source venv/bin/activate
    pip install -e .
    

    Option B: install everything via pip

    Use this if you do not have a system OpenCV or prefer a self-contained venv.

    cd $HOME/CheapSecurity
    python3 -m venv venv
    source venv/bin/activate
    pip install opencv-python-headless
    pip install -e .
    

    Note: On some older ARM boards the pip opencv-python-headless wheel may not be available or may be slow. If that happens, install OpenCV from your distribution’s package manager instead and use Option A.

  3. Find your webcam device (usually /dev/video0):

    v4l2-ctl --list-devices
    
  4. Copy config.json.example to config.json and edit it:

    cp config.json.example config.json
    nano config.json
    
    • Set camera device, resolution, and frame rate
    • Fill in SMTP credentials if you want email alerts
    • Fill in Telegram bot token and chat ID if you want Telegram uploads

    Security: config.json is listed in .gitignore and must never be committed. It contains passwords and tokens. Always edit config.json, not config.json.example. If you add a new setting, update both files so the example stays in sync.

  5. Run the app:

    ./venv/bin/python -m cheapsecurity.app
    
  6. Open the dashboard in a browser:

    http://<odroid-ip>:5000
    

Configuration

Edit config.json:

Section Key Description camera device V4L2 device index (0 = /dev/video0) camera width, height, fps Capture resolution and frame rate camera night_mode Enable low-light enhancement camera night_mode_fps Target FPS in night mode (camera may ignore this) camera night_mode_gain Target analog gain in night mode (camera may ignore this) camera night_mode_brightness Brightness boost in night mode camera night_mode_contrast Contrast boost in night mode motion threshold Pixel difference threshold (0-255) motion min_area Minimum contour area to trigger motion (full-res pixels) motion cooldown_seconds Keep recording after motion stops motion scale Downscale factor for motion detection (saves CPU) recording dir Where videos are saved recording max_duration_seconds Maximum length of one clip recording pre_buffer_seconds Seconds before motion included in clip recording codec Preferred FourCC codec (MJPG for low CPU, mp4v for smaller files) notifications enabled Send email alerts on motion notifications smtp SMTP server, port, username, password, TLS notifications from, to, subject Email sender/recipients/subject (use ["a@...", "b@..."] for multiple recipients) notifications min_interval_minutes Minimum time between alert emails telegram enabled Send videos to Telegram after motion is recorded telegram bot_token, chat_id Telegram Bot API token and destination chat telegram send_video Whether to upload the video file automatically telegram min_interval_minutes Minimum time between Telegram uploads telegram poll_commands Enable /snapshot, /video, and /help bot commands storage max_age_days Delete recordings older than this (default 3 days = 72h) storage max_size_gb Delete oldest files if total exceeds this storage cleanup_interval_minutes How often storage cleanup runs storage delete_old_on_startup If false, old recordings are kept when the app restarts storage emergency_free_space_gb If free disk space drops below this, delete old recordings before a new one storage emergency_delete_count How many oldest recordings to delete in an emergency cleanup web host, port Dashboard bind address and port web stream_scale Downscale factor for live stream (saves bandwidth/CPU) web.auth enabled, username, password Optional HTTP Basic Auth

Telegram setup

1. Create a bot

  1. Open Telegram and message @BotFather.
  2. Send /newbot and follow the prompts to choose a display name and username.
  3. Copy the bot token (looks like 123456789:ABCdefGHIjklMNOpqrsTUVwxyz).
  4. Keep this token secret — anyone with it can control your bot.

2. Get your chat ID

  1. Start a private chat with your new bot and send any message (for example, /start).

  2. Open this URL in a browser, replacing <YOUR_BOT_TOKEN> with the real token:

    https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
    
  3. Look for "chat":{"id":123456789. The number is your chat ID.

    • If getUpdates is empty, send another message to the bot and refresh.
    • If you want to use a group chat, add the bot to the group first and send a message there; the chat ID will be negative for groups.
  4. Copy the chat ID exactly, including the - sign if it is a group.

3. Configure the app

Fill in the telegram section of config.json:

"telegram": {
  "enabled": true,
  "bot_token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
  "chat_id": "123456789",
  "send_video": true,
  "min_interval_minutes": 5,
  "poll_commands": true
}

Then restart the service:

sudo systemctl restart cheapsecurity@$(whoami).service

Automatic uploads

After a motion clip is saved, the video is uploaded to your Telegram chat. Uploads are rate-limited by min_interval_minutes.

Bot commands

From your configured chat, send:

  • /snapshot — receive the current camera picture
  • /video 10 — record and send a 10-second video (1–60 seconds, default 10)
  • /help — list commands

The bot only responds to your configured chat_id.

Motion has priority: if the system is already recording because motion was detected, a /video request will not interrupt it. The bot will reply that a motion video is in progress and will be uploaded automatically.

Email alerts

Configure the notifications section in config.json. A picture from the moment motion starts is attached. Alerts are rate-limited by min_interval_minutes.

Gmail / Google Workspace setup

Google no longer allows "less secure apps" to use your regular Gmail password. You must create an App Password.

  1. Enable 2-Step Verification on your Google account:
  2. Create an App Password:
    • Go to https://myaccount.google.com/apppasswords
    • Select app: Mail
    • Select device: Other (Custom name) — type "CheapSecurity"
    • Click Generate and copy the 16-character password (for example, abcd efgh ijkl mnop).
  3. In config.json, set:
"notifications": {
  "enabled": true,
  "smtp": {
    "server": "smtp.gmail.com",
    "port": 465,
    "username": "you@gmail.com",
    "password": "abcdefghijklmnop",
    "use_tls": true
  },
  "from": "you@gmail.com",
  "to": "you@gmail.com",
  "subject": "CheapSecurity motion alert",
  "min_interval_minutes": 5
}
  • Use the App Password (no spaces) in the password field, not your Google account password.
  • For Google Workspace accounts, the username is usually your full email address.
  • The app uses implicit TLS (SMTP_SSL) on the port you configure. Gmail accepts this on port 465.

Multiple recipients

"to": [
  "you@gmail.com",
  "family@example.com"
]

Night mode

Night mode combines:

  • Software enhancement (CLAHE on the L channel)
  • Camera brightness/contrast boost
  • Attempts to lower FPS and raise gain/ISO if the camera supports it

Toggle it from the dashboard. It is applied to the live stream, recordings, and alert pictures.

Important: most USB webcams do not expose ISO/gain/exposure controls via V4L2, so FPS/gain adjustments may be ignored. True night vision requires an IR-sensitive camera and an IR illuminator.

Storage and cleanup

  • Recordings are saved in recordings/.
  • Recordings older than max_age_days are deleted during periodic cleanup, not on startup (unless delete_old_on_startup is true).
  • If free disk space drops below emergency_free_space_gb, the oldest emergency_delete_count recordings are deleted before starting a new clip.
  • Recordings older than max_age_days or exceeding max_size_gb are removed during periodic cleanup.

Web interface

  • Live stream
  • Status panel (resolution, FPS, recording state, motion state)
  • Settings toggles: night mode, email notifications, Telegram uploads, built-in basic auth
  • Recordings list with per-row checkboxes and bulk actions:
    • Select all
    • Send to Telegram
    • Download selected (ZIP)
    • Delete selected

Production deployment

Do not expose Flask's development server to the internet. Use Gunicorn behind the built-in auth or another reverse proxy you trust.

1. Install Gunicorn

It is already defined in pyproject.toml:

cd $HOME/CheapSecurity
source venv/bin/activate
pip install -e .

2. Run with systemd + Gunicorn

Copy the service template and enable it from a user shell:

sudo cp cheapsecurity.service /etc/systemd/system/cheapsecurity@.service
sudo systemctl daemon-reload
sudo systemctl enable --now cheapsecurity@$(whoami).service

This binds Gunicorn to 0.0.0.0:5000 with one worker and four threads, so the dashboard and stream are reachable directly on your network. Only one worker is used because the camera must be opened by a single process.

Security: if you expose this to the internet, put a reverse proxy with HTTPS and authentication in front of Gunicorn. If you only access it locally, keep the built-in auth enabled.

View logs:

sudo journalctl -u cheapsecurity@$(whoami).service -f

Project structure

CheapSecurity/
├── src/
│   └── cheapsecurity/     # Python package
│       ├── app.py         # Development launcher
│       ├── cctv.py        # Motion detection, recording, alerts, Telegram bot
│       ├── web.py         # Flask dashboard and APIs
│       ├── wsgi.py        # Production WSGI entry point
│       ├── diagnose.py    # Diagnostic/troubleshooting script
│       ├── templates/     # HTML templates
│       └── static/        # CSS/JS
├── tests/                 # Test suite
├── config.json            # Your local settings (gitignored, never commit)
├── config.json.example    # Example settings template (committed)
├── pyproject.toml         # Package metadata and dependencies
├── cheapsecurity.service  # systemd template
├── LICENSE                # GNU AGPLv3
└── recordings/            # Saved videos

Troubleshooting

If recordings stop appearing:

  1. Check the service is running:

    sudo systemctl status cheapsecurity@$(whoami).service
    
  2. Check logs:

    sudo journalctl -u cheapsecurity@$(whoami).service -f
    
  3. Run the diagnostic script:

    source venv/bin/activate
    python -m cheapsecurity.diagnose
    
  4. Try lowering motion.min_area if no motion is detected.

License

This project is licensed under the GNU Affero General Public License v3.0 or later (AGPLv3). See LICENSE.

The Daily Front Page 17 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Reverse Minesweeper
show hn

Show HN: Reverse Minesweeper

by pompomsheep·▲ 202 points·66 comments·sunflowersgame.com ↗
The deepest logic in the game.

Difficulty tuning

Medium: min subset deductions 2

A Medium grid must force at least this many two-clue subset moves. Raise for meatier Mediums.

Hard: min overlap deductions 2

A Hard grid must force at least this many two-clue overlap moves (plus at least one subset move).

Extreme: min what-if deductions 2

An Extreme grid must force at least this many what-if moves: assume a square, watch a clue break. The deepest logic in the game.

Extreme: min chain depth 2

At least one what-if must need a chain this long (in counting steps). 1-step chains feel like overlap moves; 2+ guarantees a genuinely deep moment. Capped by MAX_WHAT_IF_FIRINGS.

Insane: min either-way deductions 0

Insane grids always exceed Extreme's chain cap. Raise this to additionally demand either-way moves: pivots where both hypotheses survive yet agree on another square. The rarest logic in the game; expect closest-match flags above 1.

Slider changes regenerate the grid. Reset defaults

How difficulty works

Every grid you generate is solved end-to-end by the same logic engine that powers the in-game hints. It plays through the whole puzzle one deduction at a time, always taking the simplest move available, exactly like a careful human, and records which reasoning techniques the grid forced it to use. There are five, and each tier is named after the hardest one the grid demands:

  1. Basic → Easy: a single clue settles its own squares: either it's already satisfied, so its remaining squares must all be empty, or it has exactly as many unknowns left as flowers it still needs.
  2. Subset → Medium: two clues compared, where every unknown one clue sees is also seen by the other. Subtracting one from the other settles the squares only the bigger clue sees.
  3. Overlap → Hard: two clues share some squares, and min/max bounds on how many flowers the shared region can hold settle the squares only one clue sees. Hard grids also always include subset moves.
  4. What-if → Extreme: cousin of Sudoku's forcing chains: hypothetically plant a flower (or a cross) in a square, follow the simple rules from there, and watch a clue break: too many flowers, or no way left to reach its number. The contradiction proves the square must be the opposite. Chains are capped at a few simple counting steps (MAX_WHAT_IF_FIRINGS), so every what-if is mentally trackable and no pencil marks are needed. Hints walk you through exactly which square to test and which clue breaks.
  5. Beyond the cap → Insane: the summit. Insane grids demand what-if chains longer than Extreme's cap (up to INSANE_CHAIN_FIRINGS counting steps), and the solver also wields the either-way technique, cousin of Sudoku colouring: try a pivot square both ways; neither hypothesis breaks anything, but both chains force the same value onto some other square, so it's proven with no contradiction at all. This is the one tier where keeping notes genuinely helps. Hints spell out both kinds: which square to test, and (for either-way) which squares both chains agree on.

The 0–100 score places a grid inside its tier's band (Easy 4–19, Medium 20–39, Hard 40–59, Extreme 60–79, Insane 80–100), positioned by its sub-metrics: how wide and forgiving the solve is for Easy, the count of forced subset or overlap moves for Medium and Hard, and, for Extreme, the total depth of the what-if chains rather than their count, since a short 1-step what-if plays much like an overlap move while a 4-step cascade is a different beast. For Insane, the combined work of both branches of every either-way pivot. Bands never overlap, so a 62 is always genuinely harder than a 40.

Estimated time simply adds up the work: about a second per square of routine marking, ~2s to spot each basic deduction, ~14s per subset, ~26s per overlap, ~15s plus ~10s-per-step for each what-if, and ~35s plus per-step costs for each two-chain either-way. The weights live in EST_TIME in the source if the estimates drift from real solve times.

Zero squares follow ZERO_CLUE_RULES: Easy and Medium carry one or two 0 tiles, Hard exactly one, Extreme and Insane none. The generator protects needed 0s during pruning, strips extras first, injects one if a grid comes up short, and rejects anything that still breaks the rules.

The sliders above tighten what a grid must contain before it's accepted. Each click tries up to 90 candidates; if none fits your settings, the closest match is served and flagged under the board: a sign to ease a slider off or move to a bigger board, where demanding grids are more common.

How to play

Place sunflowers in the grid using the number clues.

Each number tells you how many sunflowers are in the surrounding squares.

Double-tap to plant a sunflower (right-click on desktop). Single-tap to mark a square empty, or click and drag to mark a line of empties.

Tap a faded number to fill the remaining empty squares around it.

Wrong sunflowers count as mistakes: you get 3 lives per puzzle. Crosses do not.

Every move can be solved using logic. No guessing is required.

About

Hello friends!

I’m very excited to launch Sunflowers, a new logic puzzle game.

It’s basically a reverse minesweeper game where we plant sunflowers instead of blowing ourselves up.

I hope you like it! And I would love to hear your feedback.

Daniel

Terms

Sunflowers is a browser logic puzzle game. Play for fun. Scores and progress are stored on your device.

We use Google Analytics to understand basic site traffic, and the game is hosted on Cloudflare Pages.

We don’t sell your data or run ads. That’s it.

The Daily Front Page 18 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — HyperCard, Reimagined
article

Decker, a platform that builds on the legacy of Hypercard and classic macOS

by tosh·▲ 292 points·72 comments·beyondloom.com ↗
Decker is a multimedia platform for creating and sharing interactive documents.

Decker is a multimedia platform for creating and sharing interactive documents, with sound, images, hypertext, and scripted behavior. You can try it in your web browser right now.

Decker builds on the legacy of HyperCard and the visual aesthetic of classic MacOS. It retains the simplicity and ease of learning that HyperCard provided, while adding many subtle and overt quality-of-life improvements, like deep undo history, support for scroll wheels and touchscreens, more modern keyboard navigation, and bulk editing operations.

Anyone can use Decker to create E-Zines, organize their notes, give presentations, build adventure games, or even just doodle some 1-bit pixel art. The holistic "ditherpunk" aesthetic is cozy, a bit nostalgic, and provides fun and distinctive creative constraints. As a prototyping tool, Decker encourages embracing a sketchy, imperfect approach. Finished decks can be saved as standalone .html documents which self-execute in a web browser and can be shared anywhere you can host or embed a web page. Decker also runs natively on MacOS, Windows, BSD, and Linux.

For more complex projects, Decker features a novel scripting language named Lil which is strongly influenced by both Lua, an imperative language popular for embedding in tools and game engines, and Q, a functional language in the APL family used with time-series databases. Lil is easy to learn and conventional enough not to ruffle any feathers for users with prior programming experience, but also includes pleasant surprises like implicit scalar-vector arithmetic and an integrated SQL-like query language. A few lines of Lil can go a long way.

Decker provides a small collection of built-in interactive widgets for building interfaces, as well as a facility for defining new ones. Custom widgets and their definitions can be copied and pasted using the system clipboard, which also makes it possible to share them anywhere you can share or store text. Every deck is a toolkit of reusable parts that can be harvested and repurposed for another project.

Decker is command-line friendly: when built from source, it comes with Lilt, a standalone Lil interpreter which can (among other things) read, write, manipulate, and even execute Decker documents "headlessly". Lilt has even fewer dependencies than Decker itself, so it can also be compiled as a cross-platform APE executable, ready for writing run-anywhere shell scripts. Would you believe there's a Lil interpreter that runs on POSIX AWK? Decks are stored in a line-oriented text format which interoperates well with existing source control tools like Git and SVN.

Decker includes no advertising, telemetry, gamification, slop-generator integration, or other intrusions on user privacy and autonomy. If you like Decker, please share it with other people who might enjoy it. Build something that makes you happy.

Examples

Libraries

Documentation

Additional Resources

Browsable source code and a bug-tracker are available on GitHub. Decker is free and open-source, under a permissive MIT license.

Periodic binary release downloads for MacOS and Windows are available on Itch.io. The Itch page includes a community forum for discussing Decker and sharing projects made with Decker. We also hold Decker-themed "Game Jams" every July and December; the next upcoming jam will be Decker Fantasy Camp, July 2026.

The Daily Front Page 19 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — A Meteorite in the Living Room
article

Alien World Chemistry Found Inside Meteorite That Struck New Jersey Home

by spzx·▲ 169 points·64 comments·seti.org ↗
One of the most scientifically valuable meteorites ever recovered.

Fragment of the Hillsborough meteorite, broken on impact, with fusion crust from passing at high speed through the Earth’s atmosphere.

At a Glance

  • A meteorite crashed through the roof of a Hillsborough, New Jersey home on July 16, 2024.
  • The meteorite, named Hillsborough, is only the second observed fall of a rare primitive CM1/2 carbonaceous chondrite, making it one of the most scientifically valuable meteorites ever recovered.
  • The meteorite's pristine condition, preserved by the homeowner immediately after impact, allowed scientists to study fragile minerals and organic compounds rarely seen in recovered meteorites.
  • Researchers found preserved bits from near the surface of the original asteroid where it experienced concentrated salty fluids—a process not previously known from this type of asteroid.
  • Hillsborough contains a diverse suite of carbon-bearing compounds, amino acids, and other prebiotic molecules that help scientists understand what building blocks of life may have been delivered to the early Earth.
  • The findings provide new insight into the role of water, brines, and asteroid chemistry in shaping the organic inventory of the early solar system.
  • The international research team's results are published in Science Advances.

On July 16, 2024 a daytime meteor shook New York City with a sonic boom as it passed just south of the Statue of Liberty. Now, an international team of researchers reports in the journal Science Advances that a short time later, a more than two-pound meteorite crashed through the roof of a house in the town of Hillsborough, New Jersey.

"A forensic study of the fragments revealed that they contained preserved bits from near the surface of a small primitive asteroid where it experienced concentrated salty fluids—a process not previously known from this type of proto planet world," said lead author and meteor astronomer Peter Jenniskens of the SETI Institute and NASA's Ames Research Center in California's Silicon Valley.

The Meteorite Fall

On that day, a rock the size of a heavy airline bag entered the Earth's atmosphere at a speed of 32,000 miles/h (14.4 kilometers per second). Sixty observers from New York, New Jersey, Connecticut, Rhode Island and Pennsylvania reported seeing the meteor to the American Meteor Society, while sixteen in New York and New Jersey felt the shockwave.

"Our cameras in Northford, Connecticut, and Douglassville, Pennsylvania, as well as a doorbell camera in Wayne, New Jersey, captured the meteor, and from that we measured its trajectory," said American Meteor Society operations manager Mike Hankey. "The path traced back to low in the asteroid belt."

Daytime meteor (left), impact site and a fragment of the Hillsborough meteorite.

The rock was fragile and quickly broke into pieces. The meteor stopped being visible at an altitude of 22 miles (35 kilometers). After it faded, a Doppler weather radar at Newark Airport briefly detected a long cloud of falling pebbles stretching from Staten Island into New Jersey. Hillsborough was at the far end of that cloud, where the largest rocks came down. Only one was recovered because it hit a house.

The owner of the house described the scene as follows: "I was at home at the time, heard a loud crash and found a hole in the ceiling of the master bedroom. I smelled a strong sulfur-like odor and saw many black fragments along with debris and black dust that covered my bed, carpet and surrounding areas.”

He then immediately preserved and documented the entire scene using disposable gloves and aluminum foil to place the meteorite fragments in glass jars.

Evidence of Ancient Brines

When scientists examined the rocks, they determined it belonged to one of two known types of primitive meteorites called CM-type carbonaceous chondrites, where the letter "M" refers to the Mighei meteorite that fell in Ukraine in 1889.

According to paper co-author Mike Zolensky, a meteoriticist at NASA's Johnson Space Center in Houston, analysis of the Hillsborough meteorite found fragments that were more extensively altered by water  on the meteorite's parent asteroid than is typically seen in CM2 carbonaceous chondrites and classified the specimen as a CM1/2 carbonaceous chondrite, an intermediate classification between petrographic types CM1 and CM2.

Hillsborough is the 22nd observed CM-type meteorite fall, but only the second witnessed fall of a CM1/2 carbonaceous chondrite, following the Kolang meteorite that fell in North Sumatra, Indonesia, in 2020. All others are CM2-types. No CM1-type falls have been witnessed.

"Thanks to the homeowner's quick reaction, these are the most pristine CM1/2 meteorites we know of," said Jenniskens.

Scientists discovered that this bit of the Hillsborough meteorite is rich in salts and came from near the surface of the parent body asteroid.

Another prominent primitive type of carbonaceous chondrite is called CI, with "I" after the meteorite Ivuna that fell in Tanzania in 1938. Samples of this type were brought back in pristine condition from asteroid Ryugu by JAXA's Hayabusa 2 mission and from asteroid Bennu by NASA's OSIRIS-REx mission. They were found to contain ample evidence of the influence of briny fluids from just below the surface of their parent asteroid.

Zolensky and colleague JangMi Han found small salt-rich CM1 fragments within the Hillsborough meteorite, suggesting they originated from a near-surface region of the parent asteroid where liquid water evaporated and concentrated salts.  They are now working to identify the salt minerals for comparison with similar phases found among samples returned to Earth from asteroids Ryugu and Bennu.

Artist illustration (using ChatGPT) of a CM-type carbonaceous chondrite parent body asteroid with a near-surface brine deposit exposed in an impact crater.

Clues to the Origins of Life

The high concentration of salt in briny fluids can potentially create molecules crucial to life on Earth. Brines allow phosphate to remain in solution and can catalyze chemical reactions between organics and precipitate minerals.

“Isotope studies of carbon and nitrogen suggest that primitive carbonaceous chondrites, including CM-types, delivered organic matter to the early Earth,” said cosmochemist Queenie Chan of Royal Holloway University of London, England, and biogeochemist Nana Ogawa of the Biogeochemistry Research Center at the Japan Agency for Marine-Earth Science and Technology. "The Hillsborough meteorite contained 1.8% by weight of carbon and 0.07% of nitrogen, and had carbon and nitrogen isotopes typical for CM-type meteorites."

The meteorite contained a wide variety of soluble organic compounds, and its compositional range confirms that the Hillsborough meteorite was more altered by water than most other CM-type meteorites.

"A high fraction of compounds were the product of organic chemistry with minerals," said organic mass spectrometry specialist Phil Schmitt-Kopplin of Technical University Munich. "We do not know if these magnesium organic compounds were contributed by brine chemistry or were simply left over from earlier impact shock processes."

In living organisms, organo-metallic compounds are found in blood and used in photosynthesis. Among the soluble organic compounds were also many amino acids, similar to those found in more moderately altered CM2 chondrites.

Astrobiologist Danny Glavin of NASA's Goddard Space Flight Center in Greenbelt, Maryland, and his team in Goddard’s Astrobiology Analytical Lab, concluded that the delivery of amino acids, carboxylic acids, and other soluble organic molecules by CM-type bodies may have contributed to the prebiotic organic inventory that preceded the emergence of life on Earth. Their analysis suggests the complex distribution of amino acids observed in the Hillsborough meteorite formed within the parent body, likely assisted by brine fluid chemistry.

Some of the meteorite fragments will be curated at the American Museum of Natural History in New York City.

"We are thrilled that nature delivered such a precious asteroid sample on our doorstep," said curator Denton Ebel.

Read the original paper: https://www.science.org/doi/10.1126/sciadv.aea2105

About the SETI Institute
Founded in 1984, the SETI Institute is a non-profit, multi-disciplinary research and education organization whose mission is to lead humanity’s quest to understand the origins and prevalence of life and intelligence in the Universe and to share that knowledge with the world. Our research encompasses the physical and biological sciences and leverages expertise in data analytics, machine learning and advanced signal detection technologies. The SETI Institute is a distinguished research partner for industry, academia and government agencies, including NASA and NSF.

About JAMSTEC’s Biogeochemistry Research Center

**JAMSTEC Logo**The Biogeochemistry Research Center (BGC) at the Japan Agency for Marine-Earth Science and Technology (JAMSTEC) investigates the origins and evolution of life-related primordial organic matter through chemical and isotopic analyses. Its research includes studies of asteroid samples (i.e., Ryugu, Bennu) and carbonaceous meteorite samples, contributing to our understanding of the pristine chemical processes in molecular evolution.

Contact information
Rebecca McDonald
Director of Communications
SETI Institute
[email protected]

The Daily Front Page 20 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Underneath New York
article

What's Under Your Feet in New York City?

by sohkamyung·▲ 181 points·41 comments·practical.engineering ↗
There’s a whole other world of infrastructure below the surface.

[Note that this article is a transcript of the video embedded above.]

New York City is unlike any other city in the United States. It’s the most densely populated area in the country by far, absolutely packed with buildings, business, homes, and people. But for all that you can see walking the streets or flying overhead, there’s a whole other world of infrastructure that makes the city possible below the surface. What if you could peel back the paving and soil to see what one of my favorite authors, David Macaulay, called the city’s “massive root system.” Let’s take a tour underneath the Big Apple. I’m Grady and this is Practical Engineering.

Let’s pick a generic Manhattan intersection to start this journey. You’ve got cars, buses, bikes, buildings, sidewalks, traffic lights, fire hydrants, hotdog stands, manholes, and more. But open it up and there’s the whole other world below the surface. We’ll start with the water lines.

If you think about it, water pipes could run overhead like electrical wires. Digging trenches is a lot of work, after all. And fixing underground pipes is pretty disruptive to streets. Of course, New York isn’t unique in having them run underground. It’s standard practice across most of the world for a lot of simple reasons: water is heavy, so continuous support along the length of a pipe is a structural convenience. Water also freezes, so putting pipes underground below the frost line prevents them from freezing in the cold winters. It also protects them from a whole host of natural and human-caused hazards like car crashes and rogue parade balloons. That’s important because a broken water main can be a big problem.

When you see a huge rooster-tail of water spraying from the street, it’s easy to wonder why we need water mains to run at such high pressures. Of course, pressure helps move water through pipes to all the individual places where it’s needed across the city. But maybe more importantly, that pressure pushing out keeps contamination from getting in. You want any crack, hole, or break in a water main to be a one-way street. If anything’s moving from one side of the pipe wall to the other, it’s pretty important that it happens from in to out.

Like electricity, water lines typically run in somewhat of a grid pattern. This adds redundancy, providing multiple paths for water to reach a destination so that taking a line out of service doesn’t disrupt the flow to residents. It also makes sure that all the water in the pipes is constantly flowing. If you build a water distribution system like the branches of a tree, you end up with a lot of dead-ends where water can slow down or even stagnate, making it unsafe to drink.

New York City’s water system is famously gravity fed, with most of the source water coming from upstate at a higher elevation. It also requires no filtration because the source watersheds are fiercely protected to keep contamination out. But the City doesn’t just assume things are good. Dotted throughout the streets are more than 900 water sampling stations that let officials collect and test the quality of the water at the end of the distribution system to make sure it’s safe to consume.

If you could peel back the soil and look at the city’s water distribution system, you’d see water mains down nearly every street; shutoff valves used to isolate individual lines for maintenance or repairs, connections to street and wall hydrants where firefighters can hook up their engines, and service lines that tap into the mains to supply each individual building. You’ll notice that few utilities run under the buildings themselves. The main reason is that we need to be able to access them to fix them if needed. The other reason is that buildings often have their own underground structures, specifically piles, piers, or drilled shafts that serve as their foundation. I have a whole video on deep foundations if you want to learn more after this.

Unlike water pipes, it is pretty typical to see electrical distribution lines running overhead on utility poles everywhere across the globe. You won’t see this in most parts of New York City, though. Roughly 85 percent of the electrical lines are underground. Part of it’s about looks: lines clutter up the space and require dedicated rights of way that limits the use of that space. Another part is safety: keeping people and vehicles free and clear of distribution level voltages. And, of course, there’s reliability. When a heavy storm takes out an above-ground utility pole in a suburban neighborhood, the ensuing power outage is an inconvenience. That same outage in Manhattan could affect a lot more people.

There’s a lot of confusion about underground electrical service. You can kind of divide the grid into three distinct categories defined by voltage ranges: there’s transmission (where power moves over very long distances at hundreds of thousands of volts), distribution (where it’s carried throughout a populated area at a a few thousand volts), and finally service (the voltage at the plug). Putting service lines underground is pretty straightforward. You might even have an underground line at your house running to a lamp or a detached garage. Putting transmission lines at hundreds of thousands of volts underground is a pretty extreme engineering challenge because of insulation, heat buildup, and capacitance. Undergrounding distribution lines lies somewhere in the middle.

One of the big upsides of running lines above ground is the availability of air. Air is free and it works pretty well as an insulator if you keep enough space around energized conductors. You only need actual insulators at the pole. Putting lines at tens of thousands of volts underground requires pretty expensive insulation that prevents arcs to ground or other phases and resists the effects of water, a hazard that is inevitable for every underground utility.

We often call an electrical interconnection a “grid,” but that term mostly applies to the high-voltage bulk power system covering whole states or countries. It’s not really a good description at a city scale. Most urban areas use what’s called a radial system for distributing electricity, which is more akin to branches of a tree than a mesh. For a single-family residential home, you might share a transformer with a few houses. That connects to the distribution feeder, and you can follow that line all the way back to the substation. Each feeder is essentially a one-way dead end for power flow. There may be a crossover somewhere for redundancy, but it’s not an inherent part of the radial architecture. In New York City, it’s totally different.

Throughout the five boroughs, New York City operates about 70 separate so-called “secondary networks,” each of which is served by somewhere between 8 and 28 feeder lines from an area substation. Rather than individual transformers that serve one or two buildings, there are network transformers dotted around the city, usually in concrete vaults belowground, each connected to one of the redundant feeders from the substation. Because they’re underground, these transformers have to be capable of operating while fully submerged in water.

Those transformers drop the voltage to the service-level where a grid of conductors spread out to all the buildings in the area. These are true networks, actual grids of service-level voltage with multiple redundant pathways for energy to take (not like the branches of a tree at all). And there’s another way it’s not quite like the rest of North America.

A typical service transformer in the US gives you “split phase power”. It takes one phase from the grid and provides two energized lines we call hots. Each hot leg has a voltage sine wave between neutral that is 180 degrees out of phase. So between one hot and the neutral, you get 120 volts. That’s a typical wall outlet. Larger appliances and EV chargers use both hot lines to get 240 volts. In New York City, the service networks are different. They use a three-phase system just like the rest of the grid. Each phase is offset by 120 degrees. Most individual apartments or houses get two of the three phases. So in the city, you still get 120 volts from hot to neutral, but you only get 208 volts between the hots. Most large appliances are designed to work on both 240 and 208 volts in the US because of this mixing and matching with single phase and three phase power.

Larger buildings like skyscrapers usually get their power at a higher voltage directly from the feeder and use their own transformers on maintenance floors to provide service throughout the building. So ConEdison maintains basically two power grids, each underground, one for the feeders between 13,000 and 27,000 volts and one for low voltage service. They both run through ducts that travel below streets and sidewalks and are serviced in the thousands of underground manholes and vaults throughout the city. With everything protected underground with lots of redundant paths, New Yorkers enjoy one of the most reliable electrical services in the country, but obviously, that reliability comes at a price. The “service network” architecture is one reason why New York City has some of the highest electricity prices in America. But wires aren’t the only way New Yorkers get power.

Looking back at our generic intersection, you’ll see something that is not present in most other cities, and certainly not at the scale we see here. New York City has a district heating network, offering steam as a public utility. It’s not as extensive as the power system, with pipes mainly confined to Manhattan, but it is the largest of its kind in the world by far. There are about 1,500 customers with steam service delivered through the underground lines. It’s mainly used for heating buildings and for hot water, but it can also be used for sterilization in hospitals, cleaning dishes at restaurants, for presses in dry cleaning facilities, and somewhat counterintuitively, for air conditioning through steam-driven compressors.

Of course, there are a lot of engineering challenges with running steam pipes underground. Thermal expansion is a big one. If you shut one of these lines down, it changes in temperature by roughly 300 degrees Fahrenheit or 170 degrees Celsius. You need regular expansion loops or heavy-duty slip joints that can absorb the physical movement created by those huge swings in temperature. You also have to deal with condensation. Some of that steam naturally condenses into water, and if you don’t get it out of the pipe, that liquid can act like bullets inside the high speed steam flow. I have a video on that from way back in the day if you want to learn more. Steam traps can discharge condensate while keeping the steam inside the pipe. Occasionally you get a steam leak or just a spot where groundwater is coming into contact with the hot pipes, creating a cloud of steam through a manhole to the street. ConEd puts these orange smokestacks to divert the vapor away from the public until they can fix the underlying issue. Those steam lines are insulated to keep the heat in, and usually deeper than other utilities to avoid heating up the surface or the other lines.

One of those lines you definitely don’t want to heat up is natural gas. A large percentage of buildings and households in New York City use natural gas for heating, cooking, and hot water. Some of the gas lines are low-pressure mains that connect directly to buildings through the meter. The newer lines run at higher pressures and require a building regulator. For safety reasons, these are often installed outside, so they’re easy to spot. New York has put limits on when natural gas can be included in new construction, so these lines might become a thing of the past, joining other abandoned lines underground. Because of the cost and complexity of decommissioning utilities, it’s not uncommon to simply abandon them in place. In fact, there are plenty of lines underground that aren’t doing anything at all, including a once-sophisticated pneumatic tube mail delivery system, which spanned 27 miles and delivered mail throughout the city until it was shuttered in the 1950s when the maintenance costs started outweighing the benefits.

Also like other places, New York City needs telecommunications: telephone, cable, and fiber lines. And like all their other utilities, these are running below the streets, weaving their way around everything else near the surface. But unlike a lot of the utilities, telecommunications are somewhat standardized. That’s because, in 1891, the city granted a franchise to a company called Empire City Subway. That name uses a more generic meaning of subway, an underground path, rather than the much more famous one you’re thinking of (which we’ll get to soon). Empire City Subway’s whole job was and still is to build and maintain a massive network of underground utility ducts. Anyone wanting to run telephone, cable, or fiber lines through Manhattan or The Bronx has to rent space inside those conduits rather than digging their own paths.

But even with that standardization, you can see that things are starting to get pretty cramped. In many cases, especially older parts of the city, very little of this infrastructure was planned out comprehensively. As each service was installed, it had to find space among the others. And because much of it is pretty old, most streets are either imperfectly mapped, or not mapped at all. Making repairs is its own kind of treasure hunt. Utilities workers and engineers often just call it “the spaghetti” because of how big a mess it can be. To combat their proverbial pasta problems, the City is working on a comprehensive, 3D database of underground utilities to help with construction, repairs, and emergency response, but it’s an enormous challenge to combine records that, in some cases, can be more than 100 years old.

Vacuum excavation has made a big difference in being able to dig around utilities safely. Water or compressed air breaks up the soil and the truck can suck it up. This makes it a lot quicker to expose buried utilities without damaging them, especially compared to an excavator or backhoe bucket. Often you have to support utilities from above during repairs to keep them from sagging down and flexing, which could lead to breaks and service disruptions. Older materials used for pipes and ducts like cast iron and vitrified clay are brittle and relatively weak. Taking away that continuous underlying support to repair something below requires a lot of care. This is just a ton of work compared to cities where there’s less density in the buried utilities. But that’s still not everything under the street.

Of course you have sewers that carry wastewater away. They’re usually deeper than the other utilities, and for a few reasons: Sewers rely on gravity to maintain flow, so the slope carries them further below the ground. You also generally want to have them below water lines, just to be absolutely sure that a sewage leak doesn’t find its way through the soil toward fresh water. Of course, all American cities have sewer lines, but New York has some big ones. They’re not quite like the cartoon-style tunnels with handy ledges that act like walkways for crime fighting reptiles, but some of the hand-laid brick pipes are still in use. This is a big city that produces a lot of wastewater, and when all those lines start to converge and concentrate toward the treatment plants, it takes big pipes to carry it all. They may not look quite like the cartoons, but some actually are cavernous enough to walk through standing completely upright.

These big old sewers are made even more complicated by stormwater. Much of the sewer system in New York City was built before modern environmental rules. The goal back then was to get it out of town, not to treat it, so it didn’t really matter that the stormwater was diverted into sewage pipes. It was a good thing, actually, because it diluted the sewage that was just being discharged directly into waterways. The problem is that, now, it matters a lot. On rainy days, the treatment plants don’t have the capacity to clean up both the sanitary sewage (the stuff that comes from sinks, toilets, and showers) and all the stormwater runoff from streets and buildings. So they still have to discharge untreated sewage on occasion. The city has roughly 400 outfalls where these “combined sewer overflows” occur.

Of course, dumping raw sewage is highly restricted under modern laws. You can’t pollute natural waterbodies without consequence. But, it’s a practically impossible problem to fix, at least all at once, since these systems were built this way over decades and decades. Instead, the city operates under a consent decree with the state that basically says, “We won’t fine you for the overflows, but you have to implement a plan that puts a stop to this eventually.” Lots of major US cities are in the same boat. And New York City really has been working to fix it. From curbside rain gardens to massive underground retention tanks like the one at Newton Creek, New York is slowly chipping away at the number of sewer overflows. In newer parts of the city, the sanitary and storm sewers are entirely separate. Catch basins line the streets where stormwater is diverted to underground pipes. That’s just one more separate system of underground utilities to find space below these crowded streets. Go a little deeper and there’s more.

New York City is famous for its subway, one of the largest and busiest rapid transit systems in the world. They mostly run in cut-and-cover tunnels below the streets. This is a simple idea: dig a trench down from the surface of the road, construct the floors, walls, and roof of the tunnel, then backfill and replace the road on top. It’s disruptive to the street, but much simpler and more cost-effective compared to tunneling methods that don’t disturb the surface. There are many instances of multi-level tracks where one line crosses another that required elaborate steel framing to support everything during construction. Deeper sections of the subway, like those under the East River, required alternative methods. The most recent projects have used tunnel boring machines, which are much more expensive, but increasing the depth helps avoid existing utilities and minimizes disruption at the street level. And you also have the ventilation structures and the elevators and escalators and stairs that connect between subways and the surface. The stations themselves are mostly subterranean, since that’s where the trains are.

Of course, there are tunnels for cars and trucks below the surface in New York City too that pass under the East River and Hudson River. Sometimes it makes more sense than building a bridge, and other times it’s necessary for grade separation to keep traffic moving.

Go even deeper below the street, and we’re back to water. New York has three primary tunnels that bring the fresh water into the city from upstate. Tunnel 3 is one of the largest and most expensive capital construction projects in the City’s history. Started in 1970, it’s still under construction and will be for at least another decade. It’s really deep, roughly 650 feet or 200 meters below the surface. That’s nearly half the Empire State Building! That extreme depth is to avoid the underground traffic jam of all the other utilities we’ve talked about, but also because it keeps the tunnel in hard rock that’s better able to withstand the monstrous pressure inside.

The underground of New York is almost a city within a city. We kind of get used to having all these utilities and services that it’s easy to forget the physical space they all take up and the work that goes into installing and maintaining them. Besides what they provide us, there are little signs at the surface as reminders: manhole lids, puffs of steam, metal grates where you can barely catch a glimpse of the machinery below. And then sometimes the reminders are a little more in your face, like when an intersection shuts down for a careful and intricate replacement project, weaving new pipes between those that might be a century old. These kinds of disruptions are hard to love, but they do give you a chance to appreciate everything that’s underneath.

The Daily Front Page 21 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Focus and Followthrough
article

The New AI Superpowers: Focus and Followthrough

by mooreds·▲ 195 points·61 comments·rickmanelius.com ↗
Burnout is on the rise again, with an ironic twist.

Burnout is on the rise again, with an ironic twist.

Conventional wisdom is that burnout is caused (solely) by overwork.

So logically, if AI can help us complete tasks 2-100x faster than before, we should be UNDERWORKED and experiencing ZERO burnout, right?

If only it were that simple.

ALL THE THINGS!

You may be too young to recognize this meme, but it’s the feeling most people in tech had when AI started accelerating their efficiency.

Why?

Because for years, all of us had dozens, if not hundreds, of side projects we could have started but had to shelve because there weren’t enough hours in a day.

Then AI came along and teased us with a seductive lie…

Claude: “But, maybe you can do it all now that I’m here!”

For the past decade, I’ve been a repeat startup founder + new dad + always ambitious person. I had over 100 article titles + outlines in draft. I had a someday maybe list full of 50 projects to start “when I had the time.”

I started to dip my toe in the water and YOLO some of these side projects between meetings. Queuing up Claude in 5-minute increments and then letting it rip before jumping to the next call.

The crazy thing is it started to work…

Things I wanted to try, explore, and write about were working, and this only 10X’d my ambition to start more projects! Spin more plates. ALL THE THINGS!

The Crash

You already know how this ends.

Because it always ends this way.

There’s the quote: “You can have anything in life, but not everything.”

Even when AI can 100x your productivity on a single task, we humans still have a finite number of seconds in a day.

And we’re not machines. We’re not meant to operate at 100% speed every waking hour.

What happened was I started to notice I had something like 40 “proof of concept” projects going on, and I was starting to feel that pang of burnout growing inside me. Each project was a new open loop, a new responsibility, and a new thing to tend to.

I used AI to reduce my required work, so I invented an endless amount of make-work.

Make Work: “work assigned or done chiefly to keep one busy”

Don’t get me wrong, I had fun along the way. But this is a failed strategy I had to course correct.

Less, But Better

In his book Essentialism, Greg McKeown beats the drum about doing less, but better.

I think this is critical in an age of AI.

Rather than using AI to expand the number of things you’re doing (horizontal), I have come to realize the best strategy is to go way farther on the things that matter (vertical).

In short, focus and follow through.

Personal Examples

I didn’t intend to write this article this morning, but I had to to stop myself.

I had another one titled “Sesame Street Simple” (coming soon) that I was trying to rush out the door. However, as I kept sitting with that article, I realized I wasn’t giving it my very best. Maybe it was a solid B/B-, but the topic deserves an A+ effort to have the impact I want it to make.

So I stopped.

And I decided to give it at least 2-3 more revisions with focused time and attention.

Partial vs Total Eclipse

Am I being a perfectionist?

An overdramatic artisan?

I don’t think so.

Garry Tan penned an amazing article about the difference between a partial and a total eclipse.

The main thrust? You think the difference between a 99% and a 100% eclipse is a mere 1%, a rounding error. But visually, it’s a 100x difference. A partial eclipse feels like the dimming of a cloud. A full eclipse is like an eerie nighttime befalling the world.

The experiences could not be more different in effect.

Likewise, things that we love (products, books, movies, etc.) pay the price of that last 1% to deliver that 100% experience.

Here’s the hard part. That last 1% may feel like 50-90% of the total time, which is why so many people just say “good enough” and ship.

The Better Way Forward

Now that AI makes things 2-100x more efficient, there’s no reason not to pay the price of that last 1%.

The only way to do that is to be ruthlessly focused and then follow through on the few open projects that matter.

I’m still making lots of mistakes because it’s so hard not to chase the shiny (ALL THE THINGS), but the rewards are worth it.

To that end, I hope to publish fewer articles, but go deeper on each one.

You and I both deserve that.

And no one should be experiencing burnout in the age of AI. The entire point was to offload all the hard labor so we could be save time, focus on what matters, and be more human.

The Daily Front Page 22 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — A Tiny Voice That Talks
article

Inflect-Micro-v2: complete voice in 9.36M parameters

by nateb2022·▲ 205 points·27 comments·huggingface.co ↗
Complete local text-to-waveform speech synthesis under 10M parameters.

Inflect-Micro-v2 release cover

Complete local text-to-waveform speech synthesis under 10M parameters.
Fixed-voice English TTS with deterministic seeds, long-text handling, and CPU or CUDA inference.

A note from Owen

I built and funded Inflect v2 independently. If this release finds a real audience, I would like to continue the project with a broader v3, which might include things like more langauges, voices, and stability improvements. If the model is useful to you, leaving a like on Hugging Face genuinely helps more people discover it.

Live playground GitHub Inflect Nano v2 Inflect Discord Benchmarks

9,356,513 deployable parameters · 37.53 MB FP32 · 24 kHz mono output

New: public adaptation toolkit

Prepare data, audit train/validation splits, adapt a fixed voice or language, resume training, evaluate checkpoints, and export PyTorch or ONNX packages with the Inflect adaptation toolkit. Adapted quality is experimental and depends on the dataset, frontend, and fluent-speaker evaluation.


Inflect v2 uses one public API across two sizes: Micro prioritizes quality below 10M parameters; Nano prioritizes footprint below 4M.

Explore this model card

Start here Technical detail Listen Architecture Evaluation Controls and long text Choose Micro or Nano Data and adaptation Run locally Exports and quantization Adapt a voice or language Training and export workflow Package map Evaluation and raw protocol Limitations Deployment guide

Listen

These are held-out text generations, not reconstructions of training audio. Each transcript is shown exactly as passed to the public frontend.

Test Exact transcript Generated audio
Conversational It wasn't until later that I realized what had actually happened.
Punctuation First, close the window; second, turn off the lamp; finally, lock the door.
Numbers The package weighs twelve point six kilograms and arrived on July twenty-first.
Names and places Gwendolyn photographed the eucalyptus trees outside Ljubljana.
Technical The system runs on three core components that all have to stay in sync.

Evaluation

No single metric captures TTS quality. Inflect v2 reports human preference, predicted naturalness, multi-ASR intelligibility, complete footprint, and runtime separately rather than compressing them into one unverifiable score.

Community preference ↑ UTMOS22 ↑ Two-ASR semantic WER ↓ Complete FP32 weights ↓ 4-thread CPU throughput ↑ 66.2% 4.395 3.99% 37.53 MB 6.28× real-time

The headline row always refers to Inflect-Micro-v2. Detailed competitor results and protocol boundaries are kept visible below.

Comparison set. Results include KittenTTS Nano, Piper Low, and Supertonic 3, established compact or local TTS baselines with larger deployable weight footprints than both Inflect releases. Weight sizes are compared at package level, and no single metric is treated as proof of overall superiority.

1. Human blind preference

Community blind listening

Inflect-Micro-v2 recorded a 66.2% preference rate (21 wins · 10 losses · 3 ties) in the final anonymous community study. Systems were hidden, left/right order was randomized, and ties count as half a win. This is descriptive community evidence, not formal MOS.

2. Predicted naturalness versus footprint

Predicted quality versus footprint

The UTMOS22 run used 500 identical unseen prompts per voice. KittenTTS and Piper are equal-weight two-voice means; their observed voice ranges appear as whiskers. Supertonic 3-step is reported below the plotted range rather than flattening every other system.

Inflect-Micro-v2: 4.395 UTMOS22, 95% bootstrap CI 4.381–4.408. UTMOS22 is a learned predictor, not human MOS.

3. Intelligibility on unseen text

Two-ASR semantic WER consensus

The headline score is the equal-weight mean of Qwen3-ASR and Nemotron 3.5 corpus WER for every system. Whisper is excluded consistently from the headline because it produced insertion-heavy hallucinations on a subset of otherwise intelligible Supertonic 8-step clips. It is not deleted: the complete three-ASR evidence remains below.

Open the complete three-ASR audit

Semantic WER across Qwen3-ASR, Nemotron 3.5, and Whisper large-v3

System / voice Qwen3-ASR ↓ Nemotron 3.5 ↓ Whisper large-v3 ↓
Inflect-Micro-v2 2.52% 5.45% 2.73%
Inflect-Nano-v2 2.79% 5.63% 2.65%
KittenTTS Nano · Bruno 2.15% 3.96% 2.17%
KittenTTS Nano · Hugo 2.39% 3.80% 2.11%
Piper Low · Danny 2.62% 5.60% 2.55%
Piper Low · Ryan 2.81% 5.51% 2.87%
Supertonic 3 · M2 · 3-step 3.03% 6.04% 3.22%
Supertonic 3 · M2 · 8-step 2.05% 3.56% 8.08%

For Inflect-Micro-v2, the individual results are 2.52% Qwen3-ASR, 5.45% Nemotron 3.5, and 2.73% Whisper large-v3. The former three-model mean, 3.57%, is retained only as a descriptive audit value and is not used as the headline score.

Open evaluator robustness and error-category diagnostics

ASR evaluator robustness

Semantic WER by prompt category

These views are diagnostics, not additional leaderboards. They show where the recognizers disagree and which prompt categories still produce recoverable transcription errors.

4. CPU runtime

Both Inflect releases synthesize comfortably faster than real time on CPU. The managed reference run used a Hugging Face CPU Upgrade instance (8 vCPU, 32 GB RAM) with four framework threads, end-to-end text-to-waveform timing, and 100 fixed Modern400 prompts. Three complete passes were recorded; the first cache-building pass was excluded and the table pools passes two and three.

Release Steady-state RTF ↓ Audio / wall time ↑
Inflect-Micro-v2 0.1593 6.28×
Inflect-Nano-v2 0.0933 10.72×

These are package-level results from the public PyTorch runtime, not a claim that Inflect is the fastest compact TTS system. Hardware, frontend behavior, framework, compilation, and thread policy all affect small-model measurements.

Open directional compact-system speed context

The same managed CPU and four-thread policy were used for a shorter comparator pass: the identical 50-prompt prefix, repeated twice. KittenTTS and Piper are equal-work pooled across their two tested voices.

System Audio / wall time ↑
Piper Low 31.37×
KittenTTS Nano 13.33×
Inflect-Nano-v2 10.72×
Supertonic 3 · 3-step 10.15×
Inflect-Micro-v2 6.28×
Supertonic 3 · 8-step 4.37×

Because Inflect uses the larger 100-prompt steady-state run while comparator rows use the shorter 50-prompt confirmation pass, this table is deployment context rather than a perfectly matched speed leaderboard. Several comparators also use optimized ONNX runtimes, while the published Inflect benchmark above uses the canonical PyTorch runtime. The separately released Inflect ONNX path has not been substituted into those benchmark numbers.

5. Complete weight footprint

Complete deployable model footprint

Voice variants sharing the same weights are merged. Inflect totals include the integrated waveform decoder.

Open the frozen evaluation protocol

  • Modern400 uses 400 identical unseen English prompts per system: 200 fixed modern/stress prompts plus 200 deterministic FLEURS en_us test prompts.
  • Exact-text exclusion was checked against 87,362 training transcripts.
  • All ASR inputs are resampled to 16 kHz and scored with the same disclosed English normalizer.
  • UTMOS22 uses tarepan/SpeechMOS v1.2.0 on a separate 500-prompt generation set.
  • Headline intervals use 10,000 bootstrap samples.
  • The Modern400 corpus SHA-256 is b7504ce2dce44a2da82770a6a5dfd2a034fe17e2113980f8a69663ade417a34c.
  • Prompts, hypotheses, compressed row-level reports, and summaries ship under evaluation/final/.
  • Runtime is evaluated separately because framework, thread policy, compilation, and host load can dominate small-model comparisons.

Choose the right Inflect

Inflect-Nano-v2 | Inflect-Micro-v2
Complete parameters 3,966,721 | 9,356,513
FP32 weights 15.97 MB | 37.53 MB
Positioning Smallest practical footprint | Strongest Inflect v2 quality
24 kHz waveform decoder Included | Included
Python API and frontend Same | Same

Inflect-Micro-v2 is the quality-focused member of the family. Both models use the same public API and complete text-to-waveform packaging.

Run locally

Install

python -m pip install --upgrade huggingface_hub
hf download owensong/Inflect-Micro-v2 --local-dir Inflect-Micro-v2
cd Inflect-Micro-v2
python -m pip install -r requirements.txt

This uses the Hub's version-aware downloader and retrieves the complete repository. A Git clone also works, but hf download is the recommended path for ordinary model installation.

Python

from inference import InflectTTS

tts = InflectTTS(".", device="cpu")
tts.save(
    "A small voice can still have something meaningful to say.",
    "sample.wav",
    speed=1.0,
    variation=0.667,
    seed=7,
)

Download through the Hub

import sys
from huggingface_hub import snapshot_download

model_dir = snapshot_download("owensong/Inflect-Micro-v2")
sys.path.insert(0, model_dir)

from inference import InflectTTS

tts = InflectTTS(model_dir, device="cpu")
sample_rate, waveform = tts.synthesize("The complete model runs locally.")

The result is a 24 kHz mono float32 waveform. Long input is split at punctuation-aware boundaries, synthesized chunk by chunk, and joined with controlled pauses.

ONNX Runtime

The official verified FP32 export is published separately as Inflect-Micro-v2-ONNX. It supports dynamic lengths, CPU/CUDA/DirectML provider selection, deterministic seeds, and the same long-text wrapper without importing PyTorch:

git clone https://huggingface.co/owensong/Inflect-Micro-v2-ONNX
cd Inflect-Micro-v2-ONNX
python -m pip install -r onnx/requirements.txt
python onnx/inference_onnx.py \
  --text "The complete model now runs through ONNX Runtime." \
  --output sample-onnx.wav \
  --provider cpu \
  --seed 7

The neural model is split into duration.onnx and decode.onnx; together they contain the complete learned text-to-waveform path. The English eSpeak-ng frontend remains CPU-side code. See the ONNX repository for graph contracts, provenance, parity measurements, browser deployment, and re-export instructions.

Release profile

Local runtime Long-text handling CPU and CUDA inference through the same Python API and CLI. Punctuation-aware segmentation with controlled pauses and edge fades. Repeatable output Auditable evaluation Fixed seeds reproduce the same latent sample on the same runtime stack. Frozen prompts, raw ASR hypotheses, intervals, hashes, and per-system reports are included.

Architecture and parameter budget

Inflect v2 is a parameter-efficient VITS-family end-to-end text-to-waveform generator with an English phoneme frontend, monotonic alignment, stochastic latent synthesis, residual coupling flow, and an integrated alias-reduced neural waveform decoder.

Component | Inflect-Micro-v2
Latent channels | 192
Text hidden channels | 96
Encoder layers / heads | 3 / 2
Feed-forward channels | 768
Flow coupling blocks | 4
Initial decoder channels | 320
Upsample rates | 8, 8, 2, 2
Training segment | 16,384 samples
Output | 24 kHz mono waveform

The release describes the deployable architecture. Private corpus-construction and optimization details are not part of this open-weight package.

Controls, determinism, and long text

Control | Default | Public range | Meaning
speed | 1.0 | 0.5–2.0 | Lower is slower; higher is faster.
variation | 0.667 | 0.0–1.0 | Lower is steadier; higher samples more latent variation.
seed | 0 | integer | Repeats the same stochastic sample on the same runtime stack.

Long passages are punctuation-aware chunks, not one unlimited autoregressive pass. Chunk boundaries receive short pauses and edge fades. See docs/API.md for waveform contracts and concurrency notes.

Data, voice, and adaptation status

The release contains one fixed synthetic English voice. The package does not redistribute a real-speaker recording corpus, does not claim the voice as the identity of a real person, and requires no reference audio or external model at inference.

The base release remains inference-first, but a public experimental fixed-voice and language adaptation workflow is now available. A new voice replaces the built-in speaker rather than adding runtime voice cloning. A new language requires owned or licensed speech data, a compatible phoneme frontend, symbol migration, retraining, and fluent-speaker evaluation. Start with the Inflect adaptation toolkit, then review docs/DATA_AND_VOICE.md.

Package map

Path | Purpose
model.pth | Inference-only generator checkpoint
config.json | Architecture and audio configuration; also the Hub download-count query file
inference.py | Public Python API and CLI
inflect_vits_frontend.py | English normalization, phonemization, and punctuation frontend
runtime/ | Self-contained model implementation
Inflect-Micro-v2-ONNX | Separate official FP32 ONNX graphs, torch-free runner, parity report, checksums, and exporter
samples/ | Held-out example generations
evaluation/final/ | Frozen benchmark prompts, reports, and protocol artifacts
docs/ | API, deployment, evaluation, adaptation, and export documentation
release_manifest.json | File sizes and SHA-256 hashes

Limitations

  • English only, with one fixed male voice. This is not zero-shot voice cloning.
  • Unfamiliar phrasing can become flatter, less expressive, or less stable.
  • Numbers, abbreviations, homographs, and uncommon names remain frontend- and context-sensitive.
  • Long passages use punctuation-aware chunking; transitions can differ from a native long-form model pass.
  • Stochastic variation can alter timing and pronunciation. Fix the seed for comparisons.
  • UTMOS22 and ASR scores do not replace controlled human MOS or MUSHRA-style evaluation.
  • Not validated for medical, legal, emergency, or accessibility-critical communication.

Responsible use

Do not use the included voice to impersonate a real person, deceive listeners, or create fraudulent content. Disclose synthetic speech where the context could otherwise mislead. Users are responsible for applicable laws and the Apache-2.0 license.

License, integrity, and attribution

Original Inflect code and weights are released under Apache-2.0. Bundled third-party components retain their own notices in THIRD_PARTY_NOTICES.md. release_manifest.json records packaged file sizes and SHA-256 hashes.

Private training scope and contact

Inflect v2 is an open-weight release. Deployable weights, inference code, frontend code, evaluation prompts, and release reports are public. The training corpus-generation pipeline, private filtering infrastructure, and full optimization recipe are not part of the public package.

Owen Song may share additional technical context privately for credible research, collaboration, reproducibility, or deployment inquiries when the request has a clear purpose and does not conflict with licensing or data-provenance constraints.

Citation

@software{song2026inflectmicrov2,
  author = {Owen Song},
  title = {Inflect-Micro-v2: Complete Local Text-to-Waveform TTS Under 10M Parameters},
  year = {2026},
  url = {https://huggingface.co/owensong/Inflect-Micro-v2}
}

Designed and developed independently by Owen Song · open weights · Apache-2.0 · complete local text-to-waveform inference

The Daily Front Page 23 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — AI, Platforms & Power — Briefs
The Daily Front Page 24 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Playful Tech & Maps — Briefs
article

Htmx 4.0, the first JavaScript library to release exclusively on the Game Boy

by rcy·▲ 423 points·140 comments·swag.htmx.org ↗

$35.95 CAD

HTMX 4.0, the first JavaScript library to release exclusively on the Game Boy® platform!

Experience the power of HTMX, now on your favorite portable handheld.

Four levels of pickle-collecting excitement.  Squish client-side JS down to size, avoid slop and, if you have the skills, defeat Warren to unlock the htmx 4.0 source code!

Be the hero that the web needs!

htmx 4: the game product image (1)

htmx 4: the game product image (2)

htmx 4: the game product image (3)

htmx 4: the game product image (4)

htmx 4: the game product image (5)

htmx 4: the game product image (6)

htmx 4: the game product image (7)

htmx 4: the game product image (8)

Quality Guarantee & Returns

  • Quality is guaranteed. If there is a print error or visible quality issue, we'll replace or refund it.
  • Because the products are made to order, we do not accept general returns or sizing-related returns.
article

An ESP32 based plane radar for my desk

by alexktz·▲ 261 points·54 comments·blog.ktz.me ↗

ESP32 plane radar showing nearby aircraft on its round display

This project made for a perfect lazy Saturday unwind, after a busy week giving a talk at Devrelcon in NYC. Makerworld had this thing as one of their featured models about a week or two ago, and the parts came in while I was away.

GitHub preview for ironicbadger/ESP32-Plane-Radar GitHub repository ESP32-Plane-Radar Open-source ESP32 firmware for a 1.28″ round display that shows live ADS-B aircraft around your location as a sonar-style plane radar. by ironicbadger · C++ · MIT View repository on GitHub ★ 33 ⑂ 2 ↗

Plane radar is a neat little project that turns an ESP32-C3 and a 1.28-inch round display into a live aircraft radar. It pulls nearby ADS-B traffic, plots each aircraft by distance and bearing, and shows the details directly on the screen.

It was an easy build, although it did require a little soldering. I always enjoy doing that as it reminds me of my days building racing drones. Thin, silicone based wires made quick work of the cabling. And after about 15 minutes, we were ready to go.

An ESP32-C3 wired to a round display during assembly

It is insanely easy to flash firmwares to a fresh esp32 these days using the browser-based ESPHome web flashing tool. Under 30s and you’re done.

A quick note about the 3d model

The original model was featured on Makerworld, because it looks great. Reality though was, in practice, it’s not actually that great.

ESP32 Plane Radar — Live ADS-B on a Round Display by matixovi on MakerWorld MW MakerWorld 5.4Klikes 3.3Kdownloads 15.1Ksaves 2.4Kmakes Original 3D model ESP32 Plane Radar Live ADS-B on a Round Display by matixovi · Published May 31, 2026 View model on MakerWorld ↗

Unfortunately the tolerances are just too tight to be usable with the batch of boards I got. So I’m likely going to model my own replacement at some point, but for now I ended up printing this model instead.

ESP32-S3 1.28" Waveshare Plane Radar by ThePrintableWatch on MakerWorld MW MakerWorld 154likes 195downloads 471saves 103makes Original 3D model ESP32-S3 1.28" Waveshare Plane Radar by ThePrintableWatch · Published Jun 10, 2026 View model on MakerWorld ↗

Customising the firmware

I’ve spent today improving my fork of ESP32 Plane Radar project, with the help of pure vibes.

The biggest improvement is proper flight context. Where the data is available, aircraft now show their origin and destination instead of just their tail number, with the callsign used as a fallback. Aircraft types are also more descriptive, something like B737-800 rather than simply B737. I added local weather, temperature, humidity, time and date as well.

The web interface can now modify coordinates after initial setup. Display options can now be changed without resetting the Wi-Fi configuration, and there are controls for units, runways, weather, temperature format and 12/24-hour time. Text is 10% larger by default, with a persistent 80–130% slider for adjusting it.

ESP32 plane radar showing live aircraft, weather and flight context

Finally, the firmware now supports authenticated OTA updates, so future builds can be installed through the browser instead of connecting the board over USB.

This is a bit like how ESPHome applies firmware updates. Compile the binary on your local laptop and then upload it to the ESP32 wirelessly.

Next up might be porting it to a larger display, and designing a more forgiving 3D printed enclosure for it.

The Daily Front Page 25 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Research & Design — Briefs
article

Design is compromise

by ankitg12·▲ 237 points·80 comments·stephango.com ↗

When did the word “compromise” become vilified?

Compromise is neither good nor bad, it’s something we do every day. It’s decision making. Prioritizing. Deciding that one thing is more important than another. It’s finding the right balance between two competing desires.

Which compromises you make   — that’s what matters. Choosing the right compromises is what defines good design.

Companies like to tout products as “uncompromising” or having “no compromises”. That’s impossible. Once you decide on an approach, you inherently decide against other options.

Another word for compromise is “tradeoff”. The word “tradeoff” conveys the relationship between strengths and weaknesses. You are trading a weakness for a strength.

Having an opinionated set of tradeoffs exposes your approach to a set of weaknesses. The more you tip the scale on one side, the weaker something else will be. That’s okay! Making those difficult choices is what people pay you for. You should be proud of your compromises.

My favorite products are opinionated. They make a clear statement about what they are not good at, in favor of being much better at something else.

Appealing to everyone is impossible. If you make something that aims to be good across a broad range of capabilities, you are choosing not to be exceptional at anything in particular. That might be the right compromise for your audience, but it’s definitely a compromise.

Good design is opinionated. Good design is choosing the right compromise for your audience.

The Daily Front Page 26 of 27
Sunday, July 26, 2026 The Daily Front No. #260726 — Colophon

That's the Front for Today

Issue No. #260726 — Sunday, July 26, 2026 — went to press 2026-07-27 at 07:46 UTC.

About This Magazine

The Daily Front is a daily digital magazine assembled from the stories that reached the front page of Hacker News on Sunday, July 26, 2026. Headlines, points, and comment counts are recorded as they stood at press time. All articles remain the property of their original authors — every piece links back to its source and its discussion thread.

How It Was Made

Fetched, cleaned, and typeset by an automated pipeline. An editor model laid out the pages, chose the highlights, and briefed the cover illustrator — 31 model calls and 307k tokens in total. Set in Jacquard 12, Playfair Display, Source Serif 4, and IBM Plex Mono, all served via Google Fonts under the SIL Open Font License.

The Cover

The cover illustration was commissioned with this prompt:

A refined European city square rendered with elegant editorial surrealism. At its center, a towering translucent browser-shaped shield of glass rises like a civic monument, radiating one calm, luminous signal in concentric, subtle waves. Paper-thin cookie banners stamped only with simple cookie icons peel from ornate building facades and spiral through the air like autumn leaves. Across wet cobblestones, a procession of tiny mechanical spider-robots carrying sacks of glowing data pauses precisely at a bright, invisible boundary; their delicate reflections shimmer in rain puddles. Pale satellite trails curve through an airy washed sky, while fine fiber-optic strands quietly trace the plaza’s edges, suggesting an immense hidden network. Sophisticated flattened perspective, poetic visual metaphor, muted ink-and-gouache palette with restrained electric cyan and warm amber highlights, clean composition, generous negative space, intricate urban detail, witty and contemplative magazine-cover aesthetic.

Production Ledger

StageModelCallsTokens InTokens Out
extractgpt-5-mini 30 173,432 103,063
layoutgpt-5 1 18,985 11,551

The Publisher

Published by Johnny.

Support the Press

If The Daily Front brightens your morning, consider supporting its publisher.

Credits & Contact

All content — articles, posts, comments, and the images within them — belongs to its original authors and is reproduced here to point readers back to the source. Full credit goes to those creators; every item links to its original and its Hacker News discussion.

If you are an author and would like your content removed from an issue, write to hi@johnnys.page and it will be taken down.

Feedback is always welcome at the same address: hi@johnnys.page.

Credit where credit is due.

Every page of this issue began as someone else's work — these are the original sources, linked in full.

  1. Kill The Cookie Banner by rapnie — killthecookiebanner.eu·HN discussion ↗
  2. Cloudflare's new AI traffic options for customers by alphabetatango — blog.cloudflare.com·HN discussion ↗
  3. LLM Usage in Debian: Three Proposals by zdw — debian.org·HN discussion ↗
  4. Rethinking legal education in the AI era by jjwiseman — law.uchicago.edu·HN discussion ↗
  5. What is happening to jobs? Separating AI hype from reality by pod_krad — siepr.stanford.edu·HN discussion ↗
  6. The relay market powering token resellers and fraud by mlenhard — vectoral.com·HN discussion ↗
  7. Turn And Face The Strange by subarctic — fly.io·HN discussion ↗
  8. A shell colon does nothing. Use it anyway by olexsmir — refp.se·HN discussion ↗
  9. Go Analysis Framework: modular static analysis by go team by AbuAssar — pkg.go.dev·HN discussion ↗
  10. Ruff v0.16.0 – Significant new updates – 413 default rules up from 59 by vismit2000 — astral.sh·HN discussion ↗
  11. Some more things about Django I've been enjoying by surprisetalk — jvns.ca·HN discussion ↗
  12. How to write English prose (2023) by geneticdrifts — thelampmagazine.com·HN discussion ↗
  13. Running a 28.9M parameter LLM on an $8 microcontroller by boveyking — github.com·HN discussion ↗
  14. I learned PCB design, 3D printing and C just to listen to music by interfeco — pentaton.app·HN discussion ↗
  15. Show HN: CheapSecurity – Lightweight, Self-Hosted CCTV for Linux SBCs by zeldone — github.com·HN discussion ↗
  16. Show HN: Reverse Minesweeper by pompomsheep — sunflowersgame.com·HN discussion ↗
  17. Decker, a platform that builds on the legacy of Hypercard and classic macOS by tosh — beyondloom.com·HN discussion ↗
  18. Alien World Chemistry Found Inside Meteorite That Struck New Jersey Home by spzx — seti.org·HN discussion ↗
  19. What's Under Your Feet in New York City? by sohkamyung — practical.engineering·HN discussion ↗
  20. The New AI Superpowers: Focus and Followthrough by mooreds — rickmanelius.com·HN discussion ↗
  21. Inflect-Micro-v2: complete voice in 9.36M parameters by nateb2022 — huggingface.co·HN discussion ↗
  22. GrapheneOS protections against data extraction from locked devices by Cider9986 — discuss.grapheneos.org·HN discussion ↗
  23. DeepSeek pause fundraise after comments on compute gap to US leaked (transcript) [pdf] by oliculipolicula — github.com·HN discussion ↗
  24. Google Discloses $94.1B in SpaceX Stock, Marking 6% Stake by 1vuio0pswjnm7 — wsj.com·HN discussion ↗
  25. Htmx 4.0, the first JavaScript library to release exclusively on the Game Boy by rcy — swag.htmx.org·HN discussion ↗
  26. Show HN: I mapped every US golf course by rickmf — golfcoursebrowser.com·HN discussion ↗
  27. An ESP32 based plane radar for my desk by alexktz — blog.ktz.me·HN discussion ↗
  28. Clinical failure rates over the decades: yikes by EA-3167 — science.org·HN discussion ↗
  29. Design is compromise by ankitg12 — stephango.com·HN discussion ↗
  30. Introduction to Data-Oriented Design [pdf] by tosh — gamedevs.org·HN discussion ↗

Browse all issues in the archive →