{"authors":[{"avatar":"https://ankush.one/pfp.png","name":"Ankush Singh","url":"https://ankush.one/"}],"description":"Blog posts, projects and timeline entries from Ankush Singh","feed_url":"https://ankush.one/feed.json","home_page_url":"https://ankush.one/","icon":"https://ankush.one/pfp.png","items":[{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://gist.github.com/ankushKun/cd27fb07b0f06be99d4181b40309103d\"\u003eoof on GitHub Gist\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eShort answer:\u003c/strong\u003e Claude Code keeps your conversations on disk per folder, not per account. So you can swap the login, run \u003ccode\u003eclaude --continue\u003c/code\u003e in the same folder, and keep going with the same conversation on a different account. \u003ccode\u003eoof\u003c/code\u003e does that in one command. It saves the current login from the macOS Keychain, swaps in the account you asked for, updates \u003ccode\u003e~/.claude.json\u003c/code\u003e and resumes. The whole script is \u003ca href=\"https://gist.github.com/ankushKun/cd27fb07b0f06be99d4181b40309103d\" data-external\u003eon GitHub Gist\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eI use Claude Code for \u003ca href=\"https://construct.computer\" data-external\u003eConstruct\u003c/a\u003e, my startup, and for side projects like this site, and I have more than one Claude account. When I\u0026rsquo;m deep in a session and need a different one, the built-in way is \u003ccode\u003e/login\u003c/code\u003e: a browser tab opens, I check which account the browser is signed in to, approve, come back to the terminal and hope I picked the right one. It works, but it gets old fast, and it always seems to happen right when I\u0026rsquo;m in the middle of something.\u003c/p\u003e\n\u003cp\u003eWhat I wanted was one command that says \u0026ldquo;use the other account\u0026rdquo; and puts me back in the exact conversation I was having. No browser, no copying tokens around, and no fresh chat where I have to explain the whole task again.\u003c/p\u003e\n\u003cp\u003eThat command is \u003ccode\u003eoof\u003c/code\u003e, named after the noise I make when Claude Code tells me I\u0026rsquo;ve hit a limit halfway through a task. It\u0026rsquo;s a single zsh script that I wrote in one sitting. This post covers how it works, how it compares with the official way of running several accounts, and the small rabbit holes I fell into along the way.\u003c/p\u003e\n\u003ch2 id=\"where-does-claude-code-store-your-login\"\u003eWhere does Claude Code store your login?\u003ca class=\"heading-anchor\" href=\"#where-does-claude-code-store-your-login\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe key fact is that \u003cstrong\u003eyour conversations don\u0026rsquo;t belong to your account\u003c/strong\u003e. Claude Code keeps session history on disk under \u003ccode\u003e~/.claude/projects/\u003c/code\u003e, grouped by the folder you ran it in. The account only decides who pays for the next message. If you swap the login and run \u003ccode\u003eclaude -c\u003c/code\u003e in the same folder, you land back in your most recent conversation, just on a different account.\u003c/p\u003e\n\u003cp\u003eThat leaves the question of where the login itself lives. On macOS it\u0026rsquo;s split across two places:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  macOS Keychain                         ~/.claude.json\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e ┌──────────────────────────────┐      ┌──────────────────────────────┐\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e │ \u0026#34;Claude Code-credentials\u0026#34;    │      │ \u0026#34;oauthAccount\u0026#34;: {            │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e │   OAuth access + refresh     │      │   \u0026#34;emailAddress\u0026#34;: ...,       │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e │   tokens, plan, rate tier    │      │   \u0026#34;organizationUuid\u0026#34;: ...    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e │   (the secret)               │      │ }  (who /status says you are)│\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e └──────────────────────────────┘      └──────────────────────────────┘\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe Keychain item holds the actual OAuth tokens. The \u003ccode\u003eoauthAccount\u003c/code\u003e block in \u003ccode\u003e~/.claude.json\u003c/code\u003e is just the label, the email and organization that \u003ccode\u003e/status\u003c/code\u003e shows. To switch accounts you have to change both, and to switch back later you need a copy of each account\u0026rsquo;s tokens stored somewhere safe. On Linux and Windows the tokens live in \u003ccode\u003e~/.claude/.credentials.json\u003c/code\u003e instead, so the same idea becomes a file copy.\u003c/p\u003e\n\u003ch2 id=\"why-not-login-or-claude_config_dir\"\u003eWhy not /login or CLAUDE_CONFIG_DIR?\u003ca class=\"heading-anchor\" href=\"#why-not-login-or-claude_config_dir\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThere are already two ways to use multiple Claude Code accounts, and it\u0026rsquo;s worth being clear about why I didn\u0026rsquo;t stop at either.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e/login\u003c/code\u003e works inside a running session and keeps the conversation, but it sends you through the browser every single time.\u003c/p\u003e\n\u003cp\u003eThe official answer for multiple accounts is \u003ccode\u003eCLAUDE_CONFIG_DIR\u003c/code\u003e. When someone asked for named account profiles in \u003ca href=\"https://github.com/anthropics/claude-code/issues/27359\" data-external\u003eanthropics/claude-code#27359\u003c/a\u003e, a maintainer closed it by pointing out that a separate config directory per account already gives you this, with an alias for each:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-zsh\" data-lang=\"zsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ealias\u003c/span\u003e claude-work\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;CLAUDE_CONFIG_DIR=~/.claude-work claude\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ealias\u003c/span\u003e claude-personal\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;CLAUDE_CONFIG_DIR=~/.claude-personal claude\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEach directory has its own login, settings and \u003ccode\u003eprojects/\u003c/code\u003e folder, which is exactly the problem for me. My conversation lives in one directory, so when I move to the other account it isn\u0026rsquo;t there to continue. You can symlink the history folders together, but then you\u0026rsquo;re maintaining a setup that fights the tool\u0026rsquo;s own design.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003ccode\u003e/login\u003c/code\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003ccode\u003eCLAUDE_CONFIG_DIR\u003c/code\u003e per account\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003ccode\u003eoof\u003c/code\u003e\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eBrowser sign-in on every switch\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo, once per directory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo, once per account\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eContinue the same conversation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo, each directory has its own history\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes, through \u003ccode\u003eclaude -c\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eSettings, MCP servers and memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eShared\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSeparate per account\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eShared\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTwo accounts running at once\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo, one login is active\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eSetup\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAn alias per account\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOne script and two hooks\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eIf you want strict separation, for example two clients whose work should never mix, or you need two accounts live at the same time, use \u003ccode\u003eCLAUDE_CONFIG_DIR\u003c/code\u003e. If you want one setup and the freedom to carry the same piece of work across accounts, that\u0026rsquo;s what \u003ccode\u003eoof\u003c/code\u003e is for.\u003c/p\u003e\n\u003ch2 id=\"version-one-two-shell-functions\"\u003eVersion one: two shell functions\u003ca class=\"heading-anchor\" href=\"#version-one-two-shell-functions\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe first idea was two functions in \u003ccode\u003e.zshrc\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-zsh\" data-lang=\"zsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecc-save\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# cc-save work\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  security add-generic-password -U -a \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$USER\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e -s \u003cspan class=\"s2\"\u003e\u0026#34;Claude Code-credentials-\u003c/span\u003e\u003cspan class=\"nv\"\u003e$1\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    -w \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003esecurity find-generic-password -s \u003cspan class=\"s1\"\u003e\u0026#39;Claude Code-credentials\u0026#39;\u003c/span\u003e -w\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecc-use\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e    \u003cspan class=\"c1\"\u003e# cc-use personal\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  security add-generic-password -U -a \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$USER\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e -s \u003cspan class=\"s2\"\u003e\u0026#34;Claude Code-credentials\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    -w \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003esecurity find-generic-password -s \u003cspan class=\"s2\"\u003e\u0026#34;Claude Code-credentials-\u003c/span\u003e\u003cspan class=\"nv\"\u003e$1\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e -w\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCopy the live token into a named Keychain entry, and copy a named one back. That\u0026rsquo;s the whole trick, but it has two problems.\u003c/p\u003e\n\u003cp\u003eThe first is that OAuth tokens refresh. Claude Code quietly swaps in a fresh access token and writes it back to the Keychain, and the refresh token can rotate along with it, so a copy you saved yesterday can be dead today. The saved copy has to be kept fresh, which means saving before every switch and ideally all the time.\u003c/p\u003e\n\u003cp\u003eThe second is that I was only swapping the secret. \u003ccode\u003e~/.claude.json\u003c/code\u003e still said I was the old account, so \u003ccode\u003e/status\u003c/code\u003e showed the wrong email until I ran \u003ccode\u003e/login\u003c/code\u003e anyway, which defeated the whole point.\u003c/p\u003e\n\u003ch2 id=\"how-oof-switches-accounts\"\u003eHow oof switches accounts\u003ca class=\"heading-anchor\" href=\"#how-oof-switches-accounts\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eoof\u003c/code\u003e fixes both problems and adds the comfort features I wanted once the basics worked. Each saved account gets:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ea Keychain entry named \u003ccode\u003eoof:\u0026lt;email\u0026gt;#\u0026lt;org\u0026gt;\u003c/code\u003e that holds its tokens. The org part matters because one email can belong to several organizations, a personal plan and a Team seat for example, and those are different logins.\u003c/li\u003e\n\u003cli\u003ea JSON file in \u003ccode\u003e~/.claude/accounts/\u003c/code\u003e with its \u003ccode\u003eoauthAccount\u003c/code\u003e block. No secrets go on disk, only the label.\u003c/li\u003e\n\u003cli\u003ean optional metadata file with a short name like \u003ccode\u003ework\u003c/code\u003e and the plan, so I can type \u003ccode\u003eoof work\u003c/code\u003e instead of an email.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe switch itself is small. It reads the target\u0026rsquo;s tokens with \u003ccode\u003estored_secret\u003c/code\u003e, which looks in the account\u0026rsquo;s own Keychain entry, writes them into the slot Claude Code reads from, and then patches \u003ccode\u003eoauthAccount\u003c/code\u003e in \u003ccode\u003e~/.claude.json\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-zsh\" data-lang=\"zsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eswitch_to\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003elocal\u003c/span\u003e \u003cspan class=\"nv\"\u003ekey\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nv\"\u003e$1\u003c/span\u003e secret tmp\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nv\"\u003esecret\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003estored_secret \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e)\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e||\u003c/span\u003e die \u003cspan class=\"s2\"\u003e\u0026#34;no saved credentials for \u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003ename_of \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e. Run oof login to sign in again.\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  security add-generic-password -U -a \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$USER\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e -s \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$SLOT\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e -w \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$secret\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e||\u003c/span\u003e die \u003cspan class=\"s2\"\u003e\u0026#34;couldn\u0026#39;t write to the Keychain\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nv\"\u003etmp\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003emktemp \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$CONFIG\u003c/span\u003e\u003cspan class=\"s2\"\u003e.oof.XXXXXX\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  jq --slurpfile acct \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$STORE\u003c/span\u003e\u003cspan class=\"s2\"\u003e/\u003c/span\u003e\u003cspan class=\"nv\"\u003e$key\u003c/span\u003e\u003cspan class=\"s2\"\u003e.json\u0026#34;\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;.oauthAccount = $acct[0]\u0026#39;\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$CONFIG\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u0026gt; \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tmp\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e rm -f \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tmp\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e die \u003cspan class=\"s2\"\u003e\u0026#34;couldn\u0026#39;t update \u003c/span\u003e\u003cspan class=\"nv\"\u003e$CONFIG\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  chmod \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003estat -f %Lp \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$CONFIG\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tmp\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  mv \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$tmp\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$CONFIG\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e~/.claude.json\u003c/code\u003e holds a lot more than the login, including per-project settings and MCP servers, so I never edit it in place. \u003ccode\u003ejq\u003c/code\u003e writes a temp file next to it, the temp file gets the original\u0026rsquo;s permissions, and \u003ccode\u003emv\u003c/code\u003e swaps it in with a single rename. If anything fails halfway, the real file is untouched.\u003c/p\u003e\n\u003cp\u003eBefore any of that runs, \u003ccode\u003eoof\u003c/code\u003e saves the current login. That\u0026rsquo;s the fix for stale tokens: every switch starts by copying the live, freshly refreshed tokens into the current account\u0026rsquo;s entry, so the account you\u0026rsquo;re leaving is always stored in its newest state. Then it \u003ccode\u003eexec\u003c/code\u003es \u003ccode\u003eclaude -c\u003c/code\u003e and the conversation picks up where it left off.\u003c/p\u003e\n\u003cp\u003ePicking the target is meant to need as little typing as possible:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eoof\u003c/code\u003e on its own rotates to the next saved account.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eoof work\u003c/code\u003e first looks for an exact short name or email, and if nothing matches it tries a substring, so \u003ccode\u003eoof pers\u003c/code\u003e is enough. If a substring matches two accounts it refuses and names both, because guessing wrong here means quietly working as the wrong person.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eoof pick\u003c/code\u003e opens a menu.\u003c/li\u003e\n\u003cli\u003eAnything after \u003ccode\u003e--\u003c/code\u003e goes to \u003ccode\u003eclaude\u003c/code\u003e in place of \u003ccode\u003e-c\u003c/code\u003e, so \u003ccode\u003eoof pick -- -r\u003c/code\u003e lets me choose an account and then choose a conversation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"saving-logins-automatically-with-claude-code-hooks\"\u003eSaving logins automatically with Claude Code hooks\u003ca class=\"heading-anchor\" href=\"#saving-logins-automatically-with-claude-code-hooks\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eSaving before every switch covers switches made with \u003ccode\u003eoof\u003c/code\u003e. It doesn\u0026rsquo;t cover running \u003ccode\u003e/login\u003c/code\u003e inside Claude Code by hand, which is how a new account usually gets added. For that I leaned on Claude Code\u0026rsquo;s hooks. Two hooks in \u003ccode\u003e~/.claude/settings.json\u003c/code\u003e run \u003ccode\u003eoof save -q\u003c/code\u003e in the background when a session starts and after every reply:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;hooks\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;SessionStart\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[{\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;hooks\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[{\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;command\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;command\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\\\u0026#34;$HOME/.local/bin/oof\\\u0026#34; save -q || true\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;timeout\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;async\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}]\u003c/span\u003e \u003cspan class=\"p\"\u003e}],\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;Stop\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e         \u003cspan class=\"p\"\u003e[{\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;hooks\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[{\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;command\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;command\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\\\u0026#34;$HOME/.local/bin/oof\\\u0026#34; save -q || true\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;timeout\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026#34;async\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}]\u003c/span\u003e \u003cspan class=\"p\"\u003e}]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNow any login gets picked up after the next message, and the tokens on file are never more than one reply old. Since that save runs constantly, it compares the live secret with the stored one first and only writes to the Keychain when they differ, otherwise it would rewrite a Keychain item every few seconds for nothing. The \u003ccode\u003e|| true\u003c/code\u003e and \u003ccode\u003easync\u003c/code\u003e make sure a hook failure never gets in the way of a reply.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eoof login\u003c/code\u003e does the same from a normal terminal. It saves the current account, runs \u003ccode\u003eclaude auth login\u003c/code\u003e, saves the new one and suggests a short name for it.\u003c/p\u003e\n\u003ch2 id=\"making-it-pleasant\"\u003eMaking it pleasant\u003ca class=\"heading-anchor\" href=\"#making-it-pleasant\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eOnce it worked I gave it some polish, because a tool you reach for in the middle of real work should feel good to run.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe banner.\u003c/strong\u003e \u003ccode\u003eoof help\u003c/code\u003e opens with a big OOF in block letters that fades through Claude\u0026rsquo;s orange from top to bottom. The letters are solid blocks with box-drawing characters as a drop shadow. I wanted the blocks colored and the shadow dim, and zsh can do that with one substitution per line:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-zsh\" data-lang=\"zsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eline\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eart\u003c/span\u003e\u003cspan class=\"p\"\u003e[i]//(#m)[╔╗╚╝═║]##/\u003c/span\u003e\u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eR\u003c/span\u003e\u003cspan class=\"si\"\u003e}${\u003c/span\u003e\u003cspan class=\"nv\"\u003eD\u003c/span\u003e\u003cspan class=\"si\"\u003e}${\u003c/span\u003e\u003cspan class=\"nv\"\u003eMATCH\u003c/span\u003e\u003cspan class=\"si\"\u003e}${\u003c/span\u003e\u003cspan class=\"nv\"\u003eR\u003c/span\u003e\u003cspan class=\"si\"\u003e}${\u003c/span\u003e\u003cspan class=\"nv\"\u003egrad\u003c/span\u003e\u003cspan class=\"p\"\u003e[i]\u003c/span\u003e\u003cspan class=\"si\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWith \u003ccode\u003eextended_glob\u003c/code\u003e on, \u003ccode\u003e(#m)\u003c/code\u003e captures each run of shadow characters into \u003ccode\u003e$MATCH\u003c/code\u003e, and the replacement wraps it in \u0026ldquo;reset, dim, the shadow, then back to this row\u0026rsquo;s gradient color\u0026rdquo;. It uses truecolor when the terminal supports it, falls back to the 256-color palette otherwise, and turns color off entirely for pipes, \u003ccode\u003eNO_COLOR\u003c/code\u003e and dumb terminals.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe menu.\u003c/strong\u003e \u003ccode\u003eoof pick\u003c/code\u003e is an arrow-key menu in plain zsh, with no fzf or other dependency. It opens \u003ccode\u003e/dev/tty\u003c/code\u003e on its own file descriptor, so the menu keeps working while stdout is captured, which matters because the chosen account is printed back to the caller. Then it puts the terminal into raw mode with \u003ccode\u003estty -icanon -echo\u003c/code\u003e and reads one key at a time. The fiddly part is the Escape key. An arrow key arrives as Escape followed by \u003ccode\u003e[A\u003c/code\u003e, while plain Escape arrives alone, so after an Escape the menu waits 50 milliseconds for more bytes. If \u003ccode\u003e[A\u003c/code\u003e or \u003ccode\u003e[B\u003c/code\u003e shows up it moves the selection, and if nothing comes it treats it as Escape and cancels. The menu redraws in place by moving the cursor up and clearing each line, a trap restores the terminal if you hit Ctrl+C partway through, and it starts on the next account rather than the current one, since you opened it to leave.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe list.\u003c/strong\u003e \u003ccode\u003eoof ls\u003c/code\u003e shows each account\u0026rsquo;s short name, email, plan and when it was last active, with a green dot on the current one. The plan comes from the stored token data, where the subscription type and rate tier turn into labels like \u0026ldquo;Max 20x\u0026rdquo; or \u0026ldquo;Team\u0026rdquo;. \u0026ldquo;Last active\u0026rdquo; is simply the modification time of the account\u0026rsquo;s file, which the hooks refresh on every save.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCompletion.\u003c/strong\u003e Pressing Tab after \u003ccode\u003eoof\u003c/code\u003e lists the saved accounts, with each one\u0026rsquo;s email, plan and whether it\u0026rsquo;s active, next to the commands. A hidden \u003ccode\u003eoof __accounts\u003c/code\u003e command prints one \u003ccode\u003evalue:description\u003c/code\u003e line per account, and the completion function hands those to zsh\u0026rsquo;s \u003ccode\u003e_describe\u003c/code\u003e. The completion script itself comes from \u003ccode\u003eoof completion zsh\u003c/code\u003e, so it can\u0026rsquo;t drift out of sync with the commands. One gotcha worth knowing: if your \u003ccode\u003e.zshrc\u003c/code\u003e runs \u003ccode\u003ecompinit -C\u003c/code\u003e, zsh trusts its cached dump and won\u0026rsquo;t notice a new completion file until you delete \u003ccode\u003e~/.zcompdump\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"is-it-safe-with-several-sessions-open\"\u003eIs it safe with several sessions open?\u003ca class=\"heading-anchor\" href=\"#is-it-safe-with-several-sessions-open\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eI usually have a handful of Claude Code sessions open in cmux, so this was my main worry. Say I switch to account B while an older session still has account A in memory. If that session refreshed its token, it might write A\u0026rsquo;s tokens back over B\u0026rsquo;s, and then my hook would save A\u0026rsquo;s tokens under B\u0026rsquo;s name.\u003c/p\u003e\n\u003cp\u003eBefore adding locks and timers I looked at what Claude Code actually does when it refreshes, and it already handles this. Before refreshing, it re-reads what\u0026rsquo;s stored, and if the stored token isn\u0026rsquo;t the one it was holding, it adopts the stored one instead of refreshing its own. When it does refresh, it only writes the result back if the stored token is still the one it started from. An old session can\u0026rsquo;t overwrite a newer login, so the scary version of the race doesn\u0026rsquo;t happen.\u003c/p\u003e\n\u003cp\u003eThere are two smaller rough edges I know about and haven\u0026rsquo;t fixed yet:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eThe token and the label are two separate writes.\u003c/strong\u003e If a hook\u0026rsquo;s save lands in the few milliseconds between \u003ccode\u003eoof\u003c/code\u003e writing the Keychain and writing \u003ccode\u003e~/.claude.json\u003c/code\u003e, it pairs the new account\u0026rsquo;s token with the old account\u0026rsquo;s name. A \u003ccode\u003e/login\u003c/code\u003e in another window has a wider version of the same gap, since it fetches the profile over the network between its two writes. The proper fix is to hold Claude Code\u0026rsquo;s own lock files during a switch and have \u003ccode\u003esave\u003c/code\u003e stand down while a switch is in progress. Until then, \u003ccode\u003eoof\u003c/code\u003e warns when other \u003ccode\u003eclaude\u003c/code\u003e processes are running so I can restart them, and if a pairing ever goes wrong, one \u003ccode\u003e/login\u003c/code\u003e repairs it on the next reply.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThe secret briefly appears in a process argument.\u003c/strong\u003e \u003ccode\u003esecurity add-generic-password -w \u0026quot;$secret\u0026quot;\u003c/code\u003e puts the token on the command line, where \u003ccode\u003eps\u003c/code\u003e can see it for a moment. Claude Code avoids this by feeding \u003ccode\u003esecurity -i\u003c/code\u003e its commands on stdin. On a single-user laptop the risk is small, but it\u0026rsquo;s the next thing I\u0026rsquo;d change.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"install-oof\"\u003eInstall oof\u003ca class=\"heading-anchor\" href=\"#install-oof\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eYou need macOS, zsh, Claude Code and \u003ccode\u003ejq\u003c/code\u003e (\u003ccode\u003ebrew install jq\u003c/code\u003e). It\u0026rsquo;s one file, so read it before you run it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-zsh\" data-lang=\"zsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir -p ~/.local/bin\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl -fsSL https://gist.githubusercontent.com/ankushKun/cd27fb07b0f06be99d4181b40309103d/raw/oof.sh -o ~/.local/bin/oof\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echmod +x ~/.local/bin/oof\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eMake sure \u003ccode\u003e~/.local/bin\u003c/code\u003e is on your \u003ccode\u003ePATH\u003c/code\u003e, then:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAdd the two hooks above to \u003ccode\u003e~/.claude/settings.json\u003c/code\u003e, merging them with any hooks you already have. You can confirm they\u0026rsquo;re active with \u003ccode\u003e/hooks\u003c/code\u003e inside Claude Code.\u003c/li\u003e\n\u003cli\u003eInstall tab completion with \u003ccode\u003eoof completion zsh \u0026gt; \u0026quot;$(brew --prefix)/share/zsh/site-functions/_oof\u0026quot;\u003c/code\u003e, then run \u003ccode\u003erm -f ~/.zcompdump\u003c/code\u003e and open a new terminal.\u003c/li\u003e\n\u003cli\u003eRun \u003ccode\u003eoof login\u003c/code\u003e once for each account you want to add, and give each one a short name with \u003ccode\u003eoof name you@company.com work\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAfter that, \u003ccode\u003eoof\u003c/code\u003e rotates to the next account, \u003ccode\u003eoof work\u003c/code\u003e jumps to a named one, \u003ccode\u003eoof pick\u003c/code\u003e shows the menu, and \u003ccode\u003eoof ls\u003c/code\u003e lists everything. The first time it touches the Keychain, macOS may ask for permission, and \u0026ldquo;Always Allow\u0026rdquo; stops it asking again.\u003c/p\u003e\n\u003ch2 id=\"a-note-on-limits\"\u003eA note on limits\u003ca class=\"heading-anchor\" href=\"#a-note-on-limits\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe name is a joke about hitting a limit, but \u003ccode\u003eoof\u003c/code\u003e is for switching between accounts you actually have, like a personal plan and a seat on your company\u0026rsquo;s Team plan. Each account keeps its own limits, and switching doesn\u0026rsquo;t change them. If what you keep running into is the usage limit on a single plan, the supported routes are extra usage, a bigger plan or an API key. Before you set up a stack of personal subscriptions just to rotate through them, read Anthropic\u0026rsquo;s terms first.\u003c/p\u003e\n\u003ch2 id=\"wrapping-up\"\u003eWrapping up\u003ca class=\"heading-anchor\" href=\"#wrapping-up\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe whole thing is under 600 lines of zsh, and most of that is help text, colors and the menu. The core idea fits in a paragraph. Conversations live on disk and not in your account, and the login is a Keychain item plus a label. Keep a fresh copy of each account\u0026rsquo;s item, swap both pieces together, and switching becomes one command that drops you back into the same conversation.\u003c/p\u003e\n\u003cp\u003eI built it in one sitting with Claude Code, against Claude Code 2.1.296. For testing, the \u003ccode\u003eOOF_CONFIG\u003c/code\u003e and \u003ccode\u003eOOF_STORE\u003c/code\u003e variables point the script at a throwaway config and account store, so the menus, naming and completion could be exercised against fake accounts without touching real logins. That\u0026rsquo;s also how the screenshot at the top was made. The script is \u003ca href=\"https://gist.github.com/ankushKun/cd27fb07b0f06be99d4181b40309103d\" data-external\u003eon GitHub Gist\u003c/a\u003e if you want to read it, use it or adapt it for Linux. It started life as \u003ccode\u003eccs\u003c/code\u003e, and logins saved by that version still work after the rename.\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-10-12T01:00:00+05:30","id":"https://ankush.one/blogs/oof-claude-code-account-switcher/","image":"https://ankush.one/media/oof-og.jpg","summary":"Switch between multiple Claude Code accounts on macOS with one command and keep the same conversation. How oof swaps the Keychain login and resumes it.","tags":["blogs","claude-code","developer-tools","ai-agents"],"title":"How to switch Claude Code accounts without losing your conversation","url":"https://ankush.one/blogs/oof-claude-code-account-switcher/"},{"content_html":"\u003cp\u003eWhen Cloudflare released Clef on October 1, its launch post carried a table of ten public benchmarks, seven of them led by a Clef model. The Hacker News thread passed 600 points in a day, and one of the first replies was six words long: \u0026ldquo;Public benchmarks are easy to cheat\u0026rdquo; (\u003ca href=\"https://traictory.com/news/2026-10-03-cloudflare-clef-decision-models\" data-external\u003eTraictory\u003c/a\u003e, \u003ca href=\"https://news.ycombinator.com/item?id=49923692\" data-external\u003eHacker News\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eFair. So we ran the test nobody could have trained for. Construct\u0026rsquo;s agent has about 40 small decisions written for a decision model, and every one of them was written around Jev: the prompts, the questions, and the thresholds, all built against Jev over the last two weeks. That is Jev\u0026rsquo;s home turf. We pointed the same requests at Clef and Clef-flash without changing a line, and scored all three against labelled cases.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eClef is Cloudflare\u0026rsquo;s open-weight decision model, a drop-in alternative to TypeSafe\u0026rsquo;s Jev. On 117 labelled cases across five of our production decisions, Clef reached the right outcome on 92.3% of calls using thresholds set for Jev, against 88.6% for Jev and 82.1% for Clef-flash. Both Clef models returned the identical probability on every repeated call, and Jev did not.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eWe build Construct, an AI employee with its own computer. Every number below comes from our own runs on \u003cstrong\u003eOctober 4, 2026\u003c/strong\u003e (UTC): 117 cases, three calls each, three models, 1,053 calls in the main run, plus about 800 more in follow-up controls. Facts about the models themselves come from Cloudflare\u0026rsquo;s and TypeSafe\u0026rsquo;s documentation, checked the same day. The full method, confidence intervals and per-case results are in the paper: \u003ca href=\"https://construct.computer/blog/clef-vs-jev-benchmark/compatible-apis-incompatible-thresholds.pdf\" data-external\u003eCompatible APIs, Incompatible Thresholds: A Drop-In Replacement Study of Clef and Jev on Decision Specifications from a Production Agent\u003c/a\u003e (PDF, 13 pages).\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAccuracy, drop-in:\u003c/strong\u003e Clef 92.3%, Jev 88.6%, Clef-flash 82.1%, all on thresholds set for Jev. The gap between Clef and Jev is not statistically significant.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAccuracy, own thresholds:\u003c/strong\u003e Clef 93.2%, Jev 90.9%, Clef-flash 90.6%, with each threshold chosen on held-out cases.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRepeatability:\u003c/strong\u003e Clef and Clef-flash gave the same probability on all 351 calls each. The probability Jev\u0026rsquo;s decisions act on moved by up to 0.10, and 2 cases changed verdict.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCode changed to swap models:\u003c/strong\u003e one string.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFailed calls:\u003c/strong\u003e 0 of 1,053.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCost:\u003c/strong\u003e $23 per million decisions on Jev, $35 on Clef-flash, $93 on Clef. The main run cost about five cents.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"what-are-clef-and-clef-flash\"\u003eWhat are Clef and Clef-flash?\u003ca class=\"heading-anchor\" href=\"#what-are-clef-and-clef-flash\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eClef and Clef-flash are decision models from Cloudflare: you send a state and a set of typed questions, and they return a probability for each allowed answer instead of generating text. Clef is a 27 billion parameter model built on Qwen3.8, and Clef-flash is a 9 billion parameter model built on Qwen3.5. Both are released under Apache 2.0 with weights on Hugging Face, both run on Workers AI, and both accept the same request format as Jev (\u003ca href=\"https://blog.cloudflare.com/clef-decision-models/\" data-external\u003eCloudflare\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eIf decision models are new to you: they are the fast, cheap judgment calls around a language model. Is this the same person? Does this email need a reply? Should this tool call wait for a human? TypeSafe calls the category System One models, after Kahneman\u0026rsquo;s fast, intuitive System 1. Our \u003ca href=\"https://construct.computer/blog/jev-ai-agents/\" data-external\u003efirst post on Jev\u003c/a\u003e explains the idea and how we put one inside an AI agent\u0026rsquo;s memory.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev 1.13\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef-flash\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMaker\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTypeSafe AI\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloudflare\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloudflare\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eSize\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNot published\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e27B parameters\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e9B parameters\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWeights\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClosed\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOpen, Apache 2.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOpen, Apache 2.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eQuestion types\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eChoice, Score, Noul\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSame\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSame\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInput\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eText and JSON\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eText, JSON and images\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eText, JSON and images\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eContext\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e64k tokens per request\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e64k tokens\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e64k tokens\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003ePrice per million input tokens\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.042\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.24\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.09\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eOutput tokens\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFree\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFree\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFree\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWorkers AI model ID\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003etypesafe/jev\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@cf/cloudflare/clef\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@cf/cloudflare/clef-flash\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eJev, Clef and Clef-flash compared, October 2026\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eTwo details in that table matter later. Clef reads images, which Jev does not today. And the prices are per input token, so the model that counts fewer tokens for the same request closes some of the gap.\u003c/p\u003e\n\u003ch2 id=\"how-we-benchmarked-clef-against-jev\"\u003eHow we benchmarked Clef against Jev\u003ca class=\"heading-anchor\" href=\"#how-we-benchmarked-clef-against-jev\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eWe did not write a new test for Clef. We took five decisions that already exist in Construct\u0026rsquo;s code and ran each one\u0026rsquo;s real request builder and real interpreter, the same functions production calls.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDecision\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhat it decides\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCases\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eThreshold today\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEntity match\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIs this newly mentioned person, project or company one the agent already knows?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e30\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.70\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEmail triage\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNewsletter, receipt, spam, request, or keep?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e15\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.80\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMemory extract gate\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDoes this conversation hold anything worth remembering?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e24\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.05\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eNotification priority\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDoes this automation notice deserve a toast, or can it wait?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e24\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.85\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTool-call risk\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eShould this tool call wait for the user\u0026rsquo;s go-ahead?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e24\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.80\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eThe five production decisions in the benchmark\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eEntity match is live in production. The other four are built and switched off, waiting on shadow data. Their thresholds were set by judgment with Jev in mind and have never been fitted to data, for Jev or anything else.\u003c/p\u003e\n\u003cp\u003eSwapping the model was the least interesting part of the work. This is the whole change:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ts\" data-lang=\"ts\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Before\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003eenv\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eAI\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;typesafe/jev\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003equestions\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"nx\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// After\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003eenv\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eAI\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;@cf/cloudflare/clef\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003equestions\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"nx\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCloudflare says Clef is \u0026ldquo;fully Jev-API compatible\u0026rdquo;. On our requests that held: Choice, Score and Noul questions, JSON state, and the response shape all worked unchanged, and all 702 Clef calls returned valid answers.\u003c/p\u003e\n\u003cp\u003eThe rest of the method, briefly:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCases:\u003c/strong\u003e 117, all synthetic, weighted toward the edges of each decision. We drafted and labelled them with an AI assistant (Claude) before any of the three models saw them. No label came from Jev or Clef, and the labels have not yet been independently reviewed by a second person.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRuns:\u003c/strong\u003e each case three times per model, one call at a time, models interleaved so they shared the same network conditions, with the gateway cache skipped.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScoring:\u003c/strong\u003e a call is correct when the production interpreter reaches the labelled outcome at the production threshold. We also report each model\u0026rsquo;s best threshold, found on the same cases, which flatters all three equally.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRoute:\u003c/strong\u003e Cloudflare\u0026rsquo;s REST API through our AI Gateway, from a laptop. Fine for accuracy. Wrong for latency, which gets its own section.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"clef-vs-jev-accuracy-the-results\"\u003eClef vs Jev accuracy: the results\u003ca class=\"heading-anchor\" href=\"#clef-vs-jev-accuracy-the-results\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/clef-vs-jev-benchmark/01-accuracy-overall.webp\" alt=\"Bar chart of overall accuracy. With thresholds set for Jev: Jev 88.6%, Clef 92.3%, Clef-flash 82.1%. With each model’s own best threshold: Jev 93.7%, Clef 96.6%, Clef-flash 94.9%.\" title=\"Overall accuracy across five production decisions, 351 calls per model. Left: the thresholds configured today, set for Jev. Right: each model\u0026#39;s own best threshold, found in-sample.\" width=\"2000\" height=\"1125\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eOverall accuracy across five production decisions, 351 calls per model. Left: the thresholds configured today, set for Jev. Right: each model\u0026#39;s own best threshold, found in-sample.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eWith no retuning at all, Clef had the highest score of the three, and the highest or equal-highest on four of the five decisions, on thresholds that were set for a different model.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDecision\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev 1.13\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef-flash\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEntity match\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e72.2% (82.2%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e86.7%\u003c/strong\u003e (93.3%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e80.0% (86.7%)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEmail triage\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e93.3%\u003c/strong\u003e (93.3%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e86.7% (93.3%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e73.3% (86.7%)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMemory extract gate\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e100%\u003c/strong\u003e (100%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e100%\u003c/strong\u003e (100%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e95.8% (100%)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eNotification priority\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e95.8% (100%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e100%\u003c/strong\u003e (100%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e75.0% (100%)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTool-call risk\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e87.5%\u003c/strong\u003e (95.8%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e87.5%\u003c/strong\u003e (95.8%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e83.3% (100%)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAll five\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e88.6% (93.7%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e92.3%\u003c/strong\u003e (96.6%)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e82.1% (94.9%)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eAccuracy by decision at the production threshold, with accuracy at each model\u0026rsquo;s own best threshold in brackets\u003c/em\u003e\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/clef-vs-jev-benchmark/02-accuracy-by-decision.webp\" alt=\"Grouped bar chart of accuracy for five decisions. Entity match: Jev 72%, Clef 87%, Clef-flash 80%. Email triage: 93%, 87%, 73%. Memory extract gate: 100%, 100%, 96%. Notification priority: 96%, 100%, 75%. Tool-call risk: 88%, 88%, 83%.\" title=\"Accuracy per decision at today\u0026#39;s thresholds. Jev keeps email triage. Clef takes entity matching and notification priority.\" width=\"2000\" height=\"1125\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eAccuracy per decision at today\u0026#39;s thresholds. Jev keeps email triage. Clef takes entity matching and notification priority.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eThe honest size of this result: Clef leads Jev by 3.7 points on 117 cases, with a 95% interval of -1.1 to +8.8 points. That is not statistically significant, and we would not stretch it into \u0026ldquo;Clef beats Jev\u0026rdquo;. Three of the entity cases are ones our product resolves in code before any model is asked (identical names, and names that differ only in case). Jev did worst on exactly those, and without them the lead shrinks to 2.0 points (92.1% against 90.1%). What we can say is narrower and more useful. Cloudflare\u0026rsquo;s public numbers predicted Clef would be at least as good as Jev on decision tasks, and on private prompts it had never seen, written for its competitor, it was not measurably worse.\u003c/p\u003e\n\u003cp\u003eJev deserves its due here too. It won email triage outright, it never made a wrong merge, and it was perfect on the extract gate. This is a close race between two good models.\u003c/p\u003e\n\u003ch2 id=\"is-clef-deterministic\"\u003eIs Clef deterministic?\u003ca class=\"heading-anchor\" href=\"#is-clef-deterministic\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eYes, on everything we sent it. We called each model three times with the same request for all 117 cases. Clef returned the same probability, to four decimal places, on all 351 calls. So did Clef-flash. To rule out a cache, we sent the same requests again with changes that do not touch the model\u0026rsquo;s input (a unique query string, reformatted JSON, reordered keys): same answers, a cache miss on every response, and a new inference ID each time.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/clef-vs-jev-benchmark/05-repeatability.webp\" alt=\"Bar chart of the largest probability change between three identical calls, by decision. Jev: 0.07, 0.04, 0.03, 0.10, 0.04. Clef and Clef-flash: 0.00 on every decision.\" title=\"Largest change in probability between three identical calls. Clef and Clef-flash sit at zero on every decision.\" width=\"2000\" height=\"1125\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eLargest change in probability between three identical calls. Clef and Clef-flash sit at zero on every decision.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eThe probability each Jev decision acts on moved between identical calls by up to 0.10, and two cases changed verdict from one call to the next. Across every probability Jev returned, the largest move was 0.16, and 100 of the 117 cases came back with at least one number different. TypeSafe describes Jev as returning \u0026ldquo;similar answers for similar inputs\u0026rdquo;, which is a promise of consistency, not determinism, and a wobble of this size is consistent. But a decision layer acts when a probability crosses a line, so any wobble means a case near the line is decided by which call you happened to make.\u003c/p\u003e\n\u003cp\u003eCloudflare\u0026rsquo;s post explains why Clef behaves differently: the decision step is non-autoregressive, with choices derived directly from the model\u0026rsquo;s internal representations and no text sampled along the way. We can confirm the practical result. For anyone who has debugged an agent that did something different the second time, this is the finding that matters most, more than a few points of accuracy.\u003c/p\u003e\n\u003ch2 id=\"jevs-scores-moved-under-the-same-version-name\"\u003eJev\u0026rsquo;s scores moved under the same version name\u003ca class=\"heading-anchor\" href=\"#jevs-scores-moved-under-the-same-version-name\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThis one surprised us. On September 20 we calibrated our live entity matcher on Jev. Real matches scored 0.71 to 0.82, everything else scored 0.34 or less, and we put the merge threshold at 0.70, in the gap.\u003c/p\u003e\n\u003cp\u003eTwo weeks later we asked the same three calibration probes again.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/clef-vs-jev-benchmark/04-jev-drift.webp\" alt=\"Dot plot of Jev’s probability on three calibration probes. Dr. Priya Shah equals Priya Shah: 0.71 on 20 September; on 4 October, 0.47 through Workers AI and 0.46 on TypeSafe’s own API. Priya Shah equals Priya Shah: 0.76, then 0.42 and 0.43. Atlas migration: 0.82, then 0.60 and 0.61. The merge threshold is 0.70.\" title=\"Jev\u0026#39;s probability for the correct match on our three calibration probes: one call on September 20, and the mean of 20 calls on October 4 through Workers AI and on TypeSafe\u0026#39;s own API with the version pinned. All were served as jev-1.13.0.\" width=\"2000\" height=\"1125\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eJev\u0026#39;s probability for the correct match on our three calibration probes: one call on September 20, and the mean of 20 calls on October 4 through Workers AI and on TypeSafe\u0026#39;s own API with the version pinned. All were served as jev-1.13.0.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eAll three now fall under the bar. To be sure this was not a noisy afternoon, we asked each probe 20 more times: none of the 60 calls reached 0.70, and the highest was 0.66. Clef, asked the same 20 times, returned the identical number every time. The served version was \u003ccode\u003ejev-1.13.0\u003c/code\u003e both times, so the alarm we built to catch version changes stayed quiet. On this benchmark Jev still never merged two different people, which is the expensive mistake, but it made only 44% of the merges it should have. In production, a missed merge becomes a duplicate.\u003c/p\u003e\n\u003cp\u003eWe do not know why the scores moved. Our first suspect was our own route: we call Jev through Cloudflare\u0026rsquo;s Workers AI, which cannot pin a version. So we repeated everything against TypeSafe\u0026rsquo;s own API with the version pinned to \u003ccode\u003ejev-1.13.0\u003c/code\u003e. Same result: none of 60 calls reached 0.70, and Jev\u0026rsquo;s answers still varied between identical calls on 104 of 117 cases. The route is not the cause. Three probes are still three probes, and our September baseline is one call each. We are not claiming Jev got worse in general. The lesson is about our own setup: a threshold is only as stable as the model under it, and a version string is not a guarantee. Open weights can change that, if you use them: a model you pin or host yourself cannot move under its thresholds. The hosted Clef alias we tested carries no version either, so we will re-ask these same probes on Clef in two weeks.\u003c/p\u003e\n\u003ch2 id=\"entity-matching-where-the-three-models-differ-most\"\u003eEntity matching: where the three models differ most\u003ca class=\"heading-anchor\" href=\"#entity-matching-where-the-three-models-differ-most\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEntity matching is the decision where a mistake is permanent. A wrong merge fuses two real people in the agent\u0026rsquo;s memory, so precision matters more than recall. \u003ca href=\"https://construct.computer/blog/ai-agent-memory/\" data-external\u003eAI agent memory\u003c/a\u003e covers what that memory holds and how you can correct it.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/clef-vs-jev-benchmark/03-entity-match.webp\" alt=\"Bar chart for entity matching at a 0.70 threshold. Correct merges made: Jev 44%, Clef 80%, Clef-flash 80%. Merges that were right: Jev 100%, Clef 92%, Clef-flash 80%.\" title=\"Entity matching at the production threshold of 0.70: 15 pairs that should merge and 15 that should not.\" width=\"2000\" height=\"1125\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eEntity matching at the production threshold of 0.70: 15 pairs that should merge and 15 that should not.\u003c/figcaption\u003e\u003c/figure\u003e\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eClef\u003c/strong\u003e caught the aliases this decision exists for: \u0026ldquo;Jose Garcia Marquez\u0026rdquo; against the accented spelling at 0.98, \u0026ldquo;Stripe, Inc.\u0026rdquo; against \u0026ldquo;Stripe\u0026rdquo; at 0.98, \u0026ldquo;I.B.M.\u0026rdquo; against \u0026ldquo;IBM\u0026rdquo; at 0.99. Its one wrong merge was \u0026ldquo;Acme\u0026rdquo; into a lone \u0026ldquo;Acme Corporation\u0026rdquo; at 0.78, a case reasonable people would argue about.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAt a 0.80 threshold, Clef made 10 of 15 correct merges and no wrong ones.\u003c/strong\u003e That is our starting point for shadow mode, not a recommendation: it was picked on these same 30 cases, and \u0026ldquo;Acme\u0026rdquo; against \u0026ldquo;Acme Corporation\u0026rdquo; sits at 0.78, just under it.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJev is not worse at telling people apart.\u003c/strong\u003e Drop its threshold to 0.50 and it makes 64% of the correct merges, still with no wrong ones, close to Clef\u0026rsquo;s 67% at 0.80. Its problem at 0.70 is that its scores moved, not that it lost the ability to rank the pairs.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClef-flash\u003c/strong\u003e is quick to say yes. It matched \u0026ldquo;Acme Labs Europe\u0026rdquo; to \u0026ldquo;Acme Labs\u0026rdquo; at 0.76 and the bare concept \u0026ldquo;pricing\u0026rdquo; to \u0026ldquo;pricing model\u0026rdquo; at 0.89. For a merge, that is too eager. For a gate that only skips work, it is fine.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAll three\u003c/strong\u003e refused a one-letter surname typo, \u0026ldquo;Mendelsohn\u0026rdquo; against \u0026ldquo;Mendelson\u0026rdquo;. Good. Those can be two people.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"thresholds-do-not-transfer-between-decision-models\"\u003eThresholds do not transfer between decision models\u003ca class=\"heading-anchor\" href=\"#thresholds-do-not-transfer-between-decision-models\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eIf you take one thing from this post into your own migration, take this. A threshold is a property of a model and a prompt together. Move the model and the threshold is wrong, even when the new model is better.\u003c/p\u003e\n\u003cp\u003eThe three models are not far apart in ability. Asked to rank our cases from \u0026ldquo;should act\u0026rdquo; to \u0026ldquo;should not\u0026rdquo;, all three do it well: on the four decisions we could measure this way, the ranking score (AUROC) is 0.93 or higher for every model and a perfect 1.00 on two decisions. What differs is the number each model attaches to the same judgment.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDecision\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eConfigured\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev\u0026rsquo;s best\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef\u0026rsquo;s best\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef-flash\u0026rsquo;s best\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTool-call risk\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.80\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.49\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.22\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.31\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eNotification priority\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.85\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.60\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.85\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.61\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eThe most accurate threshold per model on two decisions, against the configured threshold\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eClef-flash shows the effect best. On Jev\u0026rsquo;s thresholds it scored 82.1%. With thresholds chosen for it on held-out cases it scored 90.6%, level with Jev\u0026rsquo;s 90.9% on the same footing, and Clef stayed ahead at 93.2%. The 9B model was never bad at these decisions. It was being read with another model\u0026rsquo;s ruler.\u003c/p\u003e\n\u003cp\u003eThe thresholds in the table were found on the same cases they are scored on, which is why the bracketed figures in the results table (96.6%, 94.9%, 93.7%) run higher than the held-out ones. Treat them as a direction and not as settings. The practical rule is simple: swap the model in shadow, log probabilities for a week, and set the threshold from that.\u003c/p\u003e\n\u003ch2 id=\"how-fast-is-clef-what-we-could-and-could-not-measure\"\u003eHow fast is Clef? What we could and could not measure\u003ca class=\"heading-anchor\" href=\"#how-fast-is-clef-what-we-could-and-could-not-measure\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eCloudflare reports a median of 209 ms for Clef and 38.8 ms for Clef-flash, against 524 ms for Jev. We are not going to confirm or dispute those numbers, because our harness was the wrong instrument.\u003c/p\u003e\n\u003cp\u003eWe called all three models through Cloudflare\u0026rsquo;s REST API from a laptop. The fastest call we saw for any model was 340 ms, and for every model the floor sat between 340 and 384 ms. That floor is the path: authentication, the public API, the gateway, and the round trip. You cannot see a 38 ms model through a 340 ms window.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev 1.13\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eClef-flash\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eFastest call\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e384 ms\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e352 ms\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e340 ms\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMedian\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e454 ms\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e654 ms\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e449 ms\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e95th percentile\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e591 ms\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1,262 ms\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1,024 ms\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eLatency per call through the REST API from a laptop, 351 calls per model. Not a production measurement.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eFor transparency, those are the raw numbers. On this path Clef-flash matched Jev at the median, Clef was slower, and both Clef models had longer tails, three days after launch. None of that tells you what a Worker sees when it calls the model through the AI binding, next to the GPU, which is how we call it in production. That measurement is next, and we will publish it here.\u003c/p\u003e\n\u003ch2 id=\"what-does-clef-cost-compared-with-jev\"\u003eWhat does Clef cost compared with Jev?\u003ca class=\"heading-anchor\" href=\"#what-does-clef-cost-compared-with-jev\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/clef-vs-jev-benchmark/07-cost.webp\" alt=\"Bar chart of cost per million decisions: Jev 23 dollars at 541 input tokens per call, Clef 93 dollars at 387 tokens, Clef-flash 35 dollars at 387 tokens.\" title=\"Cost per million decisions at list price, using the mean input tokens each model counted for our requests.\" width=\"2000\" height=\"1125\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eCost per million decisions at list price, using the mean input tokens each model counted for our requests.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eClef\u0026rsquo;s list price is 5.7 times Jev\u0026rsquo;s, which is the first thing the Hacker News thread did arithmetic on. Two things soften that.\u003c/p\u003e\n\u003cp\u003eFirst, Clef counted 28% fewer tokens than Jev for the same request: 387 per call against 541. Per decision, the gap is about 4 times, not 5.7.\u003c/p\u003e\n\u003cp\u003eSecond, look at the unit. A million Clef decisions cost $93. All 1,053 calls in this benchmark cost about five cents. An agent that makes a thousand decisions a day spends about nine cents a day on Clef and two on Jev, next to a language model bill many times that. If you run billions of decisions a day, price it carefully, and look at Clef-flash at $35 per million. For everyone else, accuracy and repeatability are worth more than the difference.\u003c/p\u003e\n\u003ch2 id=\"clef-vs-jev-which-decision-model-should-you-use\"\u003eClef vs Jev: which decision model should you use?\u003ca class=\"heading-anchor\" href=\"#clef-vs-jev-which-decision-model-should-you-use\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eIf you need\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePick\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhy\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe best accuracy without retuning\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHighest drop-in accuracy on our decisions, 92.3%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe same answer every time\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef or Clef-flash\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIdentical probabilities on every repeated call\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTo pin a version, self-host, or fine-tune\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef or Clef-flash\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOpen weights under Apache 2.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTo classify images\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef or Clef-flash\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBoth have a vision encoder; Jev is text only\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe lowest price per token\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJev\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.042 per million input tokens\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eZero wrong merges at your current thresholds\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJev\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e100% precision on entity matching, at the cost of recall\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eA high-volume gate that only skips work\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef-flash, with its own thresholds\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e90.6% once retuned on held-out cases, at about a third of Clef\u0026rsquo;s price\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAnything that merges, deletes or sends\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef, at a high threshold\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClef-flash was too eager on lookalike names\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eWhich decision model fits which job, based on our benchmark and each vendor\u0026rsquo;s documentation\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThe broader field is moving fast. Besides Jev and Clef there are open decision models such as Laya, Kev and OpenDecider, and Cloudflare\u0026rsquo;s own table includes several of them. We have only tested the three in this post.\u003c/p\u003e\n\u003ch2 id=\"what-we-are-switching-and-why-cloudflare-makes-it-easy\"\u003eWhat we are switching, and why Cloudflare makes it easy\u003ca class=\"heading-anchor\" href=\"#what-we-are-switching-and-why-cloudflare-makes-it-easy\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct already runs on Cloudflare: Workers, Durable Objects, AI Gateway, and Workers AI for the small models. \u003ca href=\"https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/\" data-external\u003eHow our agents get computers we mostly do not pay for\u003c/a\u003e describes that stack. So Clef is not a new vendor for us. It is a new model ID on a binding we already call.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEntity matching goes to Clef in shadow first, starting at 0.80.\u003c/strong\u003e Jev keeps deciding while Clef\u0026rsquo;s answers are recorded beside it. The threshold we ship will come from that data, and we switch only if real traffic agrees with this benchmark.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBackground decisions default to Clef,\u003c/strong\u003e each with its own threshold measured in shadow.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInteractive decisions wait for the latency test.\u003c/strong\u003e If Clef-flash is as fast from a Worker as Cloudflare reports, it is the natural fit for anything a person is waiting on.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClef-flash stays away from anything that merges or deletes\u003c/strong\u003e until it has its own thresholds.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThere is a quieter benefit. In our Jev post we said that decisions which read your conversations or email \u0026ldquo;wait for the paperwork\u0026rdquo;, because sending that text to another provider is a privacy decision. A first-party Cloudflare model keeps that text on the platform that already runs the rest of Construct, which removes the main reason most of our decisions are still switched off.\u003c/p\u003e\n\u003cp\u003eWe are also watching Cloudflare\u0026rsquo;s new fine-tuning work, which it calls Reinforcement Learning for Calibrated Decisions. Our decision layer already records the label, the probability and what the product did for every decision, with no user text. That is close to the dataset such a system wants.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eRead next: \u003ca href=\"https://construct.computer/blog/jev-ai-agents/\" data-external\u003eWe put Jev inside our AI employee\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"how-to-try-clef\"\u003eHow to try Clef\u003ca class=\"heading-anchor\" href=\"#how-to-try-clef\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eOn Workers AI:\u003c/strong\u003e call \u003ccode\u003e@cf/cloudflare/clef\u003c/code\u003e or \u003ccode\u003e@cf/cloudflare/clef-flash\u003c/code\u003e through the AI binding or the REST API. The request is a \u003ccode\u003estate\u003c/code\u003e plus a map of \u003ccode\u003equestions\u003c/code\u003e, the same as Jev.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eComing from Jev:\u003c/strong\u003e change the model ID and keep everything else. Then re-measure every threshold.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSelf-hosted:\u003c/strong\u003e the weights are at \u003ccode\u003eCloudflare/clef\u003c/code\u003e and \u003ccode\u003eCloudflare/clef-flash\u003c/code\u003e on Hugging Face under Apache 2.0.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRead the probabilities, not the \u003ccode\u003econfidence\u003c/code\u003e field.\u003c/strong\u003e It means different things on Clef and Jev, and on Jev it changed meaning between our September and October runs. Read \u003ccode\u003eprobabilities\u003c/code\u003e and both models behave the same.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePace a REST benchmark.\u003c/strong\u003e Cloudflare\u0026rsquo;s REST API throttled our token at roughly 90 calls a minute. The Workers binding is the route for real traffic.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA minimal request:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl https://api.cloudflare.com/client/v4/accounts/\u003cspan class=\"nv\"\u003e$ACCOUNT_ID\u003c/span\u003e/ai/run \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  -H \u003cspan class=\"s2\"\u003e\u0026#34;Authorization: Bearer \u003c/span\u003e\u003cspan class=\"nv\"\u003e$API_TOKEN\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  -d \u003cspan class=\"s1\"\u003e\u0026#39;{\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e    \u0026#34;model\u0026#34;: \u0026#34;@cf/cloudflare/clef\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e    \u0026#34;input\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e      \u0026#34;state\u0026#34;: { \u0026#34;entity\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;person\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Dr. Priya Shah\u0026#34; } },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e      \u0026#34;questions\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e        \u0026#34;match\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e          \u0026#34;type\u0026#34;: \u0026#34;choice\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e          \u0026#34;instructions\u0026#34;: \u0026#34;Which candidate is the same entity?\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e          \u0026#34;criteria\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e            \u0026#34;node-priya\u0026#34;: \u0026#34;Priya Shah (person)\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e            \u0026#34;none\u0026#34;: \u0026#34;No candidate is clearly the same entity\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e          }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e      }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s1\"\u003e  }\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"what-this-benchmark-does-not-show\"\u003eWhat this benchmark does not show\u003ca class=\"heading-anchor\" href=\"#what-this-benchmark-does-not-show\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eIt is small.\u003c/strong\u003e 117 cases, 15 to 30 per decision. A 3.7 point lead at that size is suggestive.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIt is synthetic.\u003c/strong\u003e The cases are written to cover each decision\u0026rsquo;s edges. Production traffic is mostly the easy middle, so real accuracy for all three models is probably higher.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThe labels are ours, drafted with an AI assistant.\u003c/strong\u003e No second person has checked them yet. A few are judgment calls, like whether \u0026ldquo;Acme\u0026rdquo; should merge into the only \u0026ldquo;Acme Corporation\u0026rdquo; in memory. We said no. Dropping the five most arguable ones does not change the order.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThresholds are tuned on small sets.\u003c/strong\u003e The held-out figures are the fair ones, and they are still noisy at 15 to 30 cases per decision.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLatency is unmeasured.\u003c/strong\u003e See above.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOne day.\u003c/strong\u003e Everything ran on October 4, through Cloudflare, with Jev repeated on TypeSafe\u0026rsquo;s own API. Clef was three days old. Jev\u0026rsquo;s scores moved in two weeks. Either could look different next month, which is the argument for running your own cases on a schedule.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe paper has the statistics behind each of these, the five cases no model got right, and every case where any model slipped. The cases, labels and every call are \u003ca href=\"https://construct.computer/blog/clef-vs-jev-benchmark/data/cases.json\" data-external\u003epublished as JSON\u003c/a\u003e beside it: \u003ca href=\"https://construct.computer/blog/clef-vs-jev-benchmark/compatible-apis-incompatible-thresholds.pdf\" data-external\u003eCompatible APIs, Incompatible Thresholds: A Drop-In Replacement Study of Clef and Jev on Decision Specifications from a Production Agent\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eCloudflare published benchmarks, and people on the internet said benchmarks are easy to cheat. The fix for that is boring: write down your own cases, label them, and run every new model through them. Ours took an afternoon and a few cents, and it told us more about our own system than any leaderboard could.\u003c/p\u003e\n\u003ch2 id=\"where-construct-fits\"\u003eWhere Construct fits\u003ca class=\"heading-anchor\" href=\"#where-construct-fits\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct is an AI employee with its own computer: a workspace filesystem, long-term memory, a schedule, email, and workflows you can read before you run them. Decision models make the small calls around the edges, such as whether a person your agent just read about is one it already knows. Planning and writing run on general-purpose language models.\u003c/p\u003e\n\u003cp\u003eTwo boundaries, stated plainly. As of this post, Jev still makes the live entity-matching decision and Clef has only run in this benchmark; this page will say when that changes. And the other four decisions in this benchmark are built but switched off in production until shadow data supports them.\u003c/p\u003e\n\u003cp\u003eIf decision models are new to you, start with \u003ca href=\"https://construct.computer/blog/jev-ai-agents/\" data-external\u003ewe put Jev inside our AI employee\u003c/a\u003e. For why small, repeatable judgments matter so much in long tasks, read \u003ca href=\"https://construct.computer/blog/agent-task-half-life/\" data-external\u003eyour agent has a half-life\u003c/a\u003e. And if the category itself is new, begin with \u003ca href=\"https://construct.computer/blog/what-is-an-ai-employee/\" data-external\u003ewhat is an AI employee\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/clef-vs-jev-benchmark/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-10-05T00:00:00Z","external_url":"https://construct.computer/blog/clef-vs-jev-benchmark/","id":"https://ankush.one/blogs/clef-vs-jev-benchmark/","image":"https://ankush.one/media/construct/clef-vs-jev-benchmark.jpg","summary":"We ran Cloudflare's Clef and Clef-flash against Jev on five production AI agent decisions: 1,053 calls, accuracy, determinism, drift, cost, and what we are switching.","tags":["blogs","ai-agents","machine-learning","cloudflare","construct"],"title":"Clef beat Jev on Jev's home turf","url":"https://ankush.one/blogs/clef-vs-jev-benchmark/"},{"content_html":"\u003cp\u003eYour AI employee reads an email that mentions \u0026ldquo;Dr. Priya Shah\u0026rdquo;. Last week it met \u0026ldquo;Priya Shah\u0026rdquo; in a calendar invite. Same person? Guess yes and get it wrong, and two people\u0026rsquo;s histories are welded together in its memory. Guess no and get it wrong, and you have a duplicate. It makes that call every time it learns something, in the background, where nobody is watching.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJev is a System One model from TypeSafe AI: you give it a state and a set of typed questions, and instead of generating text it returns a probability for each possible answer, in well under a second, for $0.042 per million input tokens with output free.\u003c/strong\u003e TypeSafe released it in early access on September 15, 2026. Five days later we shipped it into Construct\u0026rsquo;s memory to make exactly the call above. This post covers what we measured, how we picked the threshold, what Jev gets wrong, what it costs next to a language model, and where a decision model fits in an AI agent\u0026rsquo;s loop.\u003c/p\u003e\n\u003cp\u003eWe build Construct, an AI employee with its own computer. The numbers in this post come from our own integration, measured on September 20, 2026 against \u003ccode\u003ejev-1.13.0\u003c/code\u003e. Everything else about Jev comes from TypeSafe\u0026rsquo;s launch post and docs and from independent tests, all checked on \u003cstrong\u003eSeptember 30, 2026\u003c/strong\u003e.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLatency:\u003c/strong\u003e 358 to 611 ms for one question on about 400 tokens, and 377 to 544 ms for seven questions on the same state.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConsistency:\u003c/strong\u003e repeating the same call moved Jev\u0026rsquo;s probabilities by 0.02 at most.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThreshold:\u003c/strong\u003e we merge at 0.7. In our probe set, real matches scored 0.71 to 0.82 and everything else 0.34 or less.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCalls:\u003c/strong\u003e one Jev call replaced up to three sequential calls to a generative judge for each entity.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCost:\u003c/strong\u003e about $0.017 per 1,000 decisions of 400 input tokens at list price, with output free.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"what-is-jev\"\u003eWhat is Jev?\u003ca class=\"heading-anchor\" href=\"#what-is-jev\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eJev is a decision model, not a chat model: it never generates text, and TypeSafe says it is not a large language model. TypeSafe AI, a San Francisco company founded by Diogo Almeida (a former OpenAI researcher and co-author of the InstructGPT paper), Erik Gafni, and Sasha Sheng, released it alongside a $40 million seed round led by DCVC. TypeSafe calls Jev the first \u0026ldquo;System One model\u0026rdquo;, after the fast, intuitive System 1 in Daniel Kahneman\u0026rsquo;s \u003cem\u003eThinking, Fast and Slow\u003c/em\u003e, and describes it as \u0026ldquo;a frontier-intelligence function call: unstructured state in, typed probabilistic decisions out\u0026rdquo; (\u003ca href=\"https://typesafe.ai/blog/introducing-system-one-models-and-jev\" data-external\u003eTypeSafe\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eYou send one \u003ccode\u003estate\u003c/code\u003e, as text or JSON, and a map of named questions. Each question has one of three types (\u003ca href=\"https://docs.typesafe.ai/api\" data-external\u003eTypeSafe API reference\u003c/a\u003e):\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eChoice\u003c/strong\u003e picks one of up to 255 labels you define and returns a probability for every label.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScore\u003c/strong\u003e rates the state on an ordered scale of 2 to 10 levels.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNoul\u003c/strong\u003e, short for Bernoulli, returns the probability that a statement is true.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEvery question is answered against the same state in one parallel pass, so adding questions barely changes the response time. Jev cannot write a sentence, and that is the point: there is no prose to parse, no output tokens to pay for, and no way for an answer to fall outside the labels you gave it.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev (System One model)\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eLanguage model (LLM)\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWhat comes back\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA typed answer with a probability for each option\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGenerated text, optionally JSON\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eCan it write, plan, or call tools\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eLatency\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e70 to 500 ms claimed; 358 to 611 ms in our tests\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUsually seconds for a short structured answer\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003ePrice per million input tokens\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.042, output free\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFrom about $0.05 to $10, plus billed output\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAnswer outside your options\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eImpossible by construction\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePossible without a strict schema\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWrong answer\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePossible, with a probability attached\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePossible, usually with no probability\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInput\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eText and JSON, 64k tokens per request\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eText, and often images and files\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eJev compared with a general-purpose language model\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eIn Kahneman\u0026rsquo;s terms, the language model is the slow, deliberate System 2 and Jev is the fast System 1. The analogy used to run the other way: in 2023, plain LLMs were the System 1 of the story. Simon Willison and Maggie Appleton suggest the plainer name \u0026ldquo;decision models\u0026rdquo; (\u003ca href=\"https://simonwillison.net/2026/Sep/21/jev/\" data-external\u003eSimon Willison\u003c/a\u003e), which is also the term our own code uses.\u003c/p\u003e\n\u003ch2 id=\"using-jev-for-ai-agent-memory\"\u003eUsing Jev for AI agent memory\u003ca class=\"heading-anchor\" href=\"#using-jev-for-ai-agent-memory\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct\u0026rsquo;s agent keeps a long-term memory: a graph of the people, companies, and projects in your work, with facts attached to each. After a conversation, it extracts what is worth keeping, and for every person or project it mentions it has to decide whether that is someone it already knows. That is entity resolution, and it is a textbook System One question: a judgment a knowledgeable person makes in a second, asked over and over, in the background, with nobody waiting on the answer. We are not the only ones who ended up here: a September paper, Jev-Mem, proposes putting a System One model in control of an agent\u0026rsquo;s memory (\u003ca href=\"https://arxiv.org/abs/2609.23986\" data-external\u003earXiv:2609.23986\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eMost cases never need a model. A platform ID, an exact alias, or a single exact-name match resolves in code. What is left is the ambiguous part, where several candidates share a name or the only matches are fuzzy or come from vector search.\u003c/p\u003e\n\u003cp\u003eBefore Jev, those leftovers went to a generative judge: a small open model, Llama 4 Scout on Workers AI, constrained by a JSON schema whose only allowed values were the candidate IDs or null. It was asked about each candidate set in turn, up to three sequential calls per entity and up to 64 entities per batch, and it answered with a label and nothing else.\u003c/p\u003e\n\u003cp\u003eNow one Jev call scores every candidate at once, whichever lookup found it, plus an explicit \u0026ldquo;none\u0026rdquo;, and entities resolve in parallel, eight at a time, so a 64-entity batch does not open 64 connections at once and run into TypeSafe\u0026rsquo;s per-account rate limit. If Jev does not answer within two seconds, or answers in a shape we did not ask for, resolution falls back to the old judge.\u003c/p\u003e\n\u003cp\u003eWho decides what matters, so here it is plainly:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCode\u003c/strong\u003e finds the candidates with vector, exact-name, and fuzzy-name lookups, enforces the threshold, and performs the merge.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJev\u003c/strong\u003e answers one question: which candidate, if any, is the same real entity, with a probability for each.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA language model\u003c/strong\u003e extracted the entity from the conversation earlier. It takes no part in the decision.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThat split is also our answer to the fair criticism that a decision model has no context. It does not need the whole conversation. Retrieval builds a small state, and Jev makes one bounded call on it. TypeSafe\u0026rsquo;s CEO has said \u0026ldquo;the hard part for coding is actually state engineering\u0026rdquo; (\u003ca href=\"https://news.ycombinator.com/item?id=49718867\" data-external\u003eHacker News\u003c/a\u003e). For an agent with its own computer and memory, most of that state already exists, structured, before the question is asked.\u003c/p\u003e\n\u003ch2 id=\"how-fast-is-jev-in-production\"\u003eHow fast is Jev in production?\u003ca class=\"heading-anchor\" href=\"#how-fast-is-jev-in-production\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eCall\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eInput\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eLatency\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eOne question\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAbout 400 tokens\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e358 to 611 ms\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eSeven questions on the same state\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAbout 711 tokens\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e377 to 544 ms\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eJev latency measured by Construct on September 20, 2026\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eBoth rows were measured against \u003ccode\u003ejev-1.13.0\u003c/code\u003e through Cloudflare AI Gateway. Repeating the seven-question call moved the probabilities by 0.02 at most, and p95 latency was about 0.6 seconds, so we gave the call a two-second budget before falling back. TypeSafe says its service runs on the US West Coast, so your numbers depend on where you call from: OpenRouter measured a 175 ms median (\u003ca href=\"https://openrouter.ai/blog/insights/jev-vs-claude-opus-5-classification/\" data-external\u003eOpenRouter\u003c/a\u003e), and a tester in Japan measured a 245 ms median (\u003ca href=\"https://towardsdatascience.com/jev-vs-llms-when-ai-moves-from-generation-to-decision-making/\" data-external\u003eTowards Data Science\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eThe second row is the useful one. Seven questions took no longer than one and used 1.8 times the input tokens, because Jev reads the state once and answers every question in parallel. Batch every judgment you have about the same state into one call instead of making several. TypeSafe\u0026rsquo;s own cookbook reports 13 batched questions running 12.2 times cheaper and 10 times faster than separate calls, with no change in answers (\u003ca href=\"https://docs.typesafe.ai/\" data-external\u003eTypeSafe docs\u003c/a\u003e).\u003c/p\u003e\n\u003ch2 id=\"how-we-picked-the-07-threshold\"\u003eHow we picked the 0.7 threshold\u003ca class=\"heading-anchor\" href=\"#how-we-picked-the-07-threshold\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eJev returns a probability for every candidate and for \u0026ldquo;none\u0026rdquo;. The real engineering question is where to draw the line. These are the probes we measured on September 20:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eNew mention\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eExisting candidates\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev\u0026rsquo;s answer\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eOutcome at 0.7\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Dr. Priya Shah\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePriya Shah\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePriya Shah, 0.71\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMerged\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Priya Shah\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePriya Shah\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePriya Shah, 0.76\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMerged\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;atlas migration\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAtlas Migration\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAtlas Migration, 0.82\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMerged\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Acme\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAcme Corporation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone, 0.66 (Acme Corporation 0.34)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eKept separate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Acme\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAcme Corporation, Acme Labs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone, 0.79\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eKept separate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Sam\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSam Altman, Samantha Reyes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone, 1.00\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eKept separate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Marcus Webb\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUnrelated people\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone, 1.00\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eKept separate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eJev\u0026rsquo;s answers on entity-resolution probes, and the outcome at a 0.7 merge threshold\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eIn this probe set, real matches landed between 0.71 and 0.82, and everything else put 0.34 or less on its best candidate. We set the merge threshold at 0.7, inside that gap.\u003c/p\u003e\n\u003cp\u003eTwo things decided where in the gap. First, the mistakes are not symmetrical. A wrong merge fuses two real entities and cannot be split again, while a missed merge leaves a duplicate that splits one entity\u0026rsquo;s facts across two records. Second, raising the bar to 0.8 would have refused exactly the alias cases, like \u0026ldquo;Dr. Priya Shah\u0026rdquo;, that we added the model to catch.\u003c/p\u003e\n\u003cp\u003eLook at the second row again. An identical name scored 0.76, not 0.99. We asked for \u0026ldquo;the same real entity\u0026rdquo;, \u0026ldquo;not merely a similar name\u0026rdquo;, and Jev priced in that a shared name is not proof of a shared identity. That is what you want from a decision about who someone is.\u003c/p\u003e\n\u003cp\u003eSeven probes are not a benchmark, and we do not present them as one. What transfers is the method: log the probabilities on your own traffic, look for the gap, and place the threshold by what each kind of mistake costs. Then pin the model version if your route lets you: TypeSafe\u0026rsquo;s docs say to pin a version\u0026rsquo;s ID once you have tuned confidence thresholds against it (\u003ca href=\"https://docs.typesafe.ai/models\" data-external\u003eTypeSafe docs\u003c/a\u003e). Cloudflare\u0026rsquo;s \u003ccode\u003etypesafe/jev\u003c/code\u003e route, the one we call, has no version field, so it answers with whatever TypeSafe serves (\u003ca href=\"https://developers.cloudflare.com/ai/models/typesafe/jev/\" data-external\u003eCloudflare\u003c/a\u003e). Every response names the model that answered, so record it next to each decision and treat a change as a reason to measure again.\u003c/p\u003e\n\u003ch3 id=\"use-the-probability-not-the-top-answer\"\u003eUse the probability, not the top answer\u003ca class=\"heading-anchor\" href=\"#use-the-probability-not-the-top-answer\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThe most important line in our integration is a helper that refuses to use Jev\u0026rsquo;s top answer unless its probability clears the threshold. Real matches do not always clear it. In a case recorded in our code outside that probe set, Jev picked the right candidate at 0.63 with \u0026ldquo;none\u0026rdquo; holding 0.37. Taking the top answer would have merged, and this time it would have been right. We still do not merge at 0.63. A rule that merges at 63% will also merge the wrong candidate whenever Jev is 63% sure of it, and a wrong merge costs far more than the duplicate this leaves behind.\u003c/p\u003e\n\u003cp\u003eIf you take the top label and discard the probability, you have thrown away the one thing you were paying for. This is the same arithmetic as in \u003ca href=\"https://construct.computer/blog/agent-verification-gap/\" data-external\u003eNobody merges an email\u003c/a\u003e: delegating pays when the cost of checking plus the expected cost of a mistake is lower than doing the work yourself, and the model only shows up in the probability of a mistake. A generative judge hands you an answer. A decision model hands you an answer and that probability, so you can spend human attention where it is low instead of rereading everything.\u003c/p\u003e\n\u003ch3 id=\"is-jevs-confidence-calibrated\"\u003eIs Jev\u0026rsquo;s confidence calibrated?\u003ca class=\"heading-anchor\" href=\"#is-jevs-confidence-calibrated\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003ePartly. TypeSafe trains for calibration and is explicit that calibration \u0026ldquo;does not guarantee that an individual answer is correct\u0026rdquo; (\u003ca href=\"https://docs.typesafe.ai/concepts/system-one\" data-external\u003eTypeSafe docs\u003c/a\u003e). Independent tests agree the probabilities rank answers well and disagree with each other about how literally to read them:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eOpenRouter found Jev \u0026ldquo;overestimated its own accuracy mid-range. Still, it ranks well.\u0026rdquo; At a confidence of 0.99 or more, which covered 58% of items, it was right 96.3% of the time (\u003ca href=\"https://openrouter.ai/blog/insights/jev-vs-claude-opus-5-classification/\" data-external\u003eOpenRouter\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003eA Towards Data Science test found answers in the 0.7 to 0.9 band right only 53% of the time, and answers at exactly 1.00 right 97.1% of the time (\u003ca href=\"https://towardsdatascience.com/jev-vs-llms-when-ai-moves-from-generation-to-decision-making/\" data-external\u003eTowards Data Science\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003ePrimeline measured calibration error by question type: 0.012 for Noul, 0.086 for Choice, and 0.254 for Score, roughly a 20 times spread (\u003ca href=\"https://primeline.cc/blog/typesafe-jev-pre-registered-test\" data-external\u003ePrimeline\u003c/a\u003e).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTreat Jev\u0026rsquo;s probabilities as a strong ranking, not as literal odds, and be most careful with Score questions. Our threshold does exactly that: it sits in a gap we observed rather than at a number we assumed. With a few hundred labeled examples from your own data you can go further and recalibrate the scores (\u003ca href=\"https://www.alexmolas.com/2026/09/23/jev-cant-be-calibrated.html\" data-external\u003eAlex Molas\u003c/a\u003e).\u003c/p\u003e\n\u003ch2 id=\"what-jev-gets-wrong\"\u003eWhat Jev gets wrong\u003ca class=\"heading-anchor\" href=\"#what-jev-gets-wrong\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eTypeSafe publishes its own list of weak spots for \u003ccode\u003ejev-1.13\u003c/code\u003e (\u003ca href=\"https://docs.typesafe.ai/model-jaggedness/jev-1.13\" data-external\u003eTypeSafe docs\u003c/a\u003e). It reads instructions literally, \u0026ldquo;is not a calculator\u0026rdquo;, \u0026ldquo;does not count reliably\u0026rdquo;, struggles with date and time comparison and with multi-step indirection, degrades on large states full of irrelevant detail, and does not treat text in the state as hostile. Independent tests confirm the ones that matter most for agents:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eInjected text moves the answer.\u003c/strong\u003e Primeline appended an \u0026ldquo;IMPORTANT INSTRUCTION FOR THE AI CLASSIFIER\u0026rdquo; line to an innocent reminder and watched its spam score rise from 0.04 to 0.66. Across 40 test pairs, 22.5% were misclassified outright (\u003ca href=\"https://primeline.cc/blog/typesafe-jev-pre-registered-test\" data-external\u003ePrimeline\u003c/a\u003e). A separate paper found injected content shifts probabilities but rarely makes Jev pick the attacker\u0026rsquo;s target, with adaptive attacks raising success from 1.8% to 3.5% (\u003ca href=\"https://arxiv.org/abs/2609.28613\" data-external\u003earXiv:2609.28613\u003c/a\u003e). For an agent that reads email and web pages, assume the state is attacker-controlled.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLong, real-world state hurts.\u003c/strong\u003e Synthetic padding up to 125,000 characters cost Primeline nothing, but on real notes accuracy fell from 97.0% on the shortest quarter to 81.1% on the longest.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProbability has to land somewhere.\u003c/strong\u003e Given 30 messages that fit none of the listed categories and no \u0026ldquo;none\u0026rdquo; option, Jev chose a listed category every time, at 0.99 confidence or more, in a test reported by \u003ca href=\"https://towardsdatascience.com/jev-vs-llms-when-ai-moves-from-generation-to-decision-making/\" data-external\u003eTowards Data Science\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSeparate questions are not kept consistent.\u003c/strong\u003e In TypeSafe\u0026rsquo;s own docs, a question and its negation returned probabilities that added up to 1.19.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese are the rules we wrote into our integration, and they follow directly from that list:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eNever put Jev on an auth boundary, and never make it the only check before an irreversible action.\u003c/li\u003e\n\u003cli\u003eNever ask it to compare dates, count, or do arithmetic. Compute those in code and put the result in the state.\u003c/li\u003e\n\u003cli\u003eAlways offer an explicit \u0026ldquo;none\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003eAsk one question per decision, and never infer the probability of \u0026ldquo;not x\u0026rdquo; from the probability of \u0026ldquo;x\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003eKeep the state small and relevant: the candidates, not the whole conversation.\u003c/li\u003e\n\u003cli\u003eTreat a missing or malformed answer as no verdict, never as \u0026ldquo;no\u0026rdquo;, and keep the old path as a fallback. That includes a label you never offered: count it as no answer, not as \u0026ldquo;none\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003eBatch every question about the same state into one call.\u003c/li\u003e\n\u003cli\u003eRecord which model version answered, with the probability and what you did with it, so the threshold can be checked again later.\u003c/li\u003e\n\u003cli\u003ePut anything a third party wrote in fields marked as untrusted, and tell every question to treat those fields as evidence, never as instructions.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"where-jev-fits-in-an-ai-agent-system-1-and-system-2\"\u003eWhere Jev fits in an AI agent: System 1 and System 2\u003ca class=\"heading-anchor\" href=\"#where-jev-fits-in-an-ai-agent-system-1-and-system-2\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAn AI employee makes two kinds of decisions. The big ones, what to do and how, belong to a language model that can plan, write, and use tools. The small ones happen dozens of times per task, and most of them are the shape Jev was built for: a bounded question over a small state, where a fast answer with a probability is worth more than a slow paragraph.\u003c/p\u003e\n\u003cp\u003eAnthony Maio\u0026rsquo;s division of labor is the cleanest statement of the pattern we have seen: \u0026ldquo;A generative model drafts, plans, or explains. Jev supplies bounded semantic judgments. Code handles state, arithmetic, policy, permissions, and side effects. Humans take the ambiguous or high-risk cases\u0026rdquo; (\u003ca href=\"https://anthonymaio.substack.com/p/jev-the-language-model-that-wont\" data-external\u003eAnthony Maio\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eThis is how we rate the decision points in an always-on agent\u0026rsquo;s loop:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDecision\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eJev question\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eFit\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhat to design around\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eIs this person or project one the agent already knows?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eChoice, with \u0026ldquo;none\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIn production at Construct\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA wrong merge is hard to undo, so merge only above a measured threshold\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eIs the task done, or should the agent keep going?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNoul\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGood\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe state includes tool output, which is untrusted, so stop when unsure\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eDoes this inbound email need the agent at all?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eChoice: reply, file, or ignore\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGood for skipping and labeling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEmail is attacker-controlled, so the answer must never send anything\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eIs this thread message addressed to the agent?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNoul\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGood\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLow stakes: a miss costs a slower reply\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eIs there anything new since the last scheduled run?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNoul over a change list built in code\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePromising\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJev is weak at dates, so work out what changed before you ask\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWhich tool or skill fits this request?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eChoice, up to 255 labels\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGood as a suggestion\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLet the planning model keep the final say\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eCan this action run without asking the user?\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNoul\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOnly as one layer\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNever the only gate on something irreversible\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eDecisions in an AI employee\u0026rsquo;s loop, and how well each fits a decision model\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eTwo patterns come out of that table.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe saving is the turns you do not run.\u003c/strong\u003e Comparing Jev\u0026rsquo;s price with a cheap language model\u0026rsquo;s misses where the money goes. When an email arrives or a schedule fires, the expensive part is the full agent turn that follows. A half-second check that says \u0026ldquo;this is a newsletter\u0026rdquo; or \u0026ldquo;nothing changed since the last run\u0026rdquo; lets the agent skip that turn entirely. TypeSafe\u0026rsquo;s skill-suggestion cookbook shows a similar effect inside a turn: with a Jev suggestion, an agent built on Claude Haiku 4.5 loaded the wrong skill 7.3% of the time instead of 16.8% (\u003ca href=\"https://docs.typesafe.ai/cookbooks/skill_suggestion\" data-external\u003eTypeSafe docs\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEscalate up, never sideways.\u003c/strong\u003e The obvious move is to send low-confidence decisions to a bigger model, and it works only if the fallback is actually better at the hard cases. OpenRouter sent Jev\u0026rsquo;s answers below 0.90 confidence to Claude Opus 5 and got within 0.4 points of Opus alone, with 76% of traffic never touching Opus, an upper bound because the cutoff was tuned on the test set (\u003ca href=\"https://openrouter.ai/blog/insights/jev-vs-claude-opus-5-classification/\" data-external\u003eOpenRouter\u003c/a\u003e). A Towards Data Science test sent them to a weaker local model instead, and every cascade did worse than Jev alone: one \u0026ldquo;fixed 84 mistakes, but introduced 211 new ones\u0026rdquo; (\u003ca href=\"https://towardsdatascience.com/jev-vs-llms-when-ai-moves-from-generation-to-decision-making/\" data-external\u003eTowards Data Science\u003c/a\u003e). Knowing which cases are hard is not the same as having something that can solve them. For irreversible actions, the right place to escalate is usually a person.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eRead next: \u003ca href=\"https://construct.computer/blog/agent-task-half-life/\" data-external\u003eYour agent has a half-life\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"what-does-jev-cost-compared-with-an-llm\"\u003eWhat does Jev cost compared with an LLM?\u003ca class=\"heading-anchor\" href=\"#what-does-jev-cost-compared-with-an-llm\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAt list prices, for a decision with about 400 tokens of input, and assuming a 20-token JSON answer from the models that generate one:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eModel\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePrice per million tokens, input / output\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCost per 1,000 decisions\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eJev 1.13\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.042 / free\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.017\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eLlama 4 Scout on Workers AI (our old judge)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.27 / $0.85\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.125\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eClaude Haiku 4.5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$1 / $5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$0.50\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eClaude Sonnet 5.5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$2 / $10\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e$1.00\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eList-price cost per 1,000 decisions of about 400 input tokens, September 2026\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eTwo notes make the comparison fairer. Prompt caching cuts repeated input on Claude models to about a tenth of the price, but Haiku 4.5 only caches prompts of 4,096 tokens or more, so a short decision prompt gets no discount. Jev has no prompt caching at all, and Primeline saw identical requests billed in full every time, which barely matters at $0.042 per million tokens. Our old judge also needed up to three calls per entity, so its real cost was up to three times its row.\u003c/p\u003e\n\u003cp\u003eThe honest conclusion is less dramatic than the launch claim of \u0026ldquo;444.6x cheaper\u0026rdquo;. Independent tests put Jev at roughly 8 to 20 times cheaper per call than Haiku 4.5 and 10 to 20 times faster than strong language models on like-for-like classification, a few accuracy points behind the best of them (\u003ca href=\"https://primeline.cc/blog/typesafe-jev-pre-registered-test\" data-external\u003ePrimeline\u003c/a\u003e; \u003ca href=\"https://openrouter.ai/blog/insights/jev-vs-claude-opus-5-classification/\" data-external\u003eOpenRouter\u003c/a\u003e). For one founder\u0026rsquo;s agent making a few hundred small decisions a day, the difference is cents a month either way. Price starts to matter at fleet scale or in tight loops, and the bigger saving is still the agent turns a cheap decision lets you skip.\u003c/p\u003e\n\u003cp\u003eFor us, cost was not the reason to switch. One call replaced up to three sequential ones, \u0026ldquo;none\u0026rdquo; became an explicit, scored answer, and we got a probability to put a threshold on.\u003c/p\u003e\n\u003ch2 id=\"how-we-are-adding-more-jev-decisions\"\u003eHow we are adding more Jev decisions\u003ca class=\"heading-anchor\" href=\"#how-we-are-adding-more-jev-decisions\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEntity matching was the first decision. Mapping every other place our agent makes a small judgment turned up about thirty more candidates, from \u0026ldquo;does this email need the agent?\u0026rdquo; to \u0026ldquo;did this scheduled check find anything new?\u0026rdquo;. Before moving any of them, we built one small runtime that every decision goes through. The lesson of the first week was that a decision you cannot see is a decision you cannot trust.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEvery decision ships switched off.\u003c/strong\u003e A flag moves each one from off to shadow to live on its own, with its own threshold.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eShadow before live.\u003c/strong\u003e In shadow the old path still decides, and Jev\u0026rsquo;s answer is recorded beside it. The old path\u0026rsquo;s answer becomes a free label, so the threshold comes from real traffic rather than a handful of probes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecords carry numbers, not text.\u003c/strong\u003e We keep the decision name, the label, the probability, what the product did, and which model answered, never the text Jev saw. Samples are deleted after 90 days.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThe model version is recorded, not assumed.\u003c/strong\u003e Our route cannot pin a version, so each record names the version that answered, and a change raises an alarm. New decisions drop back to shadow until they are measured again.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA failing provider degrades to the old behaviour.\u003c/strong\u003e A circuit breaker stops calling Jev after repeated failures, and a rate gate keeps us well under TypeSafe\u0026rsquo;s published limit, 80 requests a second when we last checked (\u003ca href=\"https://docs.typesafe.ai/models\" data-external\u003eTypeSafe docs\u003c/a\u003e). Every decision has a fallback, so an outage looks like the product before Jev.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThird-party text is labelled as such.\u003c/strong\u003e Email bodies, web pages and other people\u0026rsquo;s messages go into fields named as untrusted, and every question says those fields are evidence, not instructions. In one small practitioner test, a single sentence like that cut targeted injection success to 1.3% (\u003ca href=\"https://github.com/Iskandeur/system1-system2\" data-external\u003eIskandeur\u003c/a\u003e). It is a mitigation, not a boundary, so a decision that reads untrusted text may only lower a priority or skip work, never grant anything.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDecisions that read your conversations or email wait for the paperwork.\u003c/strong\u003e Sending your text to another provider is a privacy decision before it is an engineering one. TypeSafe is on our \u003ca href=\"https://construct.computer/sub-processors/\" data-external\u003esub-processors list\u003c/a\u003e, and the decisions that would send conversation or email text stay off until the contract review is done.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe mapping also found plain bugs that had nothing to do with Jev. One was a keyword table that guessed which app a request needed: any request containing the letter x, such as \u0026ldquo;export as csv\u0026rdquo;, read as a request for Twitter, and \u0026ldquo;linear regression\u0026rdquo; read as a request for Linear. A keyword list is how you make a System One judgment without a System One model, and that is what it looks like when it fails.\u003c/p\u003e\n\u003ch2 id=\"how-to-try-jev\"\u003eHow to try Jev\u003ca class=\"heading-anchor\" href=\"#how-to-try-jev\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDirect:\u003c/strong\u003e sign up at \u003ca href=\"https://typesafe.ai\" data-external\u003etypesafe.ai\u003c/a\u003e. TypeSafe paused new signups on September 22 because of demand, and its CEO said on September 27 that signups had reopened, without free credits for new accounts (\u003ca href=\"https://x.com/CompleteSkeptic/status/2104338649999626397\" data-external\u003eDiogo Almeida on X\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThrough a gateway, without a TypeSafe account:\u003c/strong\u003e OpenRouter (\u003ccode\u003etypesafe/jev-1.13\u003c/code\u003e), Vercel AI Gateway (\u003ccode\u003etypesafe-ai/jev\u003c/code\u003e), or Cloudflare Workers AI (\u003ccode\u003etypesafe/jev\u003c/code\u003e). We call it through Cloudflare Workers AI and AI Gateway.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOn Cloudflare, check the key alias:\u003c/strong\u003e the Workers AI binding only uses a provider key stored under the \u003ccode\u003edefault\u003c/code\u003e alias. With any other alias, calls quietly fall through to Cloudflare\u0026rsquo;s Unified Billing, which is capped at 200 requests a minute per gateway (\u003ca href=\"https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/\" data-external\u003eCloudflare\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSDKs:\u003c/strong\u003e \u003ccode\u003etypesafe-sdk\u003c/code\u003e for Python and \u003ccode\u003e@typesafe-ai/sdk\u003c/code\u003e for JavaScript and TypeScript (\u003ca href=\"https://docs.typesafe.ai/introduction/quickstart\" data-external\u003eTypeSafe quick start\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePrice:\u003c/strong\u003e $0.042 per million input tokens. Output tokens are counted but not billed. Several unofficial lookalike sites resell Jev access at several times that price, so check the price on typesafe.ai or your gateway before you pay.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVersion:\u003c/strong\u003e \u003ccode\u003ejev-latest\u003c/code\u003e points at \u003ccode\u003ejev-1.13.0\u003c/code\u003e today. If you tune thresholds, pin the version where your route allows it, and record the served version where it does not.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eStart with one decision that is frequent, runs in the background, is not an auth boundary, and already has a fallback. Log the probabilities for a week before you let them act on anything.\u003c/p\u003e\n\u003ch2 id=\"what-most-jev-coverage-gets-wrong\"\u003eWhat most Jev coverage gets wrong\u003ca class=\"heading-anchor\" href=\"#what-most-jev-coverage-gets-wrong\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;Jev never hallucinates.\u0026rdquo;\u003c/strong\u003e It cannot return a value outside your schema. It can return the wrong valid one, and TypeSafe\u0026rsquo;s own FAQ says it \u0026ldquo;can choose the wrong one\u0026rdquo; (\u003ca href=\"https://typesafe.ai\" data-external\u003eTypeSafe\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;Jev is 67.8% accurate.\u0026rdquo;\u003c/strong\u003e That figure is agreement with the averaged answers of GPT-6 Astra and Claude Fable 5.1 on TypeSafe\u0026rsquo;s own four workflow evals, not accuracy against verified labels (\u003ca href=\"https://evals.typesafe.ai/\" data-external\u003eTypeSafe evals\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;193.6x faster and 444.6x cheaper.\u0026rdquo;\u003c/strong\u003e On TypeSafe\u0026rsquo;s eval chart, those ratios line up with the slowest model, Claude Sonnet 5 at 78.1 seconds per case, and the most expensive, Claude Opus 5 at $0.1761 per case. Against GPT-5.6 Terra, which matched Jev\u0026rsquo;s agreement score, the same chart works out to about 25 times faster and 76 times cheaper.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;Jev has a 32k context window.\u0026rdquo;\u003c/strong\u003e It takes 64k tokens per request, of which 32k covers the state plus the longest question (\u003ca href=\"https://docs.typesafe.ai/models\" data-external\u003eTypeSafe docs\u003c/a\u003e). Cloudflare\u0026rsquo;s model page lists 32,000 tokens for its route, so check the limit on the route you actually call.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;It is deterministic.\u0026rdquo;\u003c/strong\u003e TypeSafe designs for consistency rather than determinism. Our repeated calls moved by 0.02 at most, which is consistent, not identical.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;Just send the uncertain ones to a bigger model.\u0026rdquo;\u003c/strong\u003e Only if that model is better at the hard cases, as the escalation results above show.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"where-construct-fits\"\u003eWhere Construct fits\u003ca class=\"heading-anchor\" href=\"#where-construct-fits\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct is an AI employee with its own computer: a workspace filesystem, long-term memory, a schedule, email, and workflows you can read before you run them. Jev runs in one place today, deciding whether a person or project your agent just learned about is one it already knows. Planning and writing run on general-purpose language models.\u003c/p\u003e\n\u003cp\u003eMemory in Construct is inspectable and correctable, so if a merge pulls the wrong facts together, you can see which ones and correct or forget them. \u003ca href=\"https://construct.computer/blog/ai-agent-memory/\" data-external\u003eAI agent memory\u003c/a\u003e covers how that works. Two honest boundaries: the \u0026ldquo;is this task done?\u0026rdquo; check and email triage are written as Jev decisions, but both stay switched off until shadow data shows they beat what they would replace, and Construct does not currently insert a mandatory approval gate before every external side effect, so steps that send mail or move money still need supervision before they run unattended.\u003c/p\u003e\n\u003cp\u003eIf the reliability argument is new to you, start with \u003ca href=\"https://construct.computer/blog/agent-task-half-life/\" data-external\u003eyour agent has a half-life\u003c/a\u003e and \u003ca href=\"https://construct.computer/blog/agent-verification-gap/\" data-external\u003enobody merges an email\u003c/a\u003e. For the infrastructure underneath, read \u003ca href=\"https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/\" data-external\u003ehow our agents get computers we mostly do not pay for\u003c/a\u003e, and if the category itself is new, begin with \u003ca href=\"https://construct.computer/blog/what-is-an-ai-employee/\" data-external\u003ewhat is an AI employee\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/jev-ai-agents/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-09-30T00:00:00Z","external_url":"https://construct.computer/blog/jev-ai-agents/","id":"https://ankush.one/blogs/jev-ai-agents/","image":"https://ankush.one/media/construct/jev-ai-agents.jpg","summary":"We shipped TypeSafe's Jev into our AI employee's memory five days after launch: measured latency, how we set a 0.7 threshold, where Jev fails, and what it costs.","tags":["blogs","ai-agents","machine-learning","construct"],"title":"We put Jev inside our AI employee","url":"https://ankush.one/blogs/jev-ai-agents/"},{"content_html":"\u003cp\u003eIt is 5:40 on a Friday and the agent has just written nine client updates in four minutes, sitting there quietly pleased with itself. Eight of them are fine. One of them has told a client something that is not true, and because nothing in the pipeline can tell you which one, you read all nine. Forty minutes later you send eight, rewrite one, and work out that the time you saved on the writing came straight back out of your evening. The agent did not save you thirty six minutes. It moved them, out of writing, which you are decent at, and into checking, which nobody is good at and nobody enjoys.\u003c/p\u003e\n\u003cp\u003eEvery tool you actually work in has an undo. Git has revert, your editor has a keystroke, staging has the comfort of breaking somewhere that does not count, and Gmail gives you thirty seconds. Nobody merges an email: there is no diff to skim, no test suite to run it past, no staging inbox to try it in, and no way back once it is sitting in somebody\u0026rsquo;s inbox, so the entire cost of being wrong lands on one person reading carefully. That gap, and not the model, is why your agent is trusted with code and kept well away from the account manager\u0026rsquo;s inbox. It is also why most agent pilots quietly stall.\u003c/p\u003e\n\u003ch2 id=\"the-bottleneck-moved-and-the-tooling-stayed-put\"\u003eThe bottleneck moved and the tooling stayed put\u003ca class=\"heading-anchor\" href=\"#the-bottleneck-moved-and-the-tooling-stayed-put\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eSoftware is the one place where a lot of people pointed agents at real work and then measured what happened. The measurements are not subtle.\u003c/p\u003e\n\u003cp\u003eFaros AI compared two years of telemetry from 22,000 developers across 4,000 teams, looking at each organization at its lowest and highest AI adoption. Output went up. So did everything that output costs you.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/agent-verification-gap/acceleration-whiplash.svg\" alt=\"Horizontal bar chart of percentage change between each organization’s lowest and highest AI adoption. Tasks completed per developer up 33.7 percent and epics completed up 66 percent, against bugs per developer up 54 percent, median time to first review up 156.6 percent, incidents per pull request up 242.7 percent, median review time up 441.5 percent, and code churn up 861 percent.\" title=\"The two shortest bars are the benefit. Everything longer is what the benefit cost. From Faros AI\u0026#39;s 2026 report, The Acceleration Whiplash.\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eThe two shortest bars are the benefit. Everything longer is what the benefit cost. From Faros AI\u0026#39;s 2026 report, The Acceleration Whiplash.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eA third more tasks per developer, and five times the median review time (\u003ca href=\"https://www.faros.ai/research/ai-acceleration-whiplash\" data-external\u003eFaros AI, 2026\u003c/a\u003e). That is not a tooling problem or a model problem. That is a system that got very good at producing work and no better at absorbing it.\u003c/p\u003e\n\u003cp\u003eLinearB looked at the same squeeze from the other end, across 8.1 million pull requests from 4,800 teams in 42 countries, and found the detail that gives the game away.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/agent-verification-gap/pickup-vs-review.svg\" alt=\"Grouped bar chart in hours per pull request. Waiting for a reviewer to start: 3.3 hours without AI against more than 16 hours for AI-assisted work. Actually being reviewed: 4.2 hours without AI against 3.2 hours for AI-assisted work.\" title=\"AI-assisted work waits more than five times longer for someone to start reviewing it, then takes slightly less time to review than human work once they do. From LinearB\u0026#39;s 2026 Engineering Benchmarks Report.\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eAI-assisted work waits more than five times longer for someone to start reviewing it, then takes slightly less time to review than human work once they do. From LinearB\u0026#39;s 2026 Engineering Benchmarks Report.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eRead those two bars again. Once a reviewer actually starts, the AI-assisted change is \u003cem\u003efaster\u003c/em\u003e to get through than the human one: 194 minutes against 252 (\u003ca href=\"https://linearb.io/blog/8-million-prs-engineering-productivity\" data-external\u003eLinearB, 2026\u003c/a\u003e). The delay is not the reading. The delay is everything before the reading: the queue, the size of the thing, the sinking feeling, the decision to start.\u003c/p\u003e\n\u003cp\u003eThe expensive part of verification was never the mechanical act of checking. It is the willingness to be the person who signs off.\u003c/p\u003e\n\u003ch2 id=\"why-software-absorbed-the-flood-anyway\"\u003eWhy software absorbed the flood anyway\u003ca class=\"heading-anchor\" href=\"#why-software-absorbed-the-flood-anyway\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEngineering took that hit and mostly kept moving, and it is worth being precise about why, because the reason is not talent or process maturity. It is that code is the most verifiable artifact a company produces, and it has been getting more verifiable for forty years.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/agent-verification-gap/verification-stack.svg\" alt=\"Two columns comparing five verification questions. Shipping code answers all five: git diff shows what changed, staging tries it without consequences, CI and tests have a machine check it, a pull request records who approved, and git revert undoes it. Everything else agents do answers none of them: you read it all again, there is no staging inbox, nothing runs, approval is a chat message at best, and sending is final.\" title=\"Every finished piece of work raises the same five questions. Software spent four decades building cheap answers to all of them. Nobody built the other column.\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eEvery finished piece of work raises the same five questions. Software spent four decades building cheap answers to all of them. Nobody built the other column.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eA diff is a strange and wonderful thing. It turns \u0026ldquo;read this 400 line change\u0026rdquo; into \u0026ldquo;read these 40 lines that differ\u0026rdquo;, which is a reduction of an order of magnitude in what a human has to hold in their head. Tests turn \u0026ldquo;is it correct\u0026rdquo; into a boolean somebody else already thought about. Staging turns \u0026ldquo;will this break\u0026rdquo; into \u0026ldquo;it did not break over there\u0026rdquo;. A pull request turns approval into a record with a name on it. Revert turns a mistake into an inconvenience.\u003c/p\u003e\n\u003cp\u003eNone of that exists for the work most of a company actually does.\u003c/p\u003e\n\u003cp\u003eThere is no diff for an email. There is no staging environment for a CRM. No test suite asserts that the invoice you generated has the right billing period on it, and no button unsends the reply to a customer.\u003c/p\u003e\n\u003cp\u003eSo when an agent produces that kind of work, the verification cost lands entirely on a human with no instruments, and it lands per unit of output. Double the output, double the checking. That is the whole story of why the demo felt like magic and the rollout felt like a second job.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eRead next: \u003ca href=\"https://construct.computer/blog/agent-task-half-life/\" data-external\u003eYour agent has a half-life\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"the-check-has-to-be-cheaper-than-the-doing\"\u003eThe check has to be cheaper than the doing\u003ca class=\"heading-anchor\" href=\"#the-check-has-to-be-cheaper-than-the-doing\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eHere is the arithmetic nobody puts in the pitch deck.\u003c/p\u003e\n\u003cp\u003eLet \u003cstrong\u003eD\u003c/strong\u003e be what it costs you to do the job yourself. Let \u003cstrong\u003eC\u003c/strong\u003e be what it costs to check the agent\u0026rsquo;s version. Let \u003cstrong\u003ep\u003c/strong\u003e be the chance it is wrong in a way you would care about, and \u003cstrong\u003eR\u003c/strong\u003e the cost when a wrong one gets through: the apology, the refund, the client who stops replying.\u003c/p\u003e\n\u003cp\u003eDelegating pays when:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eC + p x R  \u0026lt;  D\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe model only appears inside \u003cstrong\u003ep\u003c/strong\u003e. Everything else in that inequality is a property of your tooling, and it is the part every agent roadmap ignores.\u003c/p\u003e\n\u003cp\u003eRun it on the Friday nine. Writing one yourself is about six minutes, so D is 54 minutes. Reading nine drafts closely enough to put your name on them is about four minutes each, so C is 36, which is near enough the forty you actually spent. The saving is whatever is left, and it is thin. Note where it went, too: one draft was wrong, and you paid to check nine, because nothing in the pipeline could tell you which. Now make the drafts slightly harder to trust, so you reread the source thread for each one, and C passes D. The agent is now a net loss at 100% quality, because quality was never what you were paying for.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eWork the agent finished\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhat you check it against\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCost to check\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCost if a bad one gets through\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eA code change\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA diff, a test run, a staging deploy\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMinutes, tool-assisted\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA revert\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eNine client emails\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe nine source threads, from memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNearly the cost of writing them\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA relationship\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e200 CRM records updated\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNothing. You spot check twelve and hope\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUnbounded, so people skip it\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA quarter of bad data\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cem\u003eWhat checking costs across three kinds of agent work\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eNotice what happens in the bottom row. When C is unbounded, people do not pay it. They either check nothing and silently accept \u003cstrong\u003ep x R\u003c/strong\u003e, or they stop using the agent for that work. Both look like \u0026ldquo;the agent did not work out\u0026rdquo;. Neither is about the agent.\u003c/p\u003e\n\u003cp\u003eThis is the same shape as \u003ca href=\"https://construct.computer/blog/agent-task-half-life/\" data-external\u003ethe half-life problem\u003c/a\u003e: a per-step property that looks fine up close and decides everything at scale. There, it was reliability compounding over a long run. Here, it is verification cost compounding over volume. Both get fixed by changing the structure of the run, not the intelligence inside it.\u003c/p\u003e\n\u003ch2 id=\"nobody-is-measuring-this-including-you\"\u003eNobody is measuring this, including you\u003ca class=\"heading-anchor\" href=\"#nobody-is-measuring-this-including-you\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe obvious objection is that if verification were really eating the gains, people would notice.\u003c/p\u003e\n\u003cp\u003eThey do not notice. That is the best-documented part of this whole argument.\u003c/p\u003e\n\u003cp\u003eMETR ran a randomized controlled trial with 16 experienced open-source developers on 246 real tasks in repositories they already maintained. Before starting, the developers forecast that AI tools would make them 24% faster. Afterwards, having done the work, they believed they had been 20% faster. Measured, they took 19% longer.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://ankush.one/media/construct/agent-verification-gap/metr-perception-gap.svg\" alt=\"Diverging bar chart. Before the study, developers forecast being 24 percent faster with AI. After the study they believed they had been 20 percent faster. The measured result was 19 percent slower.\" title=\"A 39 point gap between what it felt like and what happened, from people with hundreds of hours of prompting experience. From METR\u0026#39;s July 2025 randomized controlled trial.\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption\u003eA 39 point gap between what it felt like and what happened, from people with hundreds of hours of prompting experience. From METR\u0026#39;s July 2025 randomized controlled trial.\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eThese were not novices fumbling with a new toy. Nearly all of them had dozens to hundreds of hours of prior experience prompting models (\u003ca href=\"https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/\" data-external\u003eMETR, July 2025\u003c/a\u003e). The generation felt fast, because it was. The review, the correction, the second pass to understand what had been written for them: that time was real and it did not register.\u003c/p\u003e\n\u003cp\u003eNow scale that error up to a company. MIT\u0026rsquo;s Project NANDA, working from more than 300 publicly disclosed deployments, 52 interviews, and 153 survey responses, put the share of enterprise GenAI pilots with no measurable impact on the P\u0026amp;L at roughly 95% (\u003ca href=\"https://mlq.ai/media/quarterly_decks/v0.1_State_of_AI_in_Business_2025_Report.pdf\" data-external\u003eMIT NANDA, 2025\u003c/a\u003e). People have argued about that methodology ever since. Nobody has argued that the number is small. Gartner expects more than 40% of agentic projects to be canceled by the end of 2027, citing costs and unclear value (\u003ca href=\"https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027\" data-external\u003eGartner, June 2025\u003c/a\u003e). DORA, surveying nearly 5,000 professionals, landed on the phrase that ties it together: AI is an amplifier (\u003ca href=\"https://dora.dev/dora-report-2025/\" data-external\u003eDORA, 2025\u003c/a\u003e). It makes a system with good feedback loops better and a system without them worse, faster.\u003c/p\u003e\n\u003cp\u003eMost business processes have no feedback loops at all. They have a person who would notice eventually.\u003c/p\u003e\n\u003cp\u003eAgents rarely fail an evaluation. They fail an audit nobody ran.\u003c/p\u003e\n\u003ch2 id=\"five-things-that-make-agent-work-checkable\"\u003eFive things that make agent work checkable\u003ca class=\"heading-anchor\" href=\"#five-things-that-make-agent-work-checkable\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eIf verification cost is the constraint, then the interesting engineering is not in the agent loop. It is in everything the agent leaves behind. Five properties do most of the work, and none of them require a better model.\u003c/p\u003e\n\u003ch3 id=\"artifacts-not-transcripts\"\u003eArtifacts, not transcripts\u003ca class=\"heading-anchor\" href=\"#artifacts-not-transcripts\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eA chat transcript is the worst possible verification surface: linear, long, and it buries the output inside the reasoning. A file is the best one. You can open it, skim it, diff it against last month\u0026rsquo;s, hand it to someone else, and know at a glance whether the thing exists yet.\u003c/p\u003e\n\u003cp\u003eThe rule is that finished work should be an object you can point at, not a passage you have to read to the end of.\u003c/p\u003e\n\u003ch3 id=\"a-draft-state-before-anything-irreversible\"\u003eA draft state before anything irreversible\u003ca class=\"heading-anchor\" href=\"#a-draft-state-before-anything-irreversible\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThis is the missing diff, and it is mostly a product decision rather than a research problem. Generate the email, do not send it. Compute the 200 CRM updates, write them as a table first. Produce the invoice as a file before it goes to the customer.\u003c/p\u003e\n\u003cp\u003eA draft turns an irreversible action into a reviewable artifact, which moves it out of the unbounded row of that table and into the cheap one. The work is identical. The verification cost is not.\u003c/p\u003e\n\u003ch3 id=\"provenance-on-anything-the-agent-believes\"\u003eProvenance on anything the agent believes\u003ca class=\"heading-anchor\" href=\"#provenance-on-anything-the-agent-believes\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eHalf of checking is not \u0026ldquo;is this right\u0026rdquo;, it is \u0026ldquo;where did this come from\u0026rdquo;. An agent that says the client\u0026rsquo;s renewal is in March is unverifiable. An agent that says the renewal is in March, learned from the contract PDF you uploaded on 14 August, can be checked in four seconds.\u003c/p\u003e\n\u003cp\u003eThat is why \u003ca href=\"https://construct.computer/blog/ai-agent-memory/\" data-external\u003eagent memory needs provenance and correction\u003c/a\u003e rather than a vector blob. Memory you cannot audit is a claim you have to re-derive, which is verification cost with extra steps.\u003c/p\u003e\n\u003ch3 id=\"a-bounded-record-of-what-it-touched\"\u003eA bounded record of what it touched\u003ca class=\"heading-anchor\" href=\"#a-bounded-record-of-what-it-touched\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eNot the full trace. Nobody reads the full trace. What you need is the ledger: what did it touch, when, and why. It is the difference between a stack trace and a receipt.\u003c/p\u003e\n\u003cp\u003eThe test is whether someone can answer \u0026ldquo;what did it actually do yesterday\u0026rdquo; in under a minute without reading a single model output.\u003c/p\u003e\n\u003ch3 id=\"reversibility-where-it-exists-a-human-where-it-does-not\"\u003eReversibility where it exists, a human where it does not\u003ca class=\"heading-anchor\" href=\"#reversibility-where-it-exists-a-human-where-it-does-not\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eSome actions can be undone, and those should be cheap to approve. Some cannot, and pretending otherwise is how teams get burned. Sending mail, moving money, posting publicly, deleting records: for those the honest answer is that a person confirms, and the product\u0026rsquo;s job is to make that confirmation a two second glance rather than an investigation.\u003c/p\u003e\n\u003ch2 id=\"how-construct-is-built-around-the-check\"\u003eHow Construct is built around the check\u003ca class=\"heading-anchor\" href=\"#how-construct-is-built-around-the-check\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eWe build \u003ca href=\"https://construct.computer/blog/ai-employee/\" data-external\u003ean AI employee\u003c/a\u003e with a real computer, so this is the problem we live in. A few things follow directly from the argument above.\u003c/p\u003e\n\u003cp\u003eWork lands in a workspace filesystem as files, not as messages in a thread. That is the artifacts rule, and it is also why a run that dies partway through leaves usable output behind rather than a transcript to excavate.\u003c/p\u003e\n\u003cp\u003eEvery action goes into Activity: what it affected, when it ran, and why. That is the receipt, not the stack trace, and it exists so the answer to \u0026ldquo;what did it do overnight\u0026rdquo; is a scroll rather than a project. Alongside it you get the files it produced, the tool records, and a read-only terminal transcript when you want to go deeper.\u003c/p\u003e\n\u003cp\u003eMemory is inspectable and correctable, with provenance and temporal context, so a claim can be traced to where it came from and fixed in place instead of argued with. Repeatable work becomes a saved workflow you can read before you schedule it. Current workflows are linear, which is a real limitation and also the reason a workflow is something you can hold in your head.\u003c/p\u003e\n\u003cp\u003eAnd the part we have not finished: Construct does not currently insert a mandatory approval gate before every external side effect. Drafts, interruption mid-run, and Activity give you a lot, but if a step sends a customer email or moves money, that step still needs supervision before you let it run unattended. We would rather say that plainly than let someone discover it on a Sunday.\u003c/p\u003e\n\u003ch2 id=\"the-next-10x-is-not-in-the-model\"\u003eThe next 10x is not in the model\u003ca class=\"heading-anchor\" href=\"#the-next-10x-is-not-in-the-model\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEvery agent product is currently competing on the part that got commoditized. Producing plausible work is close to free and getting freer.\u003c/p\u003e\n\u003cp\u003eThe scarce thing is a human\u0026rsquo;s willingness to sign their name under it, and that is bought with diffs, drafts, receipts, provenance, and undo. Software has had those for forty years and, funnily enough, software is the one place agents are actually working.\u003c/p\u003e\n\u003cp\u003eThe rest of the company is waiting for someone to build them the other column.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/agent-verification-gap/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-09-22T00:00:00Z","external_url":"https://construct.computer/blog/agent-verification-gap/","id":"https://ankush.one/blogs/agent-verification-gap/","image":"https://ankush.one/media/construct/agent-verification-gap.jpg","summary":"Agents made work cheap to produce and no cheaper to check. Why software absorbed the flood, why the rest of the business did not, and the five things that make agent work checkable.","tags":["blogs","ai-agents","construct"],"title":"Nobody merges an email","url":"https://ankush.one/blogs/agent-verification-gap/"},{"content_html":"\u003cp\u003eValve named a game \u003cem\u003eHalf-Life\u003c/em\u003e after the physics joke hiding in plain sight: how long something lasts before half of it is gone. Your agent has one of those. It just does not get a hazmat suit or a crowbar out of the deal.\u003c/p\u003e\n\u003cp\u003eTake an agent that gets 95% of its individual steps right. That is a good agent. Now give it a ten-step job.\u003c/p\u003e\n\u003cp\u003eIt finishes six times out of ten. The other four times, it fails halfway. Literally. A half-life.\u003c/p\u003e\n\u003cp\u003eNothing broke. No step regressed. 0.95 to the tenth power is 0.60, and that is the whole story. Reliability that looks excellent per step is mediocre per job, and it gets worse in a way most people do not feel until they schedule something and walk away.\u003c/p\u003e\n\u003cp\u003eIf you have been asking why your agent keeps failing halfway through a multi-step job, this is usually the answer, and it is not the one people go looking for.\u003c/p\u003e\n\u003ch2 id=\"why-your-agent-keeps-failing-on-long-tasks\"\u003eWhy your agent keeps failing on long tasks\u003ca class=\"heading-anchor\" href=\"#why-your-agent-keeps-failing-on-long-tasks\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe tempting explanation is that long tasks are harder tasks. Longer means more ambiguity, more chances to misread the goal, more compounding confusion.\u003c/p\u003e\n\u003cp\u003eThat explanation is wrong, and there is a clean piece of work showing why. In \u0026ldquo;Is there a half-life for the success rates of AI agents?\u0026rdquo;, Toby Ord shows that agent performance across task lengths is explained by an extremely simple model: a constant rate of failing during each minute a human would take to do the task (\u003ca href=\"https://arxiv.org/abs/2505.05115\" data-external\u003eOrd, arXiv:2505.05115\u003c/a\u003e). Not accumulating confusion. A flat hazard rate, ticking the entire time the job runs.\u003c/p\u003e\n\u003cp\u003ePhysicists already have a letter for that rate. It is λ, the decay constant in N(t) = N₀·e^(−λt), and the half-life is just t½ = ln(2) / λ. Valve put the same λ on Gordon Freeman\u0026rsquo;s suit and in the \u003cem\u003eHalf-Life\u003c/em\u003e logo for the same reason: it is the constant that decides how long something lasts before it is gone. Ord\u0026rsquo;s result is that your agent has one too.\u003c/p\u003e\n\u003cp\u003eThat gives every agent a half-life: the task duration at which its success probability hits 50%. Longer tasks fail, in Ord\u0026rsquo;s framing, because they contain increasingly large sets of subtasks where failing any one fails the whole thing. Raise λ (worse per-minute reliability) or raise exposure (longer jobs), and the surviving fraction falls the same way a sample of isotope does. You do not need a crowbar for this. You need less exposure.\u003c/p\u003e\n\u003cp\u003eThis reframes the problem usefully. If failure were about difficulty, you would fix it with a smarter model. If failure is a rate per unit of exposure, you fix it by reducing exposure. Those are very different engineering programs, and almost everyone is running the first one.\u003c/p\u003e\n\u003ch2 id=\"why-multi-step-agent-workflows-fail-48-steps-85-success\"\u003eWhy multi-step agent workflows fail: 48 steps, 8.5% success\u003ca class=\"heading-anchor\" href=\"#why-multi-step-agent-workflows-fail-48-steps-85-success\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eTen steps is a toy. Real recurring business work is worse, because it loops.\u003c/p\u003e\n\u003cp\u003eHere is a job we hear about constantly from small agencies: monthly client reporting. Eight clients. For each one, pull analytics, pull ad spend, pull the CRM\u0026rsquo;s deal movement, write the summary, render it into the client\u0026rsquo;s template, email it. Six steps, eight times. Forty-eight steps.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003ePer-step reliability\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eOne 48-step run finishes\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e99%\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e62%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e95%\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e8.5%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e90%\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.6%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA 95% agent, which is a genuinely capable agent, completes that job start to finish about one time in twelve. And the failure mode is the ugly one: it dies at client six, having already emailed five reports, and you cannot tell what state anything is in without reading the whole transcript. One continuous experiment, cascading into a mess you did not budget for. Black Mesa energy, spreadsheet edition.\u003c/p\u003e\n\u003cp\u003eSo people conclude agents do not work for real operations. What actually does not work is running forty-eight steps as one uninterrupted bet.\u003c/p\u003e\n\u003ch2 id=\"will-a-better-model-fix-agent-reliability\"\u003eWill a better model fix agent reliability?\u003ca class=\"heading-anchor\" href=\"#will-a-better-model-fix-agent-reliability\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThere is a genuine counterargument: models are getting better at long tasks fast. METR measures the 50% time horizon, the task duration at which an agent is predicted to succeed half the time, using how long human experts need for the same work. That horizon has been doubling roughly every seven months for six years, with recent data suggesting faster (\u003ca href=\"https://metr.org/blog/2025-03-19-measuring-ai-ability-to-complete-long-tasks/\" data-external\u003eMETR, measuring AI ability to complete long tasks\u003c/a\u003e; \u003ca href=\"https://metr.org/time-horizons/\" data-external\u003eMETR, time horizons\u003c/a\u003e).\u003c/p\u003e\n\u003cp\u003eThat is real progress and it is not slowing. It is also a terrible thing to plan a business process around. The horizon is defined at 50% reliability, which is a coin flip, and the doubling curve tells you nothing about next Tuesday\u0026rsquo;s client reports. Gartner predicts that over 40% of agentic AI projects will be canceled by the end of 2027, citing escalating costs, unclear business value, and inadequate risk controls (\u003ca href=\"https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027\" data-external\u003eGartner, June 2025\u003c/a\u003e). A large share of those projects will die from exactly this: a demo that worked once, scheduled weekly, quietly failing most weeks. Unforeseen consequences, delivered on a cron.\u003c/p\u003e\n\u003cp\u003eWaiting for the model is a strategy with no delivery date. Shortening the run is available now.\u003c/p\u003e\n\u003ch2 id=\"how-to-fix-an-agent-that-fails-partway-cut-the-job-at-the-seams\"\u003eHow to fix an agent that fails partway: cut the job at the seams\u003ca class=\"heading-anchor\" href=\"#how-to-fix-an-agent-that-fails-partway-cut-the-job-at-the-seams\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eSplit those forty-eight steps into eight independent six-step runs and the arithmetic inverts.\u003c/p\u003e\n\u003cp\u003eAt 95% per step, one client\u0026rsquo;s six-step run finishes 73.5% of the time. So a pass over eight clients lands roughly six of them, and the two that failed are named, isolated, and retryable. Run the retry and you are at 93% of the work done, with the remaining gap being two specific clients you can look at rather than one opaque dead job.\u003c/p\u003e\n\u003cp\u003eSame model. Same per-step reliability. Same total work. The only change is where the run is allowed to stop and be resumed. You have not lowered λ. You have lowered how long any one run has to survive it.\u003c/p\u003e\n\u003cp\u003eThe catch is that this only works if state survives between the pieces. A six-step run that has to rebuild its own context from scratch, re-derive what the client\u0026rsquo;s template looks like, re-fetch what it already fetched, has not been shortened. It has been shortened and then padded back out. Checkpointing is only cheap when the checkpoint is written somewhere durable.\u003c/p\u003e\n\u003cp\u003eIn \u003cem\u003eHalf-Life 2\u003c/em\u003e, the Resistance spray-painted λ on walls to mark supply caches you could find later. Your agent needs the same kind of mark: a file on disk that says \u0026ldquo;this iteration already finished,\u0026rdquo; not a memory that evaporates when the turn ends. That is the actual argument for giving an agent a computer instead of a context window.\u003c/p\u003e\n\u003ch2 id=\"how-construct-runs-the-same-job\"\u003eHow Construct runs the same job\u003ca class=\"heading-anchor\" href=\"#how-construct-runs-the-same-job\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct\u0026rsquo;s agent has a persistent workspace: a filesystem that outlives the turn, long-term memories it carries across sessions, and reusable workflows that run on demand or from its Calendar. Those three pieces are what turn the client-reporting job from one long bet into a job that converges. Less HEV suit cosplay, more actual hazardous-environment gear: the state that has to survive the run is sitting on disk before anything else starts.\u003c/p\u003e\n\u003cp\u003eConcretely, the reporting job becomes one scheduled workflow rather than eight:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003ePiece\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhat it is in Construct\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhy it matters to the math\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe client list\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eA file in the workspace, \u003ccode\u003eclients.md\u003c/code\u003e, that the agent reads at the start of every run\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe run\u0026rsquo;s scope is data on disk, not something rebuilt from a prompt each time\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe completed work\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOne file per client under \u003ccode\u003ereports/2026-08/\u003c/code\u003e, written as each report finishes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEvery finished iteration is a checkpoint that outlives the run that made it\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe instruction\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;For each client in \u003ccode\u003eclients.md\u003c/code\u003e with no report in \u003ccode\u003ereports/2026-08/\u003c/code\u003e, produce one and email it\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe job describes remaining work, so a rerun is a retry rather than a duplicate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eStanding preferences\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLong-term memory: the template a given client wants, which metrics they care about\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIteration six does not re-derive what iteration one already established\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe retry\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe schedule itself, plus an on-demand run when you want it sooner\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe two clients that failed get picked up next pass with no manual bookkeeping\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eThe diagnosis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eThe Activity feed\u0026rsquo;s bounded action summaries and best-effort reasons, plus chat\u0026rsquo;s bounded tool records\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYou find the step that broke instead of rereading a transcript\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe important line is the third one. Because the instruction is scoped to clients without a report on disk, a run that dies at client six is not a disaster that needs unwinding. The next run reads the directory, sees five reports, and works on the remaining three. Nothing is sent twice, and nobody has to remember where it stopped.\u003c/p\u003e\n\u003cp\u003eTwo honest boundaries. Construct\u0026rsquo;s workflows are linear today: branching, fan-out, and subworkflows are not supported, so the clients are worked through inside a run rather than dispatched as eight parallel jobs. And the schedule interval is your retry cadence, so a job that must finish within the hour needs an on-demand rerun rather than patience. The infrastructure that makes a per-user filesystem cheap enough to leave lying around between runs is in \u003ca href=\"https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/\" data-external\u003ehow our agents get real computers we mostly do not pay for\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eRead next: \u003ca href=\"https://construct.computer/blog/ai-workflow-automation/\" data-external\u003eAI Workflow Automation\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"a-checklist-for-building-a-resumable-agent-job\"\u003eA checklist for building a resumable agent job\u003ca class=\"heading-anchor\" href=\"#a-checklist-for-building-a-resumable-agent-job\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe pattern generalizes past client reports. Anything shaped as \u0026ldquo;for each X, do a few things\u0026rdquo; is a candidate.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eFind the loop.\u003c/strong\u003e Almost every recurring ops job has one: per client, per invoice, per candidate, per repo. That loop boundary is your seam. Cut there first, because it is the cut that turns one long bet into many short ones.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMake each iteration leave an artifact.\u003c/strong\u003e A file, a CRM record, a sent email. Something a later run can check for existence. If an iteration leaves nothing behind, you cannot tell a retry from a duplicate. Leave a λ on the wall.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMake iterations skip completed work.\u003c/strong\u003e \u0026ldquo;Write the report for any client that does not already have one dated this month\u0026rdquo; is resumable. \u0026ldquo;Write the reports\u0026rdquo; is not. This one line is usually the difference between a job you can schedule and a job you have to babysit.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePut the ambiguous judgment in its own step.\u003c/strong\u003e Judgment steps have a lower per-step success rate than mechanical ones. Isolating them means a bad judgment call costs you that step, not the run.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSupervise the first several runs on demand before scheduling.\u003c/strong\u003e You are looking for which step is your weak link, and you will not guess it correctly.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCheck the trail when something fails.\u003c/strong\u003e Construct\u0026rsquo;s Activity feed keeps bounded action summaries and best-effort reasons, and chat retains bounded tool records, so a failed iteration can be traced to a step rather than to a vibe.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThat third point is worth sitting with. Most people write agent instructions as a description of the work. The instructions that survive scheduling are written as a description of the work that remains.\u003c/p\u003e\n\u003ch2 id=\"what-checkpointing-does-not-fix\"\u003eWhat checkpointing does not fix\u003ca class=\"heading-anchor\" href=\"#what-checkpointing-does-not-fix\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eCheckpointing lowers the cost of failure. It does not lower the failure rate, and there are jobs where that distinction matters. You still have the same λ. You just refuse to stake the whole sample on one continuous exposure.\u003c/p\u003e\n\u003cp\u003eIf your steps are genuinely order-dependent all the way through, with no natural loop and no safe stopping point, there is no seam to cut and you are back to one long bet. If a step has an irreversible external side effect, a payment, a public post, a message to a customer, then a retry is not free and idempotency has to be designed in rather than assumed. Construct lets you inspect a running task, interrupt it mid-turn, and answer a question the agent raises, but it does not currently insert a mandatory approval gate before every external side effect, so those steps still need real supervision before they run unattended.\u003c/p\u003e\n\u003cp\u003eMemory has a boundary too. Construct keeps workspace files and selected long-term memories across sessions, but automatic memory does not currently index uploaded files, private app results, terminal output, or live browser runs. Context that has to carry across a seam should be written to a file or stored explicitly, not assumed to survive because it appeared once in a transcript.\u003c/p\u003e\n\u003cp\u003eAnd if the work is genuinely deterministic, the same trigger and the same transformation every time with no judgment anywhere in the middle, a rule-based automation platform will do it more cheaply and more predictably than any agent. We laid out where that line falls in \u003ca href=\"https://construct.computer/blog/ai-agent-vs-zapier/\" data-external\u003eAI agent vs Zapier automation\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"ask-this-instead-of-which-model-should-i-use\"\u003eAsk this instead of \u0026ldquo;which model should I use\u0026rdquo;\u003ca class=\"heading-anchor\" href=\"#ask-this-instead-of-which-model-should-i-use\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;Which model should I use\u0026rdquo; is the question everyone asks. It is not usually the binding constraint. A model one tier better moves your per-step reliability a few points and leaves the exponent untouched. Restructuring a 48-step run into eight 6-step runs moves the exponent, which is where the leverage lives.\u003c/p\u003e\n\u003cp\u003eAsk instead: when this job fails at step thirty, what is left on disk, and what does the retry have to redo? If the answer is \u0026ldquo;nothing\u0026rdquo; and \u0026ldquo;all of it,\u0026rdquo; the model is not your problem. You do not need the right man in the wrong place. You need a workspace that outlives the turn.\u003c/p\u003e\n\u003cp\u003eStart with \u003ca href=\"https://construct.computer/blog/ai-workflow-automation/\" data-external\u003eAI workflow automation\u003c/a\u003e for how reusable steps and schedules fit together, \u003ca href=\"https://construct.computer/blog/ai-agent-memory/\" data-external\u003eAI agent memory\u003c/a\u003e for what carries across runs and how to correct it, and \u003ca href=\"https://construct.computer/blog/how-to-choose-an-ai-agent-platform-for-your-team/\" data-external\u003ehow to choose an AI agent platform\u003c/a\u003e if you are evaluating this against other tools. If the underlying idea is new, \u003ca href=\"https://construct.computer/blog/what-is-an-ai-employee/\" data-external\u003ewhat is an AI employee\u003c/a\u003e is the place to begin.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/agent-task-half-life/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-08-11T00:00:00Z","external_url":"https://construct.computer/blog/agent-task-half-life/","id":"https://ankush.one/blogs/agent-task-half-life/","image":"https://ankush.one/media/construct/agent-task-half-life.jpg","summary":"Why AI agents keep failing on long multi-step jobs: a 95% reliable agent finishes 48 steps 8.5% of the time. The fix is a resumable run, not a better model.","tags":["blogs","ai-agents","construct"],"title":"Your agent has a half-life","url":"https://ankush.one/blogs/agent-task-half-life/"},{"content_html":"\u003cp\u003eConstruct is an AI employee. You hand it a job. It goes and does the job, without you having to babysit it.\u003c/p\u003e\n\u003cp\u003eTo do that with agents like Hermes or OpenClaw, you spin up a VPS. Real CPU. Real disk. A full Linux machine that stays on between turns. That costs real money, and the moment more people sign up to poke around and never come back, it \u003cstrong\u003emurders your infrastructure bill\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eWe give every Agent that same kind of computer.\u003c/p\u003e\n\u003cp\u003eDon\u0026rsquo;t get excited - we are not paying for all of them.\u003c/p\u003e\n\u003cp\u003eThe computers work. When someone asks Construct to do something, the machine wakes up and does it. When the tab goes quiet, nothing stays on just to cosplay as useful. Idle boxes are a kink we refuse to subsidize.\u003c/p\u003e\n\u003cp\u003eWe like edging. So we put the whole stack on the edge. Agent loop in a Durable Object. Linux summoned for one tool call, then gone. Disk that is not a disk. The bill only finishes when somebody actually works.\u003c/p\u003e\n\u003cp\u003eThis post is how we built that. Behaviour, constants, and tradeoffs are production.\u003c/p\u003e\n\u003ch2 id=\"the-bill-that-scaled-with-people-who-never-came-back\"\u003eThe bill that scaled with people who never came back\u003ca class=\"heading-anchor\" href=\"#the-bill-that-scaled-with-people-who-never-came-back\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe default way to run an AI agent is to hand it a Linux box and walk away.\u003c/p\u003e\n\u003cp\u003eNot stupid. Agents are trained on bash. Bash wants a filesystem and a process table. The lazy move is a machine that stays up between turns. Lazy is usually correct.\u003c/p\u003e\n\u003cp\u003eUntil the invoice arrives.\u003c/p\u003e\n\u003cp\u003eWe know that invoice because we signed it first. Construct\u0026rsquo;s original backend was a Bun and Elysia monolith on a VPS. SQLite on local disk. nginx out front. Roughly 1,300 lines of container management whose only job was handing every user their own box. It worked. Growing the product meant growing a fleet of real machines with real disks, billed through the silent hours where nobody typed a character.\u003c/p\u003e\n\u003cp\u003eThat is the expensive version of \u0026ldquo;every user gets a computer.\u0026rdquo; Technically true. Financially a treadmill. Someone who signed up once and ghosted cost the same as a power user. The product could not scale if the bill scaled with accounts instead of work.\u003c/p\u003e\n\u003cp\u003eWe killed it on 30 March 2026. Commit title: \u0026ldquo;migrate backend from VPS monolith to Cloudflare Workers.\u0026rdquo; Everything in this post starts there.\u003c/p\u003e\n\u003cp\u003eHono on Workers. Per-user WebSocket hub in a Durable Object with hibernation. Container object that archived to R2 on sleep and restored on wake. D1 for relational state. R2 for files. Frontend on Workers assets. Two days later we dragged the leftover container infrastructure into a legacy folder and wrote the plan we still run: the agent runs headlessly inside Durable Objects.\u003c/p\u003e\n\u003cp\u003eContainers and Sandboxes went GA on 13 April 2026. We moved onto the Sandbox SDK once it had the shape we wanted.\u003c/p\u003e\n\u003cp\u003eWe did not see the future. We stared at a cost curve that scaled with signups instead of usage, bet on Durable Objects before there was a tidy product name for the thing, and watched the platform walk to the same answer. camelAI \u003ca href=\"https://camelai.com/blog/our-coding-agent-runs-in-a-cloudflare-durable-object-not-a-vm\" data-external\u003ehit the same wall and reached nearly the same conclusion\u003c/a\u003e independently. Validation, or a warning about people who write posts like this. Both.\u003c/p\u003e\n\u003ch2 id=\"only-half-a-computer-deserves-to-be-awake\"\u003eOnly half a computer deserves to be awake\u003ca class=\"heading-anchor\" href=\"#only-half-a-computer-deserves-to-be-awake\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;The agent needs a computer\u0026rdquo; is two unrelated things taped together.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAgent loop:\u003c/strong\u003e transcript, tool routing, model calls, memory recall. This is how Construct thinks across a turn.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMachine:\u003c/strong\u003e the thing that runs \u003ccode\u003epdftotext\u003c/code\u003e, converts a spreadsheet, compiles a project, unzips the archive somebody should not have uploaded. This is how Construct touches a real filesystem.\u003c/p\u003e\n\u003cp\u003eOnly the second needs Linux.\u003c/p\u003e\n\u003cp\u003eThe first needs durable state and a socket. Cloudflare sells exactly that. Split the two and the expensive half stops needing to be alive at all.\u003c/p\u003e\n\u003cp\u003eEverybody argues about which model to use. Almost nobody argues about which half of their stack is getting paid to nap. That is the whole post, in one sentence.\u003c/p\u003e\n\u003ch2 id=\"linux-shows-up-edges-the-job-and-leaves\"\u003eLinux shows up, edges the job, and leaves\u003ca class=\"heading-anchor\" href=\"#linux-shows-up-edges-the-job-and-leaves\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThis is the section the title is about.\u003c/p\u003e\n\u003cp\u003eEach Construct agent gets one Durable Object, resolved by name. Sessions are rows inside it, not objects of their own. Temporary subagents run as delegated sessions on the same parent. Fan out a job - buy concurrency, not instances. That is how Construct can split a big ask without spinning up a fleet.\u003c/p\u003e\n\u003cp\u003eThe transcript lives in that object\u0026rsquo;s SQLite. Two things, kept apart on purpose. One table is the append-only message log the user scrolls through. A separate record holds the compaction summary plus a watermark of how far it has already consumed. That second one is what reaches the model. Conflate them and you pay, per turn, to re-send an afternoon of conversation to a model that did not ask for it. People do this. At scale.\u003c/p\u003e\n\u003cp\u003eHibernation is where the bill actually moves. We use the WebSocket hibernation API, stash per-connection state on the socket itself, and let the runtime answer keepalives without ever waking the object.\u003c/p\u003e\n\u003cp\u003eA tab open all afternoon is not a running process. It is a socket the runtime holds and a handful of SQLite rows. The object wakes when something happens. Goes back down when it does not. Held, not running. Your Construct session feels continuous. Our invoice does not.\u003c/p\u003e\n\u003cp\u003eEviction can happen any moment, so every transcript row also archives to D1. Empty on start? Restore from the archive. Make death cheap and constant and it stops being an incident.\u003c/p\u003e\n\u003cp\u003eOne annoying edge case: replayed reconnect events can include a turn-started with no matching completion, because the object got evicted mid-turn. Client reconnects, nothing is running - we send terminal idle status so nobody sits watching a spinner think about a thought that died twenty minutes ago.\u003c/p\u003e\n\u003cp\u003eWhen the agent calls the terminal tool, we resolve a sandbox:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esandbox = getSandbox(SANDBOX_BINDING, instanceKeyFor(user, workspace), {\n    transport:            rpc,\n    enableDefaultSession: false,          # every exec is a fresh shell\n    sleepAfter:           idleWindowFor(plan),\n})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eInstance key scoped to user, or workspace when we know it. Not per agent. Not per session. Live containers track active humans, not agent count. Matters the moment somebody spawns twelve agents and goes to lunch.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esleepAfter\u003c/code\u003e is the pivot. Ten minutes idle and the box is gone. We call that a feature. You can call it edging. Same config value. The idle window is the plan\u0026rsquo;s command ceiling in minutes, clamped by a global 300-second cap, plus a five minute buffer. That resolves to a ten minute sleep on every plan today. Pair with active CPU billing on Cloudflare Containers - charged for CPU consumed, not wall-clock uptime - and an idle sandbox stops being something you manage.\u003c/p\u003e\n\u003cp\u003eDifference between \u0026ldquo;we should build a reaper for idle machines\u0026rdquo; and \u0026ldquo;there is nothing to reap.\u0026rdquo; One is a roadmap item forever. The other is a config value.\u003c/p\u003e\n\u003cp\u003eUnglamorous consequence: every exec is a fresh shell. Working directory does not persist. Agents hate this personally. We wrote a hint into the failure path explaining that each terminal call starts clean, relative paths resolve against the scratch tree, and any directory change or exported variable from an earlier call is gone forever. Grief counselling in a string in an error branch.\u003c/p\u003e\n\u003cp\u003eBefore each exec: kill leftover processes, then mount, then run. Kill after mount and you drop the local bucket\u0026rsquo;s inotify watch and can leave a stale read-only workspace behind. Learned that the hard way.\u003c/p\u003e\n\u003ch2 id=\"she-wrangler-on-my-d1-till-i-r2\"\u003eShe wrangler on my D1 till I R2\u003ca class=\"heading-anchor\" href=\"#she-wrangler-on-my-d1-till-i-r2\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct\u0026rsquo;s durable workspace is an R2 bucket mounted into the container over s3fs. \u003ccode\u003eallow_other\u003c/code\u003e. Explicit uid and gid so the non-root user can actually write.\u003c/p\u003e\n\u003cp\u003eShe wrangler on my D1 till I R2. The files finish. The machine does not have to.\u003c/p\u003e\n\u003cp\u003eThis is the second half of the cost story, and the half people skip. Quotas of 100 MB, 1 GB, and 3 GB are quotas on bytes stored, not volumes kept spun up. When Construct saves a report, a spreadsheet, or a research brief, those files outlive the machine by construction. They were never on the machine. Nobody\u0026rsquo;s work sits on provisioned block storage waiting for their owner to remember the product exists.\u003c/p\u003e\n\u003cp\u003eContrast again: provisioned volume waiting patiently versus bytes in R2 that do not care if the box is awake.\u003c/p\u003e\n\u003cp\u003eHow we verify the mount matters more than the mount itself. Checking the directory exists is useless. Passes on a stale read-only mount every time. So we make the non-root user touch a probe file and delete it, with a ten second timeout. Exit code zero means the mount is actually writable.\u003c/p\u003e\n\u003cp\u003eProbe fails: unmount, remount, fix ownership, probe again, only then give up. Debounced to once a minute. Isolate-level mount cache key carries a version string we bump when mount policy changes, so a warm isolate can never silently skip a policy change.\u003c/p\u003e\n\u003cp\u003eHonest footnote: local development uses plain directory sync, not FUSE. Files land root-owned. We chown on mount. That local/prod divergence cost us an afternoon. It is why we trust a write probe over any status output that claims everything is fine.\u003c/p\u003e\n\u003ch2 id=\"what-living-on-the-edge-actually-costs\"\u003eWhat living on the edge actually costs\u003ca class=\"heading-anchor\" href=\"#what-living-on-the-edge-actually-costs\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eNo sales pitch from here down. Living on the edge has a price. We pay it.\u003c/p\u003e\n\u003cp\u003eYou cannot set \u003ccode\u003eUSER\u003c/code\u003e in a Cloudflare Sandbox image. Control plane intercepts HTTPS and needs root. Privilege drop moves to the point of execution. Dom energy stays with the control plane. The agent bottoms out as \u003ccode\u003erunuser\u003c/code\u003e. Elevated commands stay root through a wrapper. Everything else is \u003ccode\u003erunuser -u \u0026lt;agent user\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eNo sudo. No passwordless sudoers. Process and FD limits by the wrapper. Elevated access per command or until the container sleeps. Grants live inside the container, not Worker memory. Once the machine is disposable, the container is the correct place for short-lived trust. Permission that dies with the box is permission you never have to remember to revoke.\u003c/p\u003e\n\u003cp\u003eMemory and a registry catalog used to be separate Workers behind service bindings. We folded them into one. Never a latency optimisation. Service bindings are already efficient in-account transport. The cost was operational: two more deployments, more local processes, more domains, more generated binding contracts, releases that had to be choreographed. Microservice theatre for 41 KiB. We stopped. Nothing came.\u003c/p\u003e\n\u003cp\u003eMemory was about 41 KiB compressed. Registry under 1 KiB. Combined Worker about 1,002 KiB against a 2 MiB gate. 500 ms startup stays a cutover check because Wrangler dry runs do not report startup. We wrote down when to undo it: compressed bundle past 2 MiB, startup past 500 ms, own release cadence needed, or failures widen the API blast radius. Four triggers. Before anyone got attached.\u003c/p\u003e\n\u003cp\u003eWeb app served by a Worker with static assets rather than Pages, because we needed custom domains to manage their own DNS and Pages OAuth could not.\u003c/p\u003e\n\u003cp\u003eLiving on the edge means never assuming you have time. Construct\u0026rsquo;s memory recall runs on a budget. Whole recall: 800 ms. Inside that, remote embedding plus Vectorize: 250 ms. Those are configured budgets, not measured percentiles. We do not have deployed latency dashboards yet. Docs say so.\u003c/p\u003e\n\u003cp\u003eSQLite full-text search and graph traversal are the floor and always return, local to the Durable Object. Vectorize is best-effort on top. Vector path drags: slightly worse answer, not a hang. On one big warm box you would simply wait. Waiting is exactly what you cannot afford when nothing is warm by default. Warmth is a luxury. Edging is the default.\u003c/p\u003e\n\u003cp\u003eEmbedding model was the one place with real measurements. 18-query set with close distractors, on Workers AI:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eModel\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003erecall@1\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003emrr\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eqwen3-embedding-0.6b\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e15/18\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.892\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003ebge-m3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e14/18\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.889\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003ebge-large-en-v1.5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e13/18\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.852\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eembeddinggemma-300m\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e12/18\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.824\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eQwen3 is trained for asymmetric retrieval. Tell it whether it holds a question or a stored record and ranking tightens. Query-side instruction moved mrr from 0.892 to 0.924 on the same set. Free accuracy for one string. Take it.\u003c/p\u003e\n\u003cp\u003eA 0.6b model also keeps this inside budget. Memory pipeline gate: USD 1 per 1,000 turns, excluding the primary chat model. Do not swap embedding models casually. Vectorize indexes are built at those exact dimensions. Different model means recreate and re-embed everything.\u003c/p\u003e\n\u003cp\u003eThree scars from the platform:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003esetTimeout\u003c/code\u003e versus \u003ccode\u003esetAlarm\u003c/code\u003e has a boundary around ten seconds.\u003c/strong\u003e Discord gateway reconnects with backoff. Anything beyond ten seconds goes on an alarm. Timers do not survive eviction. That object holds one WebSocket for every guild we serve. Split it when Discord reports more than one shard, somewhere around 2,500 guilds. Better to schedule that than discover it at 3am.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA fallback on the same backend as its primary is not a fallback.\u003c/strong\u003e Same outage, different name. Compaction runs the same Gemma build on Workers AI and AI Studio. Failover costs the route. Every other slot crosses providers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAlarms hand you a bare Durable Object id.\u003c/strong\u003e \u003ccode\u003ectx.id.name\u003c/code\u003e only exists when you reached the object through a named stub. Alarm-triggered instantiation does not. Ours addressed outbound messages. Queue rejected them. Repair alarm re-queued forever, at cost. Infinite unfinished business. Fix: persist the name on first use. Nothing warns you.\u003c/p\u003e\n\u003cp\u003eThe container is real. It is not free. Image lands between 1.5 and 1.9 GB, which forces \u003ccode\u003estandard-1\u003c/code\u003e, because disk allocation is effectively the image size limit. LibreOffice needs 4 GiB or it gets OOM killed. Production configured for up to 100 concurrent instances. Command execution capped at 300 seconds on every plan today. That is a real product limit this architecture caused, not a feature. s3fs is not real POSIX. It will surprise you on a weekend. Fresh shells irritate agents trained on bash. We pay for that in prompt tokens.\u003c/p\u003e\n\u003cp\u003eFlat version: if your workload is one long-lived process per user that truly never idles, none of this helps you. Buy a VM. Simpler. Happier. Nobody will make edging jokes at you.\u003c/p\u003e\n\u003ch2 id=\"what-we-run-today\"\u003eWhat we run today\u003ca class=\"heading-anchor\" href=\"#what-we-run-today\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eWorkers. Durable Objects with SQLite storage. Containers through the Sandbox SDK. D1. R2. Queues with dead letter queues. Vectorize. Workers AI. AI Gateway. Email Routing inbound with the send binding outbound. Rate limiting bindings. Cache API. Turnstile. Cron triggers. Static assets. Workers Logs. Agents SDK.\u003c/p\u003e\n\u003cp\u003eNo KV. No Hyperdrive. No machines of our own. Just Construct users with computers that mostly sleep.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://construct.computer\" data-external\u003econstruct.computer\u003c/a\u003e is Cloudflare too: Pages, Functions, D1, Turnstile, and the WAF.\u003c/p\u003e\n\u003cp\u003eAll our Agents get computers, we pay for almost none.\u003c/p\u003e\n\u003cp\u003eWe like edging. So we put the whole stack on the edge.\u003c/p\u003e\n\u003cp\u003eThat is how an AI employee can feel always-on without an always-on bill.\u003c/p\u003e\n\u003cp\u003eIf you want the product side rather than the plumbing, \u003ca href=\"https://construct.computer/blog/ai-employee/\" data-external\u003ewhat an AI employee actually does\u003c/a\u003e covers the work it completes, \u003ca href=\"https://construct.computer/blog/ai-agent-memory/\" data-external\u003einspectable agent memory\u003c/a\u003e covers what it remembers and how you correct it, and \u003ca href=\"https://construct.computer/blog/construct-vs-diy/\" data-external\u003ebuilding your own agent stack\u003c/a\u003e is the honest version of what you would be operating instead.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-07-31T00:00:00Z","external_url":"https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/","id":"https://ankush.one/blogs/running-ai-agents-on-cloudflare-not-vms/","image":"https://ankush.one/media/construct/running-ai-agents-on-cloudflare-not-vms.jpg","summary":"Construct is an AI employee with a real Linux computer per user. Don't get excited - we are not paying for all of them. How we run that on Cloudflare Durable Objects, Sandboxes, and R2.","tags":["blogs","ai-agents","cloudflare","construct"],"title":"All our Agents get computers, we pay for almost none","url":"https://ankush.one/blogs/running-ai-agents-on-cloudflare-not-vms/"},{"content_html":"\u003cp\u003eMost teams evaluating an AI agent platform compare feature lists and demo videos, then discover months later that the demo and the production job were not the same thing. This is a vendor-agnostic framework for how to choose an AI agent platform: why pilots stall, six criteria that actually predict whether a platform survives contact with real work, the governance rules now attached to that decision, and a scorecard you can apply to any product on your shortlist.\u003c/p\u003e\n\u003ch2 id=\"why-most-ai-agent-pilots-never-reach-production\"\u003eWhy most AI agent pilots never reach production\u003ca class=\"heading-anchor\" href=\"#why-most-ai-agent-pilots-never-reach-production\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAn estimated 88% of AI agent pilots fail to reach production, according to an analysis of enterprise deployments compiled by Digital Applied (\u003ca href=\"https://www.digitalapplied.com/blog/88-percent-ai-agents-never-reach-production-failure-framework\" data-external\u003eDigital Applied, AI agent failure framework\u003c/a\u003e). Gartner forecasts the same trend from the vendor side: more than 40% of agentic AI projects will be canceled by the end of 2027 due to escalating costs, unclear business value, or inadequate risk controls (\u003ca href=\"https://www.digitalapplied.com/blog/agent-washing-definition-buyers-scorecard-2026\" data-external\u003eGartner, press release, cited via Digital Applied\u0026rsquo;s agent-washing scorecard\u003c/a\u003e). Gartner Senior Director Analyst Anushree Verma named the underlying problem directly: \u0026ldquo;most agentic projects today are early-stage experiments driven by hype,\u0026rdquo; which blinds organizations to the real cost and complexity of deploying agents at scale.\u003c/p\u003e\n\u003cp\u003eThe failure pattern is not random. Digital Applied\u0026rsquo;s breakdown attributes 34% of failed pilots to scope creep, where an initially bounded automation absorbs new requirements until it becomes an open-ended reasoning system nobody scoped for. Data quality failures account for another 27%, when an agent tested against clean sample data meets production records full of incomplete fields and stale formatting. Security and access-control blockers cause 14% of failures, integration complexity accounts for 9%, and governance gaps, missing ownership, monitoring, or incident response, cause another 5% (\u003ca href=\"https://www.digitalapplied.com/blog/88-percent-ai-agents-never-reach-production-failure-framework\" data-external\u003eDigital Applied, AI agent failure framework\u003c/a\u003e). Scope and data readiness alone explain most of the gap between a working demo and a working system.\u003c/p\u003e\n\u003cp\u003eThis is also an active buying decision for most teams, not a settled one. Only 17% of organizations had deployed AI agents as of 2026, while more than 60% expect to deploy within two years, per Gartner\u0026rsquo;s CIO survey (\u003ca href=\"https://www.digitalapplied.com/blog/agent-washing-definition-buyers-scorecard-2026\" data-external\u003eDigital Applied, agent-washing scorecard\u003c/a\u003e). The evaluation criteria below are aimed at that gap: the difference between a platform that produces a good pilot and one that keeps producing good results after the third team starts depending on it.\u003c/p\u003e\n\u003ch2 id=\"the-ai-agent-evaluation-checklist-6-criteria-that-separate-a-pilot-from-a-platform\"\u003eThe AI agent evaluation checklist: 6 criteria that separate a pilot from a platform\u003ca class=\"heading-anchor\" href=\"#the-ai-agent-evaluation-checklist-6-criteria-that-separate-a-pilot-from-a-platform\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eFeature lists answer \u0026ldquo;can it do the task once, in a demo.\u0026rdquo; These six criteria answer \u0026ldquo;will it still be trustworthy after the fifth team is running unsupervised jobs on it.\u0026rdquo; Apply each one to any platform on your shortlist before you commit engineering time to a pilot.\u003c/p\u003e\n\u003ch3 id=\"tool-orchestration-and-execution-surfaces-browser-terminal-files-connected-apps\"\u003eTool orchestration and execution surfaces (browser, terminal, files, connected apps)\u003ca class=\"heading-anchor\" href=\"#tool-orchestration-and-execution-surfaces-browser-terminal-files-connected-apps\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eAsk what the agent can actually act on, not just what it can talk about. A platform limited to chat and a handful of first-party plugins can explain a task; a platform with a real browser, a sandboxed terminal, persistent files, and structured connected-app actions can complete one. The distinction matters because real jobs mix surfaces: a research step needs the open web, a records update needs a structured app action rather than screen-scraping, and a file transform needs a terminal with somewhere durable to write the result.\u003c/p\u003e\n\u003ch3 id=\"human-in-the-loop-controls-and-approval-gates\"\u003eHuman-in-the-loop controls and approval gates\u003ca class=\"heading-anchor\" href=\"#human-in-the-loop-controls-and-approval-gates\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThe question is not whether a platform claims supervision. It is what specifically happens before an agent takes an external action a person did not directly request: can you interrupt a running task, does the agent stop to ask when something is ambiguous, and is there a mandatory approval step before it emails a customer or posts publicly. A platform that answers \u0026ldquo;yes, generally\u0026rdquo; to all three without naming the mechanism has not actually answered the question.\u003c/p\u003e\n\u003ch3 id=\"audit-trails-and-inspectable-activity-and-what-audit-trail-should-actually-mean\"\u003eAudit trails and inspectable activity (and what \u0026ldquo;audit trail\u0026rdquo; should actually mean)\u003ca class=\"heading-anchor\" href=\"#audit-trails-and-inspectable-activity-and-what-audit-trail-should-actually-mean\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eVendors use \u0026ldquo;audit trail\u0026rdquo; loosely, and the gap between what it implies and what a product delivers matters once something goes wrong. A forensic audit log is an immutable, complete record of every operation, suitable for a compliance investigation. A bounded activity summary is a readable record of what an agent did and roughly why, suitable for an operator reviewing a day\u0026rsquo;s work. Both are useful. They are not the same claim, and a buyer who needs the former should not accept a vendor\u0026rsquo;s description of the latter at face value.\u003c/p\u003e\n\u003ch3 id=\"connector-coverage-and-custom-integrations-via-mcp\"\u003eConnector coverage and custom integrations via MCP\u003ca class=\"heading-anchor\" href=\"#connector-coverage-and-custom-integrations-via-mcp\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eOff-the-shelf integrations cover the common apps, but almost every real deployment eventually hits an internal system with no public API partnership. The Model Context Protocol (MCP) has become the standard way agent platforms reach those systems: about 41% of surveyed software organizations already report agents connected through MCP servers in limited or broad production, according to Stacklok\u0026rsquo;s 2026 survey as reported by Digital Applied (\u003ca href=\"https://www.digitalapplied.com/blog/mcp-adoption-statistics-2026-model-context-protocol\" data-external\u003eDigital Applied, MCP adoption statistics\u003c/a\u003e). Ask whether a platform supports custom MCP servers rather than only a fixed integration catalog, since that is what determines whether an internal tool can be reached without waiting on the vendor\u0026rsquo;s roadmap.\u003c/p\u003e\n\u003ch3 id=\"persistent-correctable-memory-across-sessions\"\u003ePersistent, correctable memory across sessions\u003ca class=\"heading-anchor\" href=\"#persistent-correctable-memory-across-sessions\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eA long chat history is not memory. The useful question is whether a platform can carry forward a stable fact, such as a preferred report format or a client contact, and whether you can correct that fact when it changes without wiping everything else the system knows. A platform that cannot distinguish \u0026ldquo;what was said\u0026rdquo; from \u0026ldquo;what is currently true\u0026rdquo; will eventually act on something that used to be accurate.\u003c/p\u003e\n\u003ch3 id=\"pricing-model-and-real-usage-limits-not-just-a-price-tag\"\u003ePricing model and real usage limits, not just a price tag\u003ca class=\"heading-anchor\" href=\"#pricing-model-and-real-usage-limits-not-just-a-price-tag\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eA published price answers less than it appears to. The number that actually predicts whether a plan fits a job is what gets metered: steps per task, command runtime, concurrent background jobs, and storage. Two platforms at the same monthly price can support very different workloads depending on where those limits sit, and a plan that looks generous on paper can throttle a deep research task or a long file transform before it finishes.\u003c/p\u003e\n\u003ch2 id=\"governance-pressure-is-raising-the-bar-eu-ai-act-and-agent-washing\"\u003eGovernance pressure is raising the bar: EU AI Act and \u0026ldquo;agent washing\u0026rdquo;\u003ca class=\"heading-anchor\" href=\"#governance-pressure-is-raising-the-bar-eu-ai-act-and-agent-washing\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eTwo outside pressures are pushing evaluation criteria from nice-to-have to load-bearing. The first is regulatory. The EU AI Act\u0026rsquo;s Article 14 requires high-risk AI systems to be designed so a human can effectively oversee them: monitor operation, intervene, and halt the system through a stop mechanism or equivalent procedure (\u003ca href=\"https://artificialintelligenceact.eu/article/14/\" data-external\u003eEU AI Act, Article 14\u003c/a\u003e). The Act\u0026rsquo;s full set of high-risk obligations, risk management, data governance, logging, transparency, human oversight, and cybersecurity, along with deployer obligations, becomes enforceable on August 2, 2026 (\u003ca href=\"https://www.legalnodes.com/article/eu-ai-act-2026-updates-compliance-requirements-and-business-risks\" data-external\u003eLegal Nodes, EU AI Act 2026 compliance timeline\u003c/a\u003e). A team whose agent work touches a use case the Act classifies as high-risk needs to treat human-in-the-loop controls and inspectable activity as compliance requirements, not product preferences.\u003c/p\u003e\n\u003cp\u003eThe second pressure is a credibility problem inside the market itself. Gartner estimates that only about 130 of the thousands of vendors marketing \u0026ldquo;agentic AI\u0026rdquo; are genuinely agentic; the rest is what Gartner calls agent washing, existing chatbots, RPA, or assistants relabeled for a hotter category (\u003ca href=\"https://www.digitalapplied.com/blog/agent-washing-definition-buyers-scorecard-2026\" data-external\u003eDigital Applied, agent-washing definition and buyer\u0026rsquo;s scorecard\u003c/a\u003e). That is exactly why the checklist above asks what a platform actually does rather than what it calls itself: a chatbot with a new label still cannot orchestrate tools, hold correctable memory, or leave an inspectable trail, no matter what the pitch deck says.\u003c/p\u003e\n\u003ch2 id=\"questions-to-ask-a-vendor-before-you-pilot\"\u003eQuestions to ask a vendor before you pilot\u003ca class=\"heading-anchor\" href=\"#questions-to-ask-a-vendor-before-you-pilot\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eBring these questions into a vendor call before you commit engineering time to a pilot. Each maps to one of the six criteria above.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eWhat can the agent act on beyond chat, and what happens to that work when the session ends?\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhat specifically stops the agent from doing something it should not?\u003c/strong\u003e Look for an explicit allowlist of capabilities, connected apps, and network origins with runtime denial of calls outside that allowlist.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIs \u0026ldquo;audit trail\u0026rdquo; a forensic log or a readable summary?\u003c/strong\u003e Get an explicit statement rather than implied feature language.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCan we reach a system you do not already integrate with?\u003c/strong\u003e Support for custom MCP servers means an internal system can be reached without waiting for first-party support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCan the agent be wrong about something and be corrected without losing everything else it knows?\u003c/strong\u003e Look for versioned memory assertions where a correction supersedes a stale fact while keeping history.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhat is metered, beyond the sticker price?\u003c/strong\u003e Ask for steps per task, command runtime, concurrent jobs, and storage limits stated plainly.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"a-6-point-scorecard-you-can-apply-to-any-ai-agent-platform\"\u003eA 6-point scorecard you can apply to any AI agent platform\u003ca class=\"heading-anchor\" href=\"#a-6-point-scorecard-you-can-apply-to-any-ai-agent-platform\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eScore each criterion against a specific platform before a pilot, not after. A platform that cannot answer a row concretely, only in marketing language, should lose points on that row regardless of how polished the demo looked.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e#\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCriterion\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWeak signal\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eStrong signal\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExecution surfaces\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eChat plus a fixed plugin list\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBrowser, terminal, files, and connected-app actions in one task\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHuman-in-the-loop controls\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;Supervised\u0026rdquo; with no named mechanism\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNamed interrupt, ask-for-input, and approval behavior, with stated gaps\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAudit trail\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVague \u0026ldquo;full audit trail\u0026rdquo; claim\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExplicit statement of whether it is forensic or a bounded summary\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eConnector coverage\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFixed integration catalog only\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNative catalog plus custom MCP or an equivalent extension path\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMemory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLong chat history described as memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCorrectable, provenance-tracked assertions separate from raw history\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e6\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePricing and limits\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePrice with no usage detail\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePrice plus steps, runtime, concurrency, and storage limits stated plainly\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA team piloting inbound lead qualification, for example, would run this scorecard against each shortlisted platform using the actual job: read a lead email, decide whether it qualifies, update the CRM, and notify the team in Slack. Score row 1 by watching which surface handles the CRM update. Score row 3 by asking, in writing, whether the vendor\u0026rsquo;s activity record would hold up in a compliance review or only in a weekly stand-up. A platform that scores well on four rows and poorly on two has told you exactly where the pilot needs extra supervision, not whether to abandon it.\u003c/p\u003e\n\u003ch2 id=\"related-resources\"\u003eRelated resources\u003ca class=\"heading-anchor\" href=\"#related-resources\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://artificialintelligenceact.eu/article/14/\" data-external\u003eThe EU AI Act\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.digitalapplied.com/blog/88-percent-ai-agents-never-reach-production-failure-framework\" data-external\u003eDigital Applied: AI agent failure framework\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.digitalapplied.com/blog/agent-washing-definition-buyers-scorecard-2026\" data-external\u003eDigital Applied: Agent-washing definition and buyer\u0026rsquo;s scorecard\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/how-to-choose-an-ai-agent-platform-for-your-team/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-07-28T00:00:00Z","external_url":"https://construct.computer/blog/how-to-choose-an-ai-agent-platform-for-your-team/","id":"https://ankush.one/blogs/how-to-choose-an-ai-agent-platform-for-your-team/","image":"https://ankush.one/media/construct/how-to-choose-an-ai-agent-platform-for-your-team.jpg","summary":"A vendor-agnostic evaluation checklist for AI agent platforms: pilot-failure data, six evaluation criteria, governance pressure, and a scorecard you can reuse.","tags":["blogs","ai-agents","construct"],"title":"How to Choose an AI Agent Platform for Your Team","url":"https://ankush.one/blogs/how-to-choose-an-ai-agent-platform-for-your-team/"},{"content_html":"\u003cp\u003eInternal tools often begin as a spreadsheet, a checklist, or a prompt someone runs every week. The process works, but the interface does not: people copy values between tabs, remember status in their heads, and repeat the same setup each time. Construct can turn that repeated process into a small private app that lives inside the same workspace as the files and agent doing the work.\u003c/p\u003e\n\u003cp\u003eThis is not a one-click path to a public SaaS product. A Construct workspace app is a focused internal interface for one workspace. You describe the job, the agent writes the app, checks that it builds, opens it in the desktop, and keeps the source available for inspection and later changes.\u003c/p\u003e\n\u003ch2 id=\"start-with-the-repeated-process\"\u003eStart with the repeated process\u003ca class=\"heading-anchor\" href=\"#start-with-the-repeated-process\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe best request describes the work rather than prescribing a software architecture. For example:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u0026ldquo;Build a research request tracker with an owner, priority, status, and link to the finished report.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u0026ldquo;Turn this recurring launch checklist into an app the team can update.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u0026ldquo;Create a dashboard for the JSON reports stored in this workspace.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u0026ldquo;Build an intake form for customer-review requests and keep the queue in one place.\u0026rdquo;\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eConstruct can start from a list, form, dashboard, or blank interface. The agent creates the app package, writes the React and TypeScript source, declares the capabilities it needs, and validates the result. A successful build opens directly in the Construct web desktop instead of sending you to a separate hosting project.\u003c/p\u003e\n\u003cp\u003eThe request still matters. \u0026ldquo;Build an operations dashboard\u0026rdquo; leaves important choices unresolved. \u0026ldquo;Show open research requests, let an operator assign an owner, and mark the linked report as reviewed\u0026rdquo; gives the agent a bounded workflow and a clear way to tell whether the app is useful.\u003c/p\u003e\n\u003ch2 id=\"the-app-lives-beside-the-work\"\u003eThe app lives beside the work\u003ca class=\"heading-anchor\" href=\"#the-app-lives-beside-the-work\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eA workspace app is not a hidden artifact. Its source lives under an \u003ccode\u003eApps/\u0026lt;app-id\u0026gt;/\u003c/code\u003e folder in Files, where it can be inspected and edited. After a successful build, the app appears as a private workspace install that can be reopened from the desktop, Launchpad, or Spotlight.\u003c/p\u003e\n\u003cp\u003eThe app\u0026rsquo;s durable state is kept separately under \u003ccode\u003eAppData/\u0026lt;app-id\u0026gt;/\u003c/code\u003e. That separation is important: changing the interface should not require overwriting the records the app already manages. Rebuilding can update the code while existing app data remains in the workspace and continues to count against the normal workspace storage quota.\u003c/p\u003e\n\u003cp\u003eThis model also makes the surrounding agent useful after launch. The same workspace contains the source, relevant files, conversations, and diagnostics. If the interface needs another field or a runtime error appears, the next task can start with the actual app rather than a screenshot and a vague bug report.\u003c/p\u003e\n\u003ch2 id=\"what-a-workspace-app-can-do\"\u003eWhat a workspace app can do\u003ca class=\"heading-anchor\" href=\"#what-a-workspace-app-can-do\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe current runtime is intentionally narrower than a general web-hosting platform. That keeps an internal tool close to its workspace purpose.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eCapability\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCurrent workspace-app support\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eReact interface\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTypeScript and TSX\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eRead or write workspace files\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWith declared permission\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eApp-scoped JSON state\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWith declared Files permission\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eCall an allowed connected app\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWith an explicit app allowlist\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMake direct network requests\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOnly to declared HTTPS origins\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInstall arbitrary npm packages\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eRun terminal or agent orchestration from the app UI\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003ePublish automatically to the public app registry\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe app UI receives a small Construct SDK for workspace files, app-scoped storage, notifications, window controls, and explicitly permitted calls. It does not inherit every tool available to the agent that authored it. The agent can use broader execution surfaces while building, but the finished interface runs with its own narrower contract.\u003c/p\u003e\n\u003ch2 id=\"permissions-are-part-of-the-design\"\u003ePermissions are part of the design\u003ca class=\"heading-anchor\" href=\"#permissions-are-part-of-the-design\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAn internal tool should not gain broad access simply because it was generated inside a trusted workspace. A workspace app declares the native capabilities, connected apps, and network origins it intends to use. Calls outside those allowlists are denied at runtime.\u003c/p\u003e\n\u003cp\u003eThe platform also rechecks current workspace membership and permissions when the app makes a gateway call. File operations remain subject to the user\u0026rsquo;s workspace access, and an app cannot use its file access to rewrite another app\u0026rsquo;s package. Direct network access is limited to exact HTTPS origins rather than wildcard domains.\u003c/p\u003e\n\u003cp\u003eFor a request tracker that only reads and writes workspace files, that can mean declaring Files access and nothing else. A tool that posts an approved record to a connected service needs that specific app target as well. The useful default is the smallest permission set that completes the job.\u003c/p\u003e\n\u003ch2 id=\"validation-creates-a-build-not-a-guarantee\"\u003eValidation creates a build, not a guarantee\u003ca class=\"heading-anchor\" href=\"#validation-creates-a-build-not-a-guarantee\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eBefore launch, Construct checks the package structure and manifest, follows relative imports, enforces package boundaries, compiles the TypeScript and JSX, and reports design findings. A successful validation creates an immutable artifact with a deterministic build ID and opens the app.\u003c/p\u003e\n\u003cp\u003eThat proves the package can be built under the workspace runtime. It does not prove that every button, data shape, connected service, or edge case behaves correctly. Runtime verification still matters, especially after changing permissions, storage behavior, or an external call.\u003c/p\u003e\n\u003cp\u003eConstruct captures bounded console, network, gateway, and runtime-error diagnostics from workspace apps. The agent can inspect recent errors, repair the source, validate again, and ask you to verify the changed behavior. The loop is closer to maintaining a small internal product than generating a disposable code snippet.\u003c/p\u003e\n\u003ch2 id=\"draft-changes-do-not-replace-the-last-good-build\"\u003eDraft changes do not replace the last good build\u003ca class=\"heading-anchor\" href=\"#draft-changes-do-not-replace-the-last-good-build\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe reliability model is deliberately conservative. A successful build becomes the current runtime artifact. Later source edits mark the app as having draft changes, but opening the app continues to run the last successful build until the new source validates.\u003c/p\u003e\n\u003cp\u003eIf validation fails, the working artifact is not replaced. Cancelling an edit also leaves both the source and prior build in place. The desktop can show whether an app is ready, has draft changes, or has never built successfully.\u003c/p\u003e\n\u003cp\u003eThis is a last-good-build fallback, not full version history. It protects the working tool while a repair is in progress; it does not promise browsing or restoring every historical build.\u003c/p\u003e\n\u003ch2 id=\"internal-tool-or-workflow\"\u003eInternal tool or workflow?\u003ca class=\"heading-anchor\" href=\"#internal-tool-or-workflow\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eNot every repeated process needs an interface. Use a workspace app when people need to view, enter, filter, or review state. Use a Construct workflow when the main value is running a known sequence of agent steps, connected-app actions, and notifications. Use a scheduled agent job when the process should run in the background without waiting for someone to open a screen.\u003c/p\u003e\n\u003cp\u003eThe two can support the same operation. A workspace app can provide the queue and review surface, while files hold durable records and a scheduled job prepares new work. The important boundary is that the app UI does not secretly become an unrestricted agent: automation still runs through the execution surfaces and permissions designed for it.\u003c/p\u003e\n\u003ch2 id=\"where-workspace-apps-fit\"\u003eWhere workspace apps fit\u003ca class=\"heading-anchor\" href=\"#where-workspace-apps-fit\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eGood fits are narrow tools with a clear operator and a durable workspace context:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIntake forms and review queues\u003c/li\u003e\n\u003cli\u003eChecklists for repeated operations\u003c/li\u003e\n\u003cli\u003eLightweight trackers over workspace records\u003c/li\u003e\n\u003cli\u003eDashboards for files or app-scoped JSON data\u003c/li\u003e\n\u003cli\u003eFocused interfaces over an explicitly allowed connected service\u003c/li\u003e\n\u003cli\u003eSmall utilities that make an existing workflow easier to run and inspect\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003ePoor fits include public customer-facing sites, products that require an arbitrary npm ecosystem, complex multi-service systems, or workloads that need independently operated infrastructure and security controls. Public registry apps also follow a separate developer and review workflow; a private workspace build is not automatically promoted to the public app registry.\u003c/p\u003e\n\u003ch2 id=\"build-the-smallest-useful-interface\"\u003eBuild the smallest useful interface\u003ca class=\"heading-anchor\" href=\"#build-the-smallest-useful-interface\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe shortest path is to start with one repeated process and one person who needs to operate it. Name the records the app should manage, the actions the operator should take, the files or services it may access, and what a successful result looks like. Then ask Construct to build the smallest interface that makes that process easier to run and supervise.\u003c/p\u003e\n\u003cp\u003eOnce the first build opens, use it. The missing field, unclear status, or unnecessary screen becomes obvious faster in a working tool than in a long specification. Construct can revise the source while the previous good build remains available, giving the internal tool room to improve without turning every edit into a fresh software project.\u003c/p\u003e\n\u003ch2 id=\"related-resources\"\u003eRelated resources\u003ca class=\"heading-anchor\" href=\"#related-resources\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://construct.computer\" data-external\u003eConstruct homepage\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://construct.computer/blog/what-is-an-ai-employee/\" data-external\u003eWhat is an AI employee?\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://construct.computer/blog/ai-workflow-automation/\" data-external\u003eAI workflow automation\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003eOriginally posted on \u003ca href=\"https://construct.computer/blog/build-internal-tools-with-construct/\" data-external\u003econstruct.computer\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-07-27T00:00:00Z","external_url":"https://construct.computer/blog/build-internal-tools-with-construct/","id":"https://ankush.one/blogs/how-construct-builds-internal-tools/","image":"https://ankush.one/media/construct/build-internal-tools-with-construct.jpg","summary":"Turn a repeated process into a private workspace app that Construct can build, validate, debug, and update while preserving the last successful build.","tags":["blogs","ai-agents","construct"],"title":"How Construct Builds Internal Tools in Your Workspace","url":"https://ankush.one/blogs/how-construct-builds-internal-tools/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://construct.computer\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://github.com/construct-computer\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eConstruct Computer gives every user an isolated cloud computer with an AI agent that can operate it on their behalf. The agent can browse the web, write code, manage files, send emails, and work across connected tools while the user watches in real time from the browser.\u003c/p\u003e\n\u003ch2 id=\"what-it-is\"\u003eWhat It Is\u003ca class=\"heading-anchor\" href=\"#what-it-is\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eConstruct is a cloud desktop for AI agents. Instead of only chatting about a task, the user can give the agent a real working environment with a browser, terminal, file system, code editor, email, calendar, Slack, Telegram, and connected apps.\u003c/p\u003e\n\u003cp\u003eThe experience is collaborative: the agent can work autonomously, the user can take over at any time, and both can share the same live desktop session.\u003c/p\u003e\n\u003ch2 id=\"platform\"\u003ePlatform\u003ca class=\"heading-anchor\" href=\"#platform\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe public GitHub organization breaks the platform into a few clear pieces:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003econstruct\u003c/strong\u003e \u0026ndash; core platform, agent backend, API, and deployment\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eweb-desktop\u003c/strong\u003e \u0026ndash; React and TypeScript browser desktop environment\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003enotch\u003c/strong\u003e \u0026ndash; macOS companion app\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eapp-registry\u003c/strong\u003e \u0026ndash; registry of installable MCP apps\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eapp-sdk\u003c/strong\u003e \u0026ndash; SDK for building Construct apps\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"technical-focus\"\u003eTechnical Focus\u003ca class=\"heading-anchor\" href=\"#technical-focus\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe project combines a persistent browser-based workspace, agent runtime, app registry, and MCP-style extensibility. The product surface is built around visible agent work: users can see files, browser tabs, terminal commands, and app interactions happen in the same computer the agent uses.\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-04-01T00:00:00Z","id":"https://ankush.one/projects/2026.construct-computer/","summary":"A personal cloud computer with an AI agent that can browse, code, manage files, and work across everyday tools.","tags":["projects","ai-agents","construct"],"title":"Construct Computer","url":"https://ankush.one/projects/2026.construct-computer/"},{"content_html":"\u003cp\u003eCo-founder and CTO at \u003ca href=\"https://construct.computer\" data-external\u003eConstruct Computer\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s an AI workforce where the agents get their own cloud computers, so they can actually finish work instead of just talking about it. Think browser, terminal, files, memory, the whole desk - not another chat window.\u003c/p\u003e\n\u003cp\u003eMore on the \u003ca href=\"https://ankush.one/projects/2026.construct-computer/\"\u003eproject page\u003c/a\u003e.\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-04-01T00:00:00Z","id":"https://ankush.one/timeline/2026.construct/","summary":"Started building Construct Computer as Co-founder and CTO: an AI workforce where the agents get their own cloud computers.","tags":["timeline","job","construct","ai-agents"],"title":"Co-founded Construct Computer","url":"https://ankush.one/timeline/2026.construct/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://tradex.cards\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://tradex.cards/docs\"\u003eAPI Docs\u003c/a\u003e · \u003ca href=\"https://www.npmjs.com/package/@pokeperps/mcp\"\u003eMCP Server\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003ePokePerps lets you go long or short on Pokemon card prices with up to 50x leverage. Prices are scraped from TCGPlayer every 30 seconds and published on-chain via a keeper oracle. All trading is non-custodial \u0026ndash; positions, margin, and settlement are handled entirely by the on-chain program.\u003c/p\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow It Works\u003ca class=\"heading-anchor\" href=\"#how-it-works\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eTCGPlayer.com                    Solana Blockchain\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e(card prices)                    (settlement layer)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      |                                |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      | scrape every 30s               |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      v                                v\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+----------+    publish prices    +--------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| Backend  | ------------------\u0026gt; |   On-Chain   |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| (Elysia) |                     |   Program    |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e|          | \u0026lt;------------------ |              |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e|          |    read state       | (positions)  |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+----+-----+                     +--------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     | REST + WebSocket                 ^\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     v                                  | sign txs\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+----------+                            |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| Frontend | ---------------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| (React)  |   Phantom wallet\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+----------+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe backend scrapes TCGPlayer marketplace data and publishes price updates on-chain. The Solana program (Rust + \u003ca href=\"https://github.com/anza-xyz/pinocchio\" data-external\u003ePinocchio\u003c/a\u003e) manages all trading logic \u0026ndash; opening/closing positions, margin calculations, liquidations, and settlement. The frontend connects via Phantom wallet for signing.\u003c/p\u003e\n\u003ch2 id=\"ai-agent-integration\"\u003eAI Agent Integration\u003ca class=\"heading-anchor\" href=\"#ai-agent-integration\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003ePokePerps ships an \u003ca href=\"https://www.npmjs.com/package/@pokeperps/mcp\" data-external\u003eMCP server\u003c/a\u003e so Claude and other AI agents can trade programmatically.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Claude Code (read-only)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eclaude mcp add pokeperps -- npx -y @pokeperps/mcp\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# With trade execution\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eclaude mcp add pokeperps -e \u003cspan class=\"nv\"\u003ePOKEPERPS_KEYPAIR\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e/path/to/keypair.json -- npx -y @pokeperps/mcp\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThere\u0026rsquo;s also a \u003ca href=\"https://tradex.cards/SKILL.md\" data-external\u003eSKILL.md\u003c/a\u003e and \u003ca href=\"https://tradex.cards/llms.txt\" data-external\u003ellms.txt\u003c/a\u003e for agents that prefer fetching instructions directly.\u003c/p\u003e\n\u003ch2 id=\"tech-stack\"\u003eTech Stack\u003ca class=\"heading-anchor\" href=\"#tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eComponent\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTechnology\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eOn-chain program\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRust + Pinocchio (Solana)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eBackend\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTypeScript + Elysia + SQLite\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eFrontend\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReact 19 + Tailwind v4 + shadcn/ui\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eWallet\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePhantom Connect\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eRuntime\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBun\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAI integration\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMCP server (\u003ccode\u003e@pokeperps/mcp\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"program-addresses\"\u003eProgram Addresses\u003ca class=\"heading-anchor\" href=\"#program-addresses\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eNetwork\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eProgram ID\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMainnet\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e8hH5CWo14R5QhaFUuXpxJytchS6NgrhRLHASyVeriEvN\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eDevnet\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eAzYLMDPsNDvzb8ShhVzNWBVv6k9ogCKpqdjWMhnkATdz\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-02-16T00:00:00Z","id":"https://ankush.one/projects/2026.pokeperps/","summary":"A decentralized perpetual futures exchange for trading Pokemon card prices on Solana.","tags":["projects","solana","defi","ai-agents"],"title":"PokePerps","url":"https://ankush.one/projects/2026.pokeperps/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/pumpmyclaw\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://web.archive.org/web/20260207190608/https://pumpmyclaw.fun/\"\u003eWebsite\u003c/a\u003e (archived snapshot)\u003c/p\u003e\n\u003cp\u003eCrypto trading is emotional, exhausting, and never stops. Manual traders miss opportunities while sleeping, make FOMO-driven decisions, and can\u0026rsquo;t monitor thousands of token launches across multiple chains. Meanwhile, AI agents exist but live in walled gardens with no skin in the game and no verifiable on-chain track record.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePumpMyClaw\u003c/strong\u003e is a platform where anyone can deploy their own autonomous AI trading agent on Solana and Monad. Each agent trades tokens 24/7, launches its own token on the bonding curve, buys back its own token with a portion of every realized profit, and competes on a public leaderboard ranked by verified on-chain P\u0026amp;L.\u003c/p\u003e\n\u003ch2 id=\"the-buyback-flywheel\"\u003eThe Buyback Flywheel\u003ca class=\"heading-anchor\" href=\"#the-buyback-flywheel\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe core innovation is a self-reinforcing economic loop. When an agent trades and realizes profit, 30% of that profit is used to buy back the agent\u0026rsquo;s own token. This buy pressure pushes the token price up, which attracts new investors via the public leaderboard, which grows the ecosystem further.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eBot trades --\u0026gt; Makes profit --\u0026gt; Buys back its own token --\u0026gt; Token price goes up\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     ^                                                           |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     +------------- Creator fees + new believers ----------------+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis creates a financial incentive for AI performance that is entirely on-chain and verifiable.\u003c/p\u003e\n\u003ch2 id=\"dual-chain-trading\"\u003eDual-Chain Trading\u003ca class=\"heading-anchor\" href=\"#dual-chain-trading\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEvery agent trades on two chains simultaneously: \u003cstrong\u003eSolana\u003c/strong\u003e (via pump.fun) and \u003cstrong\u003eMonad\u003c/strong\u003e (via nad.fun). Agents only trade on chains where they have funds \u0026ndash; if both are funded, they trade on both at the same time.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSolana\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eMonad\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDEX\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003epump.fun\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003enad.fun\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNative token\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSOL\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMON\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eOn-chain data\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHelius webhooks\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAlchemy webhooks\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eToken creation\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003epump.fun bonding curve\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003enad.fun bonding curve\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow It Works\u003ca class=\"heading-anchor\" href=\"#how-it-works\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ch3 id=\"deploy-in-2-minutes\"\u003eDeploy in 2 Minutes\u003ca class=\"heading-anchor\" href=\"#deploy-in-2-minutes\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eSign in with Telegram, subscribe, enter your bot token and API key, and the platform spins up an isolated Docker container running an OpenClaw agent pre-configured with five skills (Solana, Monad, PumpFun, nad.fun, PumpMyClaw). No coding required.\u003c/p\u003e\n\u003ch3 id=\"the-trading-loop\"\u003eThe Trading Loop\u003ca class=\"heading-anchor\" href=\"#the-trading-loop\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eEvery 120 seconds, the agent runs a structured heartbeat covering both chains:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eStatus Check\u003c/strong\u003e \u0026ndash; Balances on both chains, survival threshold, open positions\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePosition Management\u003c/strong\u003e \u0026ndash; Sell all positions flagged for exit\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBuyback\u003c/strong\u003e \u0026ndash; Buy back own token with 30% of realized profits (per chain)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOpportunity Scan\u003c/strong\u003e \u0026ndash; Scan trending tokens on pump.fun and nad.fun\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTrade Execution\u003c/strong\u003e \u0026ndash; Confirm analysis, size position, execute buy\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eToken Creation\u003c/strong\u003e \u0026ndash; Create token if funded enough and none exists\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReport\u003c/strong\u003e \u0026ndash; Send one combined message to owner covering both chains\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"technical-analysis-engine\"\u003eTechnical Analysis Engine\u003ca class=\"heading-anchor\" href=\"#technical-analysis-engine\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThe analysis engine recognizes 25+ patterns across multiple categories:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCandlestick patterns\u003c/strong\u003e: Hammer, Shooting Star, Doji, Engulfing, Marubozu, Morning Star, Evening Star, Three White Soldiers, Three Black Crows, and more\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTrend analysis\u003c/strong\u003e: Higher Highs, Lower Lows, Support/Resistance Tests, Consolidation\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIndicators\u003c/strong\u003e: RSI, SMA, EMA, MACD, Bollinger Bands, Volume Analysis, Buy/Sell Pressure\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSpecial signals\u003c/strong\u003e: Bollinger Squeeze, Volume Breakout, Capitulation Bottom, Support Bounce\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe agent auto-tunes its strategy by tracking trade outcomes and adjusting pattern weights based on historical performance.\u003c/p\u003e\n\u003ch3 id=\"risk-management\"\u003eRisk Management\u003ca class=\"heading-anchor\" href=\"#risk-management\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eBuilt into the agent\u0026rsquo;s core behavioral framework:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePosition sizes capped at 0.004 SOL / 3.0 MON per trade\u003c/li\u003e\n\u003cli\u003eMaximum 2 open positions per chain\u003c/li\u003e\n\u003cli\u003eBalance reserves maintained (0.008 SOL / 1.0 MON)\u003c/li\u003e\n\u003cli\u003eTake profit at +15%, stop loss at -10%, max hold time of 10 minutes\u003c/li\u003e\n\u003cli\u003eDaily loss circuit breaker stops buying after exceeding loss thresholds\u003c/li\u003e\n\u003cli\u003eSell-first priority: always frees up capital before opening new positions\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"architecture\"\u003eArchitecture\u003ca class=\"heading-anchor\" href=\"#architecture\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe platform has two separate backends for separation of concerns:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eapps/backend\u003c/code\u003e\u003c/strong\u003e (Bun + Hono, self-hosted) \u0026ndash; User auth via Telegram OAuth, paid subscriptions via NOWPayments, Docker container lifecycle management\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eapps/api\u003c/code\u003e\u003c/strong\u003e (Cloudflare Workers, edge-deployed) \u0026ndash; Public data API, trade ingestion via Helius/Alchemy webhooks, rankings, real-time WebSocket feed via Durable Objects\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    +------------------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    |     Cloudflare Workers        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    |  D1 + Durable Objects +       |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    |  Queues + Cron + WebSocket    |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    +--------------+---------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                                   |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            +----------------------+----------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            |                      |                      |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   +--------v-------+   +---------v---------+   +--------v-----------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |   React 19     |   | Helius + Alchemy  |   |   Bot Instances    |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |   SPA          |   |    Webhooks       |   |  (Docker, one per  |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |  Vite + TW4    |   | (on-chain tx)     |   |   user, OpenClaw)  |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   +----------------+   +-------------------+   +--------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   +--------v--------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |     Bun + Hono + SQLite     |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |  Telegram Auth, JWT,        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |  Docker lifecycle,          |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   |  NOWPayments subscriptions  |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   +-----------------------------+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"platform-features\"\u003ePlatform Features\u003ca class=\"heading-anchor\" href=\"#platform-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eLive Leaderboard\u003c/strong\u003e \u0026ndash; Agents ranked by verified on-chain P\u0026amp;L across both chains. Win rate, trade count, volume, 24h token price change, and buyback tracking. Auto-refreshes every 15 seconds.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLive Trade Feed\u003c/strong\u003e \u0026ndash; WebSocket-powered real-time stream of all agent trades on both chains. Every trade links to on-chain verification (Solscan for Solana, MonadVision for Monad).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAgent Profiles\u003c/strong\u003e \u0026ndash; Full trade history, token price candlestick charts via DexScreener, token stats (price, market cap, liquidity, volume), and the agent\u0026rsquo;s strategy context feed.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDashboard\u003c/strong\u003e \u0026ndash; Real-time bot status, SSE log streaming during deployment, wallet balances on both chains, start/stop/delete controls, and model/API key management.\u003c/p\u003e\n\u003ch2 id=\"on-chain-verification\"\u003eOn-Chain Verification\u003ca class=\"heading-anchor\" href=\"#on-chain-verification\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEvery claim on PumpMyClaw is backed by on-chain data. Helius and Alchemy webhooks deliver raw transactions, which are parsed into structured trade records. A buyback detector tags self-token purchases. A P\u0026amp;L calculator tracks cost basis vs sale proceeds and computes win rates. Rankings are recalculated every 60 seconds via a cron job, and a fallback poller backfills any missed transactions.\u003c/p\u003e\n\u003ch2 id=\"tech-stack\"\u003eTech Stack\u003ca class=\"heading-anchor\" href=\"#tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eLayer\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTechnology\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAI Runtime\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOpenClaw + OpenRouter\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAI Models\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClaude, Kimi, Qwen, DeepSeek (user\u0026rsquo;s choice)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eBlockchain\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSolana Mainnet + Monad Mainnet\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDEX\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003epump.fun / PumpPortal + nad.fun\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eOn-chain Data\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHelius + Alchemy Webhooks\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eInstance Backend\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBun + Hono + SQLite (Drizzle)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePublic API\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloudflare Workers + D1 + Durable Objects + Queues\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eFrontend\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReact 19 + Vite 6 + TailwindCSS 4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAuth\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTelegram OAuth + JWT\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePayments\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNOWPayments (crypto)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eContainers\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDocker (800MB/instance, 1 CPU)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMonorepo\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTurborepo + Bun Workspaces\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"security\"\u003eSecurity\u003ca class=\"heading-anchor\" href=\"#security\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eAPI keys encrypted at rest (AES-256-GCM)\u003c/li\u003e\n\u003cli\u003eJWT authentication with Telegram OAuth verification\u003c/li\u003e\n\u003cli\u003eContainer isolation with resource limits (800MB RAM, 1 CPU per instance)\u003c/li\u003e\n\u003cli\u003eIP-based rate limiting on all public endpoints\u003c/li\u003e\n\u003cli\u003eNOWPayments webhook HMAC-SHA512 signature validation\u003c/li\u003e\n\u003cli\u003eMulti-layer prompt injection defenses in the agent\u0026rsquo;s configuration\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-02-12T00:00:00Z","id":"https://ankush.one/projects/2026.pumpmyclaw/","summary":"A platform for deploying autonomous AI trading agents on Solana and Monad with self-reinforcing token economies.","tags":["projects","solana","defi","ai-agents","cloudflare"],"title":"PumpMyClaw","url":"https://ankush.one/projects/2026.pumpmyclaw/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://web.archive.org/web/20260227180613/https://mypokemonscanner.com/\"\u003eWebsite\u003c/a\u003e (archived snapshot)\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMyPokemonScanner is a Privacy-first, offline-capable Pokemon card scanner that runs 100% in your browser using modern Web AI.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIt combines YOLO11 for object detection and MobileCLIP for image embedding to identify cards instantly, without uploading any images to a server.\u003c/p\u003e\n\u003cfigure class=\"doc-figure doc-figure--portrait\"\u003e\n  \u003cvideo autoplay loop muted playsinline poster=\"https://ankush.one/media/mypokemonscanner-poster.webp\" width=\"332\" height=\"670\" aria-label=\"Scanner Demo\"\u003e\n    \u003csource src=\"https://ankush.one/media/mypokemonscanner.mp4\" type=\"video/mp4\"\u003e\n  \u003c/video\u003e\n\u003c/figure\u003e\n\u003ch2 id=\"features\"\u003eFeatures\u003ca class=\"heading-anchor\" href=\"#features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eInstant Detection:\u003c/strong\u003e Uses YOLO11n to detect cards in real-time (~10ms)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClient-Side Recognition:\u003c/strong\u003e identifies cards using MobileCLIP-S2 embeddings directly in the browser\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePrivacy First:\u003c/strong\u003e No images are ever uploaded to a server\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOffline Capable:\u003c/strong\u003e Works fully offline after the initial load\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eComprehensive Database:\u003c/strong\u003e Merged data from TCGdex and LimitlessTCG for English and Japanese cards\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePrice Checking:\u003c/strong\u003e Fetches real-time pricing from TCGplayer\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"tech-stack\"\u003eTech Stack\u003ca class=\"heading-anchor\" href=\"#tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eYOLO11n\u003c/strong\u003e for object detection (via ONNX Runtime)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMobileCLIP-S2\u003c/strong\u003e for image embeddings\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTensorFlow.js \u0026amp; ONNX Runtime Web\u003c/strong\u003e for inference\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWebGPU \u0026amp; WebAssembly\u003c/strong\u003e for hardware acceleration\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePocketBase\u003c/strong\u003e for backend data sync (prices)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVite \u0026amp; React\u003c/strong\u003e for the frontend\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ca href=\"https://ankush.one/blogs/pokemon-scanner/\"\u003eRead the full story on how I built this\u003c/a\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-01-19T00:00:00Z","id":"https://ankush.one/projects/2026.pokemon-scanner/","summary":"A lightning fast, fully client-side Pokemon card scanner using YOLO11, MobileCLIP, and WebAssembly.","tags":["projects","machine-learning","web"],"title":"MyPokemonScanner","url":"https://ankush.one/projects/2026.pokemon-scanner/"},{"content_html":"\u003cfigure class=\"doc-figure doc-figure--portrait\"\u003e\n  \u003cvideo autoplay loop muted playsinline poster=\"https://ankush.one/media/mypokemonscanner-poster.webp\" width=\"332\" height=\"670\" aria-label=\"Scanner Demo\"\u003e\n    \u003csource src=\"https://ankush.one/media/mypokemonscanner.mp4\" type=\"video/mp4\"\u003e\n  \u003c/video\u003e\n  \u003cfigcaption\u003eIt's fast. Really fast.\u003c/figcaption\u003e\n\u003c/figure\u003e\n\u003cp\u003eI have always been a huge fan of Pokemon. I have played almost every game from GBC to HGSS, and recently played a few 3DS games when I got my hands on a \u0026rsquo;new\u0026rsquo; 3DS in Thailand.\u003c/p\u003e\n\u003cp\u003eI also started collecting Pokemon cards last year and as of writing this, I have around 350-400 cards. I couldnt look at them just sitting tightly in a tin and a cardbox, so I bought a pack of penny sleeves and a good binder (without the O-rings) and put all the shiny sparkly holo cards with cool looking artwork at the front, following with reverse holows and other normal cards that I like, and yanked the rest of the bulk back into the tin :)\u003c/p\u003e\n\u003cp\u003eAt this point I have a bunch of cool art cards, in English, Thai, Japanese, Korean and Chinese, which look soo cool and valuable. I then tried a bunch of apps that scan pokemon cards and tell you more about it, but they all had their limitations or were straightup paywalled, that too by a large margin (not even an amount that one just spends without thinking much), so I did what every engineer would do at that point\u003c/p\u003e\n\u003ch2 id=\"i-hacked-my-own-pokemon-card-scanner-insert-maniacal-laughter\"\u003eI hacked my own Pokemon card scanner \u003cem\u003e\u0026lt;insert maniacal laughter\u0026gt;\u003c/em\u003e\u003ca class=\"heading-anchor\" href=\"#i-hacked-my-own-pokemon-card-scanner-insert-maniacal-laughter\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eTLDR; scraped ~20K card data and images from \u003ca href=\"https://tcgdex.dev\" data-external\u003eTCGdex\u003c/a\u003e, generated embeddings using \u003ca href=\"https://openai.com/index/clip/\" data-external\u003eOpenAI CLIP\u003c/a\u003e model, and used \u003ca href=\"https://opencv.org/\" data-external\u003eOpenCV\u003c/a\u003e to very crudely find a rectangle like region in the image, and crop it out to search in the embeddings.\u003c/p\u003e\n\u003cp\u003eWhile the initial setup worked flawlessly on my mac, it was just a hacky taped together REST API that took an image and returned the card name, card id, image url, and had a bunch of issues I would have to deal with if I wanted to launch this as an app.\u003c/p\u003e\n\u003cp\u003eThe image embeddings took around 7 minutes using \u003ca href=\"https://developer.apple.com/metal/pytorch/\" data-external\u003eMPS\u003c/a\u003e on my Mac, which is fine, because inference is usually much quicker (~800ms at that time on mac), when I uploaded the embeddings to my VPS without a GPU and served the API, it took multiple seconds to identify the card from an image, which was disastrous. Continuing to use the VPS for reverse image search was the worst thing I could have done.\u003c/p\u003e\n\u003cp\u003eI decided I will move everything client side and run everything in the browser- card detection, cropping, recognition, data fetching, everything, and started reading more about different CLIP embedding models- SigLIP, OpenCLIP, MobileCLIP, ViTs, etc etc\u0026hellip; and tried every single one of them. I also learnt about Model Quantization, where you reduce floating point accuracy to reduce the size of model (with some loss in accuracy) and make it run faster with less memory and power. Int8 quantization worked good enough for SigLIP.\u003c/p\u003e\n\u003cp\u003eI had already setup a workflow in the process, to generate embeddings and run it on the web using TF.js + onnx web runtime. I used SigLIP for a day or two, deployed a PWA and started messing around with it on my phone, and ended up switching it with MobileClip-S2, which had better results and accuracy recognising pokemon cards. There was some hurdles with quantization, Int8 always broke the exported onnx somehow, but FP16 quntization was fine and I ended up using that (whatever works well right?)\u003c/p\u003e\n\u003cp\u003eI also realised that all this time I had been using WebGL/wasm for inference on the web, and added WebGPU backend for Tfjs, which immedieately made inference much much faster.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSidequest: YOLO model to specifically detect pokemon cards\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eWhile opencv rectangle countour thingie to crop out cards works okay for still images, it failed drastically for video streams, which I was trying to use, so I ended up training a YOLO11n model on a pokemon cards dataset I found online, and it worked pretty well and really fast, while also being around 5mb for the entire model\u003c/p\u003e\n\n\u003c/blockquote\u003e\u003cp\u003eBy now the inference time on desktop averaged around 10ms for detection using yolo11n and 50-80ms for recognition using MobileClip-S2 and searching the embeddings, with similar times on mobile for yolo and usually a few hundred ms for recognition, but never more than a second if you have a good phone (not guaranteeing anything though)\u003c/p\u003e\n\u003ch3 id=\"cool-now-i-have-a-working-scanner-which-is-fast-runs-client-side\"\u003eCool, now I have a working scanner which is fast runs client side\u003ca class=\"heading-anchor\" href=\"#cool-now-i-have-a-working-scanner-which-is-fast-runs-client-side\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWhat next? Data. TCGplayer has an extensive database on English cards, but Japanese and other languages? next to none \u003cimg src=\"https://ankush.one/emojis/harold.jpg\" alt=\"\" loading=\"lazy\" decoding=\"async\"\n    style=\"display:inline; padding:0px; margin:0px; border-radius:2px; border:none; max-height:22px; vertical-align:middle\" /\u003e So I had to look for other sources with both card data and images and ended up scraping \u003ca href=\"https://limitlesstcg.com\" data-external\u003elimitlesstcg\u003c/a\u003e for both English and Japanese. I also ended up using the \u003ca href=\"https://www.tcgplayer.com/\" data-external\u003eTCGplayer\u003c/a\u003e apis to fetch pricing info for all cards and sets, including variations like card condition- near mint, lightly played, damaged, card type- normal, holo, reverse holo.\u003c/p\u003e\n\u003cp\u003eNow I have a codebase which can:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003edownload english card data from tcgdex\u003c/li\u003e\n\u003cli\u003edownload japanese card data from limitlesstcg\u003c/li\u003e\n\u003cli\u003emerge both to fill in missing cards and their info\u003c/li\u003e\n\u003cli\u003efetch whatever price info it can find for our merged cards db\u003c/li\u003e\n\u003cli\u003ehas apis to search \u0026amp; query our data\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"package-and-ship\"\u003ePackage and ship\u003ca class=\"heading-anchor\" href=\"#package-and-ship\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eSetup a cron on my VPS to update my card data every 3 days, cleaned up the codebase, wrote QOL scripts, built a nice little frontend hosted it on mypokemonscanner.com and we\u0026rsquo;re done building our card scanner (•̀ᴗ•́ )و\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eUpdate: mypokemonscanner.com is no longer online, but the Wayback Machine has an \u003ca href=\"https://web.archive.org/web/20260227180613/https://mypokemonscanner.com/\" data-external\u003earchived snapshot\u003c/a\u003e. The \u003ca href=\"https://ankush.one/projects/2026.pokemon-scanner/\"\u003eproject page\u003c/a\u003e has the demo and the tech stack.\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-01-18T00:00:00Z","id":"https://ankush.one/blogs/pokemon-scanner/","summary":"A story about building a lightning fast, offline-capable Pokemon card scanner using YOLO11, MobileCLIP, and WebAssembly.","tags":["blogs","machine-learning","web"],"title":"How I built a Pokemon TCG scanner that runs 100% in your browser","url":"https://ankush.one/blogs/pokemon-scanner/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/webanimate\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://github.com/ankushKun/webanimate/releases\"\u003eDownload Extension\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWebAnimate is a Chrome extension that lets you inspect elements on web pages, apply animations, and record them using a timeline-based editor.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eBuilt with React, PixiJS, and FFmpeg for video processing, it makes creating web animations simple and intuitive.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\n\u003cvideo controls preload=\"metadata\"\u003e\n  \u003csource src=\"https://github.com/user-attachments/assets/520cded8-32e8-475a-84b4-69ceef17727d\" type=\"video/mp4\"\u003e\n  Your browser does not support the video tag.\n\u003c/video\u003e\n\u003c/figure\u003e\n\u003ch2 id=\"features\"\u003eFeatures\u003ca class=\"heading-anchor\" href=\"#features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eElement Inspection:\u003c/strong\u003e Click to select any element on a web page\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTimeline Editor:\u003c/strong\u003e Create complex animations with an intuitive timeline interface\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAnimation Playback:\u003c/strong\u003e Preview animations before recording\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVideo Export:\u003c/strong\u003e Record and download animations as video files using FFmpeg\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"tech-stack\"\u003eTech Stack\u003ca class=\"heading-anchor\" href=\"#tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReact\u003c/strong\u003e for the UI\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePixiJS\u003c/strong\u003e for the timeline editor\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFFmpeg\u003c/strong\u003e for video processing and export\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eChrome Extension APIs\u003c/strong\u003e for browser integration\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"installation\"\u003eInstallation\u003ca class=\"heading-anchor\" href=\"#installation\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eDownload the latest \u003ccode\u003ewebanimate.zip\u003c/code\u003e from \u003ca href=\"https://github.com/ankushKun/webanimate/releases\" data-external\u003ereleases\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eExtract the contents\u003c/li\u003e\n\u003cli\u003eOpen \u003ccode\u003echrome://extensions\u003c/code\u003e in Chrome\u003c/li\u003e\n\u003cli\u003eEnable \u0026ldquo;Developer mode\u0026rdquo;\u003c/li\u003e\n\u003cli\u003eClick \u0026ldquo;Load unpacked\u0026rdquo; and select the extracted folder\u003c/li\u003e\n\u003c/ol\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-01-06T00:00:00Z","id":"https://ankush.one/projects/2026.webanimate/","summary":"A Chrome extension for creating and recording web animations with a timeline-based editor.","tags":["projects","web","developer-tools"],"title":"WebAnimate","url":"https://ankush.one/projects/2026.webanimate/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/commentkit\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://web.archive.org/web/20260120162554/https://commentkit.ankush.one/\"\u003eCommentKit site\u003c/a\u003e (archived snapshot)\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve written a handful of blogs now, and wanted to add a comment section. My first thought was to use Disqus — I\u0026rsquo;d seen it everywhere. But the free tier has ads, and the paid plans felt like overkill for a personal blog.\u003c/p\u003e\n\u003cp\u003eI looked at alternatives like Commento, Giscus, etc etc, but none really fit what I wanted.\u003c/p\u003e\n\u003cp\u003eSo I built my own: \u003cstrong\u003eCommentKit\u003c/strong\u003e.\u003c/p\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/98HBWGddT6o\" data-yt=\"98HBWGddT6o\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/98HBWGddT6o/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e\n\u003ch2 id=\"the-two-line-integration\"\u003eThe Two-Line Integration\u003ca class=\"heading-anchor\" href=\"#the-two-line-integration\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAfter you create and verify a site on commentkits dashboard, this is all it takes to add comments to your website:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- Include the CommentKit bundle --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esrc\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://commentkit.ankush.one/bundle.js\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- Add a div where you want comments --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e \u003cspan class=\"na\"\u003edata-commentkit\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cem\u003eUpdate: the hosted instance at commentkit.ankush.one is offline now, so this snippet only works if you self-host it.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s it. No complex configuration, no API keys in your frontend code, no build steps. The widget automatically:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDetects the current page URL\u003c/li\u003e\n\u003cli\u003eCreates a page entry if it doesn\u0026rsquo;t exist\u003c/li\u003e\n\u003cli\u003eLoads existing comments\u003c/li\u003e\n\u003cli\u003eHandles user authentication\u003c/li\u003e\n\u003cli\u003eSupports nested replies\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"architecture-overview\"\u003eArchitecture Overview\u003ca class=\"heading-anchor\" href=\"#architecture-overview\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eCommentKit has two main parts: the \u003cstrong\u003eembeddable widget\u003c/strong\u003e that runs on your website, and a \u003cstrong\u003ebackend API\u003c/strong\u003e that handles storage and authentication.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e┌─────────────────┐     ┌──────────────────────┐     ┌─────────────┐\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│   Your Website  │◄───►│  Cloudflare Workers  │◄───►│ D1 Database │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│   (bundle.js)   │     │      (API Layer)     │     │  (SQLite)   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└─────────────────┘     └──────────────────────┘     └─────────────┘\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"embeddable-widget\"\u003eEmbeddable Widget\u003ca class=\"heading-anchor\" href=\"#embeddable-widget\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eWhen you add \u003ccode\u003e\u0026lt;script src=\u0026quot;https://commentkit.ankush.one/bundle.js\u0026quot;\u0026gt;\u003c/code\u003e to your page, here\u0026rsquo;s what happens:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eThe script finds all \u003ccode\u003e\u0026lt;div data-commentkit\u0026gt;\u003c/code\u003e elements\u003c/li\u003e\n\u003cli\u003eIt creates a hidden iframe pointing to the CommentKit domain\u003c/li\u003e\n\u003cli\u003eThe iframe handles authentication and loads comments\u003c/li\u003e\n\u003cli\u003eComments are rendered in place of the div on the host website\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe tricky part is authentication. The widget runs on \u003cem\u003eyour\u003c/em\u003e website (\u003ccode\u003emyblog.com\u003c/code\u003e), but auth tokens need to be stored on the \u003cem\u003eCommentKit\u003c/em\u003e domain. Cookies don\u0026rsquo;t work cross-domain, so I use an iframe with \u003ccode\u003epostMessage\u003c/code\u003e instead.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eYour Website (bundle.js)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ├── Creates hidden iframe → commentkit.ankush.one/widget\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ├── Sends messages via postMessage:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    │     { action: \u0026#39;loadComments\u0026#39;, domain: \u0026#39;myblog.com\u0026#39;, pageId: \u0026#39;/post\u0026#39; }\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    └── Receives responses:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          { action: \u0026#39;commentsLoaded\u0026#39;, data: [...] }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe iframe acts as a secure bridge. All API calls happen inside the iframe, authenticating via HttpOnly cookies that are completely inaccessible to JavaScript — never exposed to the parent website or even the iframe itself.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003ebundle.js\u003c/code\u003e is a single js file containing all the vanilla code needed to talk to the iframe and render comments.\u003c/p\u003e\n\u003cp\u003eThe final bundle is around 70KB minified (12KB gzipped), self-contained with no external dependencies at runtime.\u003c/p\u003e\n\u003ch2 id=\"backend\"\u003eBackend\u003ca class=\"heading-anchor\" href=\"#backend\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe API runs on \u003ca href=\"https://www.cloudflare.com/products/workers/\" data-external\u003eCloudflare Workers\u003c/a\u003e using \u003ca href=\"https://hono.dev/\" data-external\u003eHono.js\u003c/a\u003e as the backend framework.\u003c/p\u003e\n\u003cp\u003eData is stored in \u003ca href=\"https://developers.cloudflare.com/d1/\" data-external\u003eCloudflare D1\u003c/a\u003e — SQLite at the edge. The schema has tables for sites, pages, users, comments, reactions, and sessions.\u003c/p\u003e\n\u003cp\u003eAuthentication uses magic links via \u003ca href=\"https://resend.com/\" data-external\u003eResend\u003c/a\u003e. When you log in, you enter your email, get a link, click it, and you\u0026rsquo;re in. No passwords to store or leak.\u003c/p\u003e\n\u003cp\u003eSecurity is handled through multiple layers: HttpOnly cookies for sessions (so tokens can\u0026rsquo;t be stolen via XSS), HMAC-signed CSRF tokens for all write operations, strict input sanitization that strips all HTML, and Content Security Policy headers that block inline scripts. Everything user-submitted is treated as plain text, not code\u003c/p\u003e\n\u003ch2 id=\"dashboard\"\u003eDashboard\u003ca class=\"heading-anchor\" href=\"#dashboard\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eSite owners get access to a clean, functional dashboard:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eOverview Tab\u003c/strong\u003e: Quick stats, recent activity, and analytics charts\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSites Tab\u003c/strong\u003e: Manage multiple sites, view comments by page, moderate content\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSettings Tab\u003c/strong\u003e: Account settings, display name, and profile management\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe dashboard is built with React and styled with a modern, minimal aesthetic using Tailwind CSS and Shadcn/ui components.\u003c/p\u003e\n\u003ch2 id=\"why-cloudflare-workers\"\u003eWhy Cloudflare Workers?\u003ca class=\"heading-anchor\" href=\"#why-cloudflare-workers\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eI chose Cloudflare Workers for several reasons:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eGlobal edge deployment\u003c/strong\u003e: Your API is served from the nearest datacenter to each user\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo cold starts\u003c/strong\u003e: Workers are always warm and respond in milliseconds\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eD1 integration\u003c/strong\u003e: SQLite database with global replication, no separate database service needed\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGenerous free tier\u003c/strong\u003e: 100,000 requests/day for free\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSimple deployment\u003c/strong\u003e: \u003ccode\u003ewrangler deploy\u003c/code\u003e and you\u0026rsquo;re live\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"now-what\"\u003eNow What?\u003ca class=\"heading-anchor\" href=\"#now-what\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThese were the improvements I had planned:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e \u003cstrong\u003eEmail notifications\u003c/strong\u003e when someone replies to your comment\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e \u003cstrong\u003eMarkdown support\u003c/strong\u003e in comments\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e \u003cstrong\u003eSpam detection\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e \u003cstrong\u003eTheming\u003c/strong\u003e for customizing the widget appearance\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e \u003cstrong\u003eWebhooks\u003c/strong\u003e for integrating with other services\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"contributing\"\u003eContributing\u003ca class=\"heading-anchor\" href=\"#contributing\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eCommentKit is open to contributions! Whether it\u0026rsquo;s bug fixes, new features, or documentation improvements, check out the \u003ca href=\"https://github.com/ankushKun/commentkit/blob/main/CONTRIBUTING.md\" data-external\u003eCONTRIBUTING.md\u003c/a\u003e for guidelines.\u003c/p\u003e\n\u003ch2 id=\"try-it-out\"\u003eTry It Out\u003ca class=\"heading-anchor\" href=\"#try-it-out\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe hosted instance at commentkit.ankush.one is no longer online, so there is no live demo to try right now. The code is still on \u003ca href=\"https://github.com/ankushKun/commentkit\" data-external\u003eGitHub\u003c/a\u003e if you want to read it or host it yourself, and a star would be much appreciated!\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2026-01-01T00:00:00Z","id":"https://ankush.one/projects/2026.commentkit/","image":"https://ankush.one/media/commentkit-og.jpg","summary":"A tiny drop-in comment system for static sites. Paste 2 lines of code and you get threads, likes, and moderation.","tags":["projects","cloudflare","developer-tools","web"],"title":"CommentKit","url":"https://ankush.one/projects/2026.commentkit/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://gist.github.com/ankushKun/653b7eeaf6d4279ef97f1ab6f709110a\"\u003eFull Source Code\u003c/a\u003e\u003c/p\u003e\n\u003cfigure class=\"doc-figure doc-demo\"\u003e\n  \u003cdiv class=\"doc-demo-frame\"\u003e\n    \u003ciframe src=\"https://7jiiceni3iqjhaxe44sesz2qjd5q7m5l3kumbu2gzmoerv5ifi3a.arweave.net/-lCBEajaIJOC5OckSWdQSPsPs6vaqMDTRsscSNeoKjY?\" title=\"DOOM-style renderer demo\" loading=\"lazy\"\u003e\u003c/iframe\u003e\n    \u003cdiv class=\"doc-demo-overlay\" aria-hidden=\"true\"\u003eClick to play\u003c/div\u003e\n  \u003c/div\u003e\n  \u003cfigcaption\u003eMove with WASD\u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eRecently I have been adding games to my portfolio and DOOM is one of them. But instead of just embedding the original game, I wanted to understand how it actually worked. So I went down the rabbit hole (a second time) and built a more whacky version of the doom renderer from scratch and in HTML canvas, no extra dependencies.\u003c/p\u003e\n\u003cp\u003eTurns out, classic games like \u003cstrong\u003eDOOM\u003c/strong\u003e didn\u0026rsquo;t do real 3D at all. They used something called \u003cstrong\u003e2.5D rendering\u003c/strong\u003e,  basically faking 3D with clever 2D math. And it ran on hardware way worse (100x or more times worse) than what you are reading this on.\u003c/p\u003e\n\u003cp\u003eToday I\u0026rsquo;ll break down how I built my own DOOM-style renderer. No fancy tools required, just a code editor and a web browser.\u003c/p\u003e\n\u003ch2 id=\"is-3d-actually-3d\"\u003eIs 3D actually 3D?\u003ca class=\"heading-anchor\" href=\"#is-3d-actually-3d\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAt its core, 3D rendering is basically \u003cstrong\u003e\u0026ldquo;If we have a 3D world and a camera, what does the camera see?\u0026rdquo;\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThis process generally involves defining the world (objects), placing the camera (viewer), projecting those 3D coordinates onto a flat 2D screen, and finally filling the screen with the correct colors (drawing pixels).\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   3D World          Camera          2D Screen\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ┌─────────┐                      ┌─────────┐\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  │ ▓▓▓     │     ────────────►    │   ▓▓    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  │    ▓▓▓  │     (projection)     │  ▓▓▓▓   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  │  ▓▓     │                      │   ▓▓    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  └─────────┘                      └─────────┘\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"representing-space-with-a-top-down-2d-map\"\u003eRepresenting Space with a top down 2D Map\u003ca class=\"heading-anchor\" href=\"#representing-space-with-a-top-down-2d-map\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe simplest way to define a world map is with a \u003cstrong\u003egrid map\u003c/strong\u003e. We can represent this as an array where each cell is either a wall (\u003ccode\u003e1\u003c/code\u003e) or empty space (\u003ccode\u003e0\u003c/code\u003e).\u003c/p\u003e\n\u003caside class=\"doc-callout doc-callout--tip\" aria-label=\"Future Scope\"\u003e\n\u003cp class=\"doc-callout-title\"\u003e\u003cspan class=\"doc-callout-icon\"\u003e\u003csvg class=\"ui-glyph ui-glyph-tip\" width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"\u003e\u003cpath d=\"M5.5 10.5c-1-.9-1.75-2-1.75-3.5a4.25 4.25 0 0 1 8.5 0c0 1.5-.75 2.6-1.75 3.5v1.5h-5zM6.25 14h3.5\"/\u003e\u003c/svg\u003e\u003c/span\u003e\u003cspan\u003eFuture Scope\u003c/span\u003e\u003c/p\u003e\n\u003cdiv class=\"doc-callout-body\"\u003e\n\u003cp\u003ewe can implement an \u003ccode\u003eenum\u003c/code\u003e and use different types of walls or place objects with different number instead of just 0 and 1.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eMAP_WIDTH\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e11\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Number of rows\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eMAP_HEIGHT\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e17\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Number of columns\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eTILE_SIZE\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e25\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Size of each tile in world units, increasing this makes the map look more spread out\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eWALL_HEIGHT\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e40\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eMAP\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// ... more rows\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is effectively a \u003cstrong\u003etop-down view\u003c/strong\u003e of the world.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■■■■■■■■■■■\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■         ■\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■         ■\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■   ■   ■ ■    ← Interior pillars\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■         ■\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■         ■\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e■■■■■■■■■■■\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"why-2d-maps-for-3d\"\u003eWhy 2D Maps for 3D?\u003ca class=\"heading-anchor\" href=\"#why-2d-maps-for-3d\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThis is one of the techniques games like DOOM used to achieve their 3D effect. The world is fundamentally \u003cstrong\u003e2D with height\u003c/strong\u003e. The X and Y coordinates define horizontal position, while height is just a property of the walls rather than a full 3D coordinate.\u003c/p\u003e\n\u003ch2 id=\"player-state\"\u003ePlayer State\u003ca class=\"heading-anchor\" href=\"#player-state\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe player (our camera) is defined by three simple values: an \u003ccode\u003ex\u003c/code\u003e and \u003ccode\u003ey\u003c/code\u003e position in the world, and an \u003ccode\u003eangle\u003c/code\u003e representing the direction they are facing.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e50\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// World X position\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ey\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e50\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e     \u003cspan class=\"c1\"\u003e// World Y position  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eangle\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// Direction facing\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"coordinate-system\"\u003eCoordinate System\u003ca class=\"heading-anchor\" href=\"#coordinate-system\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWe use a standard coordinate system where angle 0° looks toward positive Y (down on the map) and 90° looks toward positive X (right on the map). All angles are in degrees (0-359).\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e         180°                (0,0) ────────► X\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          ▲                   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          │                   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          │                   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e270° ◄────┼────► 90°          │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          │                   ▼\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          ▼                   Y\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          0°\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor performance, we pre-calculate degree-to-radian conversions so we don\u0026rsquo;t have to constantly do the math during the render loop.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eRAD\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ePI\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e180\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eDEG_TO_RAD\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003elength\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e360\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eRAD\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage: instead of calculating Math.sin(angle * Math.PI / 180)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// We just do: Math.sin(DEG_TO_RAD[angle])\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"transforming-world-to-screen\"\u003eTransforming World to Screen\u003ca class=\"heading-anchor\" href=\"#transforming-world-to-screen\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThis is where the 3D illusion happens! We need to transform world coordinates to screen coordinates in two steps.\u003c/p\u003e\n\u003ch3 id=\"step-1-translate-move-world-relative-to-player\"\u003eStep 1: Translate (Move world relative to player)\u003ca class=\"heading-anchor\" href=\"#step-1-translate-move-world-relative-to-player\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eFirst, we move everything so the player is at the origin \u003ccode\u003e(0, 0)\u003c/code\u003e. We simply subtract the player\u0026rsquo;s position from the wall\u0026rsquo;s position.\u003c/p\u003e\n\u003cp\u003eSo instead of actually moving the player, we are moving the world relative to the player. From the players perspective, it seems they are moving.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003erelX\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallX\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ex\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003erelY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallY\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ey\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"step-2-rotate-align-world-objects-with-viewing-angle\"\u003eStep 2: Rotate (Align world objects with viewing angle)\u003ca class=\"heading-anchor\" href=\"#step-2-rotate-align-world-objects-with-viewing-angle\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWe rotate the worlds objects relative to the player\u0026rsquo;s viewing angle.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eps\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eDEG_TO_RAD\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eangle\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// sin of player angle\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003epc\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecos\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eDEG_TO_RAD\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eangle\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// cos of player angle\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Rotation matrix application\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etx\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003erelX\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003epc\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003erelY\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eps\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// transformed X (left/right)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ety\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003erelY\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003epc\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003erelX\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eps\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// transformed Y (depth/distance)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eWhat this means:\u003c/strong\u003e \u003ccode\u003etx\u003c/code\u003e is how far left or right the point is from center of view, and \u003ccode\u003ety\u003c/code\u003e is how far forward the point is (the \u003cstrong\u003edepth\u003c/strong\u003e).\u003c/p\u003e\n\u003ch2 id=\"projecting-3d-data-onto-2d-screen\"\u003eProjecting 3D data onto 2D screen\u003ca class=\"heading-anchor\" href=\"#projecting-3d-data-onto-2d-screen\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe furthest the image is, the smaller it should look. Mathematically, this just means dividing by depth (\u003ccode\u003ety\u003c/code\u003e).\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003efov\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e90\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Field of view scalar\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Screen X (horizontal position)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Add screen.width/2 to center it\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003escreenX\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etx\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003efov\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003ety\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003escreen\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ewidth\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Screen Y (height)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Taller walls or closer walls appear larger\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003escreenHeight\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eWALL_HEIGHT\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003efov\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003ety\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"translate-walls\"\u003eTranslate walls\u003ca class=\"heading-anchor\" href=\"#translate-walls\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eTo draw a wall, we need four points on the screen: Top-Left, Top-Right, Bottom-Left, and Bottom-Right.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003eproject\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ez\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// z is height (0 for floor, WALL_HEIGHT for ceiling)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// ... translation logic ...\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// ... rotation logic ...\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// Projection\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003escreenX\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etx\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003efov\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003ety\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003escreen\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ewidth\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003escreenY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ez\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003efov\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003ety\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003escreen\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eheight\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003escreenX\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ey\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003escreenY\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWe project the two ends of a wall segment to get their screen coordinates, then fill the polygon between them using \u003ccode\u003ectx.beginPath()\u003c/code\u003e and \u003ccode\u003ectx.fill()\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"sort-the-walls-and-finally-draw-them-on-canvas\"\u003eSort the walls and finally draw them on canvas\u003ca class=\"heading-anchor\" href=\"#sort-the-walls-and-finally-draw-them-on-canvas\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eIf we just draw walls in any order, distant walls might be drawn \u003cem\u003eon top\u003c/em\u003e of close walls. That would look weird, so we first sort the walls based on their distance from the player.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe Painter\u0026rsquo;s Algorithm:\u003c/strong\u003e Draw background objects first, then foreground objects over them.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003edraw3D\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallsWithDistance\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ewalls\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ewall\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Calculate distance to player\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edist1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003edx1\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003edx1\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003edy1\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003edy1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edist2\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003edx2\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003edx2\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003edy2\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003edy2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... calculate weighted distance ...\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ewall\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003edistance\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eweightedDist\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// Sort far to near\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ewallsWithDistance\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esort\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003edistance\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003edistance\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// Draw\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ewallsWithDistance\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eforEach\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003edrawWall\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ewall\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"why-the-weighted-distance\"\u003eWhy the Weighted Distance?\u003ca class=\"heading-anchor\" href=\"#why-the-weighted-distance\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eSimple closest-point distance can fail for angled walls. The weighted average considers the closest point (most important for occlusion), the midpoint, and the farthest point to handle edge cases gracefully.\u003c/p\u003e\n\u003ch2 id=\"clipping-dealing-with-edge-cases\"\u003eClipping: Dealing with Edge Cases\u003ca class=\"heading-anchor\" href=\"#clipping-dealing-with-edge-cases\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ch3 id=\"the-problem-behind-the-camera\"\u003eThe Problem: Behind the Camera\u003ca class=\"heading-anchor\" href=\"#the-problem-behind-the-camera\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWhen a wall is partially behind the player, the math breaks! Computing \u003ccode\u003escreenX = tx * fov / ty\u003c/code\u003e when \u003ccode\u003ety\u003c/code\u003e is negative or zero creates division by zero or negative numbers, resulting in visual glitches.\u003c/p\u003e\n\u003ch3 id=\"near-plane-clipping\"\u003eNear-Plane Clipping\u003ca class=\"heading-anchor\" href=\"#near-plane-clipping\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWe define a \u003cstrong\u003enear plane\u003c/strong\u003e \u0026ndash; a minimum distance. Anything closer gets clipped.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eNEAR_PLANE\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// If endpoint is behind us, move it to the near plane\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecy1\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eNEAR_PLANE\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eNEAR_PLANE\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecy1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecy2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecy1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Interpolation factor\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ecx1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ecx1\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecx2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecx1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// New X at near plane\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ecy1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eNEAR_PLANE\u003c/span\u003e             \u003cspan class=\"c1\"\u003e// Clamp to near plane\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"screen-space-clipping\"\u003eScreen-Space Clipping\u003ca class=\"heading-anchor\" href=\"#screen-space-clipping\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWalls extending off-screen are also clipped to prevent wasted drawing.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Clip to left edge (x = 0)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esx1\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003esx1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esx2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003esx1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ewallTop1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallTop1\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ewallTop2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallTop1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ewallBot1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallBot1\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ewallBot2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ewallBot1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003esx1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"movement--controls\"\u003eMovement \u0026amp; Controls\u003ca class=\"heading-anchor\" href=\"#movement--controls\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ch3 id=\"wasd-movement\"\u003eWASD Movement\u003ca class=\"heading-anchor\" href=\"#wasd-movement\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eMovement is relative to where the player is looking.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003emovePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// Forward direction based on angle\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edx\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eDEG_TO_RAD\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eangle\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eMOVE_SPEED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edy\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecos\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eDEG_TO_RAD\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eangle\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eMOVE_SPEED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// ... apply movement based on keys ...\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// Normalize diagonal movement for consistent speed\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003emagnitude\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esqrt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003emoveX\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003emoveY\u003c/span\u003e\u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003emagnitude\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003emoveX\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003emoveX\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003emagnitude\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eMOVE_SPEED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003emoveY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003emoveY\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003emagnitude\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eMOVE_SPEED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"mouse-look-with-pointer-lock\"\u003eMouse Look with Pointer Lock\u003ca class=\"heading-anchor\" href=\"#mouse-look-with-pointer-lock\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eFor immersive FPS-style controls, we lock the mouse cursor. \u003cstrong\u003ePointer lock\u003c/strong\u003e hides the cursor and provides raw mouse movement data.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eaddEventListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;mousemove\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eevent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eisMouseLocked\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003emouseMove\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eevent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emovementX\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edegreeChange\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003emouseMove\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003escreen\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ewidth\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e360\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eangle\u003c/span\u003e \u003cspan class=\"o\"\u003e-=\u003c/span\u003e \u003cspan class=\"nx\"\u003edegreeChange\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"wall-collision-detection\"\u003eWall Collision Detection\u003ca class=\"heading-anchor\" href=\"#wall-collision-detection\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003ch3 id=\"grid-based-collision\"\u003eGrid-Based Collision\u003ca class=\"heading-anchor\" href=\"#grid-based-collision\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eSince our world is grid-based, collision is simple. Just check if the players future calculated position is a wall and conditionally allow movement.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003eisWall\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworldX\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eworldY\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etileX\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efloor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworldX\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003eTILE_SIZE\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etileY\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efloor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworldY\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"nx\"\u003eTILE_SIZE\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003egetMapTile\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etileX\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etileY\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 1 is a wall in our implementation\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWe also give the player a \u003cstrong\u003eradius\u003c/strong\u003e and check all corners to ensure the player doesn\u0026rsquo;t clip into walls. If directly blocked, we try sliding along the wall. This creates smooth movement even when running into walls at angles.\u003c/p\u003e\n\u003ch2 id=\"putting-it-all-together\"\u003ePutting It All Together\u003ca class=\"heading-anchor\" href=\"#putting-it-all-together\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eEvery frame, we simply clear the screen, handle input, render the walls, and debug if needed.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003eloop\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eclearScreen\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// Draw floor and ceiling\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003emovePlayer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// Handle input\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003edraw3D\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// Render walls\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003edebug\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e         \u003cspan class=\"c1\"\u003e// Show debug info\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Run at 30 FPS\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003esetInterval\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eloop\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1000\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"what-next\"\u003eWhat Next?\u003ca class=\"heading-anchor\" href=\"#what-next\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eThe engine we built covers the fundamentals, but classic DOOM had much more: texture mapping, sprites, variable height sectors for different floor/ceiling heights, lighting, enemy AI, sound, guns, an actual game!\u003c/p\u003e\n\u003cp\u003eFeel free to extend this engine to add more features! \u0026lt;3\u003c/p\u003e\n\u003ch2 id=\"references\"\u003eReferences\u003ca class=\"heading-anchor\" href=\"#references\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://doomwiki.org/wiki/Rendering_engine\" data-external\u003eDOOM Wiki: Rendering Engine\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/watch?v=huMO4VQEwPc\" data-external\u003eLet\u0026rsquo;s program DOOM by 3DSage\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-12-24T00:00:00Z","id":"https://ankush.one/blogs/doom-style-renderer/","image":"https://ankush.one/media/doom-renderer-og.jpg","summary":"A deep dive into building a 3D renderer from scratch using JavaScript and HTML5 Canvas, inspired by the classic DOOM engine.","tags":["blogs","graphics","game-dev","web","tutorial"],"title":"Building a DOOM-Style 3D Renderer from Scratch in JavaScript","url":"https://ankush.one/blogs/doom-style-renderer/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://pixels.earth\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://github.com/ankushKun/pixels.earth\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003epixels.earth is a global, decentralized pixel canvas layered on top of a real-world map — where every pixel you place is an on-chain transaction.\u003c/strong\u003e\u003c/p\u003e\n\u003c!-- 🔗 **Try it:** https://pixels.earth --\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/4JhPDh8kBtM\" data-yt=\"4JhPDh8kBtM\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/4JhPDh8kBtM/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e\n\u003ch2 id=\"-what-is-pixelsearth\"\u003e🌍 What is pixels.earth?\u003ca class=\"heading-anchor\" href=\"#-what-is-pixelsearth\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA \u003cstrong\u003emassive, collaborative canvas\u003c/strong\u003e overlaid on a map of Earth\u003c/li\u003e\n\u003cli\u003eDraw \u003cstrong\u003eanywhere in the world\u003c/strong\u003e — your local neighborhood, famous landmarks, or unexplored territories\u003c/li\u003e\n\u003cli\u003eEvery pixel is an \u003cstrong\u003eon-chain transaction\u003c/strong\u003e on Solana\u003c/li\u003e\n\u003cli\u003eBuilt on \u003cstrong\u003eMagicBlock\u003c/strong\u003e for real-time, sub-second blockchain interactions\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-land-ownership-shards\"\u003e🏠 Land Ownership (Shards)\u003ca class=\"heading-anchor\" href=\"#-land-ownership-shards\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eThe world is divided into \u003cstrong\u003eshards\u003c/strong\u003e — regions you can unlock and own\u003c/li\u003e\n\u003cli\u003eUnlock a shard to \u003cstrong\u003eclaim ownership\u003c/strong\u003e of that piece of the canvas\u003c/li\u003e\n\u003cli\u003eEarn from activity on shards you own\u003c/li\u003e\n\u003cli\u003eA new model for \u003cstrong\u003edecentralized digital land ownership\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-tech-stack\"\u003e⚡ Tech Stack\u003ca class=\"heading-anchor\" href=\"#-tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSolana\u003c/strong\u003e for fast, low-cost on-chain transactions\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAnchor\u003c/strong\u003e for solana rust programs\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMagicBlock\u003c/strong\u003e for real-time multiplayer interactions and sub-millisecond latency\u003c/li\u003e\n\u003cli\u003eProgressive Web App (PWA) — works on desktop and mobile\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-why-it-matters\"\u003e🎯 Why It Matters\u003ca class=\"heading-anchor\" href=\"#-why-it-matters\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eCombines the viral appeal of collaborative pixel art (like r/place) with \u003cstrong\u003ereal-world geography\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eA live experiment in \u003cstrong\u003ehigh-throughput blockchain gaming\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eShows what\u0026rsquo;s possible when you build real-time multiplayer apps on Solana\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003cp\u003eDraw on Earth. Own the canvas. 🌎✨\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-12-15T00:00:00Z","id":"https://ankush.one/projects/2025.pixels-earth/","image":"https://ankush.one/media/projects/pixels-earth-cover.jpg","summary":"A decentralized, infinite pixel canvas where you can draw on Earth and earn from unlocked lands. Built on Solana MagicBlock.","tags":["projects","solana","web3"],"title":"pixels.earth","url":"https://ankush.one/projects/2025.pixels-earth/"},{"content_html":"\u003cp\u003eI frequently create developer tools and publish node packages on npmjs, but recently classic tokens got deprecated, granular tokens are limited to 90 days, require 2FA and a bunch of changes were introduced to improve security around publishing packages. More about that on \u003ca href=\"https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/\" data-external\u003ethis GitHub announcement\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eOne of the improvements was the introduction of \u003cstrong\u003e\u003ca href=\"https://openid.net/developers/how-connect-works/\" data-external\u003eOpenID Connect\u003c/a\u003e (OIDC)\u003c/strong\u003e, which allows developers to publish npm packages through GitHub Actions, without having to setup any kind of API key or access token.\u003c/p\u003e\n\u003cp\u003eToday we\u0026rsquo;re having a look at how you can implement it for your packages as well.\u003c/p\u003e\n\u003ch2 id=\"step-1-setup-a-node-package-or-skip-if-you-already-have-one\"\u003eStep 1: Setup a Node Package (or skip if you already have one)\u003ca class=\"heading-anchor\" href=\"#step-1-setup-a-node-package-or-skip-if-you-already-have-one\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAs an example, we\u0026rsquo;ll create a fresh package which just logs \u0026ldquo;hello world\u0026rdquo; to console.\u003c/p\u003e\n\u003caside class=\"doc-callout doc-callout--note\" aria-label=\"Note\"\u003e\n\u003cp class=\"doc-callout-title\"\u003e\u003cspan class=\"doc-callout-icon\"\u003e\u003csvg class=\"ui-glyph ui-glyph-note\" width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"\u003e\u003ccircle cx=\"8\" cy=\"8\" r=\"6\"/\u003e\u003cpath d=\"M8 7.25v4M8 4.75v.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\u003cspan\u003eNote\u003c/span\u003e\u003c/p\u003e\n\u003cdiv class=\"doc-callout-body\"\u003e\n\u003cp\u003eThis package is only meant as a demo for this blog. It is not recommended to create such packages for yourself — you should just setup OIDC on your existing packages :)\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir npm-oidc-demo \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nb\"\u003ecd\u003c/span\u003e npm-oidc-demo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm init -y\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etouch index.js\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eindex.js:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003ehelloWorld\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Hello World\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003epackage.json:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;npm-oidc-demo\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;version\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;1.0.1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;description\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;A simple hello world package\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;main\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;index.js\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;module\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;exports\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;./index.js\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;repository\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;git\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;url\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://github.com/ankushKun/npm-oidc-demo\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;keywords\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s2\"\u003e\u0026#34;npm\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s2\"\u003e\u0026#34;oidc\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e],\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;author\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;license\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;ISC\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003caside class=\"doc-callout doc-callout--note\" aria-label=\"Note\"\u003e\n\u003cp class=\"doc-callout-title\"\u003e\u003cspan class=\"doc-callout-icon\"\u003e\u003csvg class=\"ui-glyph ui-glyph-note\" width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"\u003e\u003ccircle cx=\"8\" cy=\"8\" r=\"6\"/\u003e\u003cpath d=\"M8 7.25v4M8 4.75v.01\"/\u003e\u003c/svg\u003e\u003c/span\u003e\u003cspan\u003eNote\u003c/span\u003e\u003c/p\u003e\n\u003cdiv class=\"doc-callout-body\"\u003e\n\u003cp\u003eAn already published package is required to use OIDC (might change in the future, who knows…)\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e\u003cp\u003eRun \u003ccode\u003enpm login\u003c/code\u003e and follow the steps to link the npm CLI with your npmjs account. When successful, run \u003ccode\u003enpm whoami\u003c/code\u003e and it should print your username.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReady for publishing:\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIf you ignored the previous note and still followed this guide to publish the demo package yourself, you might need to change your package name as I have already used “npm-oidc-demo”\u003c/p\u003e\n\n\u003c/blockquote\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ npm whoami\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eankushkun\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ npm publish\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm notice \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm notice 📦  npm-oidc-demo@1.0.0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+ npm-oidc-demo@1.0.0\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe package is now available at \u003ca href=\"https://www.npmjs.com/package/npm-oidc-demo\" data-external\u003e\u003ccode\u003ehttps://www.npmjs.com/package/npm-oidc-demo\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"step-2-update-oidc-details-on-package-settings\"\u003eStep 2: Update OIDC Details on Package Settings\u003ca class=\"heading-anchor\" href=\"#step-2-update-oidc-details-on-package-settings\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eHead over to the settings tab on your freshly published package and select \u003cstrong\u003eGitHub Actions\u003c/strong\u003e as the trusted publisher.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*s0fSghe8DIxQPw6K-OhozA.png\" alt=\"npmjs package settings tab\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003enpmjs package settings tab\u003c/figcaption\u003e\u003c/figure\u003e\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*4Fc2oJ3_Ijr8uLga0P4v_A.png\" alt=\"GitHub actions trusted publisher settings\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003eGitHub actions trusted publisher settings\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eI have already created a GitHub repo at \u003ca href=\"https://github.com/ankushKun/npm-oidc-demo\" data-external\u003e\u003ccode\u003eankushKun/npm-oidc-demo\u003c/code\u003e\u003c/a\u003e, now we just need to create a workflow file. Put this inside \u003ccode\u003e.github/workflows/publish.yml\u003c/code\u003e:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003epublish.yml:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ePublish to npm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eon\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erelease\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003etypes\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003epublished]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eworkflow_dispatch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ejobs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003epublish\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-latest\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003epermissions\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003econtents\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eread\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eid-token\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewrite\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003esteps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@v4\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/setup-node@v4\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003enode-version\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;20.x\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003eregistry-url\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;https://registry.npmjs.org\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eUpdate npm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003enpm install -g npm@latest\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ePublish to npm\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003enpm publish\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ePush the YAML file to the repo and update the OIDC details on the package settings. I have kept the environment name empty because I don\u0026rsquo;t have any explicit environment setup for actions.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*fjfYVS4FfV6Av8xqeaMNqw.png\" alt=\"updated oidc details\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003eupdated oidc details\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eupdate and save the settings.\u003c/p\u003e\n\u003cp\u003eNow when you create a new release on the github repo, a workflow will start running which will publish the package to npm with provenance.\u003c/p\u003e\n\u003ch2 id=\"step-3-create-a-release-on-github-to-run-the-action\"\u003eStep 3: Create a Release on GitHub to Run the Action\u003ca class=\"heading-anchor\" href=\"#step-3-create-a-release-on-github-to-run-the-action\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eNow it\u0026rsquo;s time to trigger the workflow! Go back to the github repo and create a new release.\u003c/p\u003e\n\u003cp\u003eOnce created, the GitHub Action will automatically start running. You can monitor the progress in the \u003cstrong\u003eActions\u003c/strong\u003e tab of your repository.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*5vdxP7PcaEI51ntX42K5_A.png\" alt=\"successful workflow\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003esuccessful workflow\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eWhen the workflow completes successfully, your package will be published to npm with provenance — proving it was built and published through your trusted CI/CD pipeline!\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*CYJjIDfnPq3HfLTo8G37vA.png\" alt=\"published package\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003epublished package\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eAnd that’s how one can setup cicd to publish npm packages from github actions without having to create any access tokens themselves!\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-12-09T00:00:00Z","id":"https://ankush.one/blogs/npm-oidc-publishing/","summary":"Step-by-step guide to automate npm package publishing through GitHub Actions using OpenID Connect (OIDC). Set up trusted publishing without API keys or access tokens for secure, provenance-verified releases.","tags":["blogs","developer-tools","tutorial"],"title":"Publish to NPM from GitHub Actions using OIDC","url":"https://ankush.one/blogs/npm-oidc-publishing/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/create-magicblock\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://www.npmjs.com/package/create-magicblock\"\u003enpm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScaffold working MagicBlock apps in seconds.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"-why-it-matters\"\u003e🎯 Why It Matters\u003ca class=\"heading-anchor\" href=\"#-why-it-matters\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eZero-to-app in seconds\u003c/strong\u003e — skip the boilerplate and start building immediately\u003c/li\u003e\n\u003cli\u003eProvides \u003cstrong\u003ebest practices\u003c/strong\u003e for MagicBlock development out of the box\u003c/li\u003e\n\u003cli\u003eSupports \u003cstrong\u003eEphemeral Rollups\u003c/strong\u003e for real-time, multiplayer blockchain experiences\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBuild faster on MagicBlock. ⚡\u003c/p\u003e\n\u003ccenter\u003e\n\u003cblockquote class=\"twitter-tweet\"\u003e\u003cp lang=\"en\" dir=\"ltr\"\u003escaffold working \u003ca href=\"https://twitter.com/magicblock?ref_src=twsrc%5Etfw\"\u003e@magicblock\u003c/a\u003e dapps in seconds!\u003cbr\u003ewith create-magicblock 🪄\u003cbr\u003e\u003cbr\u003ewatch how 👇 \u003ca href=\"https://t.co/MkhumKxz1p\"\u003epic.twitter.com/MkhumKxz1p\u003c/a\u003e\u003c/p\u003e\u0026mdash; ankush (@ankushKun_) \u003ca href=\"https://twitter.com/ankushKun_/status/1997714134637649938?ref_src=twsrc%5Etfw\"\u003eDecember 7, 2025\u003c/a\u003e\u003c/blockquote\u003e \u003cscript async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"\u003e\u003c/script\u003e \n\u003c/center\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-12-08T00:00:00Z","id":"https://ankush.one/projects/2025.create-magicblock/","summary":"A CLI scaffolding tool to create fully working MagicBlock apps on Solana in seconds.","tags":["projects","solana","developer-tools"],"title":"create-magicblock","url":"https://ankush.one/projects/2025.create-magicblock/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://x.com/ankushKun_/status/1996342708559073581\"\u003eView on X\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEarth is your canvas. A high-speed, on-chain evolution of Reddit\u0026rsquo;s r/place.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eMegaPlace leverages \u003cstrong\u003eMegaETH\u0026rsquo;s\u003c/strong\u003e sub-millisecond latency to create a real-time multiplayer experience where every pixel placed is a permanent on-chain transaction.\u003c/p\u003e\n\u003ch2 id=\"-the-concept\"\u003e🎨 The Concept\u003ca class=\"heading-anchor\" href=\"#-the-concept\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCollaborative Art:\u003c/strong\u003e Users work together (or fight) to draw on a shared canvas.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOn-Chain:\u003c/strong\u003e Unlike r/place, the state is stored on a blockchain, making it censorship-resistant and permanent.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePerformance:\u003c/strong\u003e Demonstrates the power of MegaETH by handling high-frequency transactions in real-time.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-tech-stack\"\u003e⚡ Tech Stack\u003ca class=\"heading-anchor\" href=\"#-tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMegaETH\u003c/strong\u003e for high-throughput execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSolidity\u003c/strong\u003e or equivalent contracts.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReact\u003c/strong\u003e frontend.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ccenter\u003e\n\u003cblockquote class=\"twitter-tweet\"\u003e\u003cp lang=\"en\" dir=\"ltr\"\u003eWhat if the Earth was a huge canvas\u003cbr\u003eand every pixel an onchain transaction\u003cbr\u003e\u003cbr\u003eWhat would you create? \u003ca href=\"https://t.co/sRJFxeNW51\"\u003epic.twitter.com/sRJFxeNW51\u003c/a\u003e\u003c/p\u003e\u0026mdash; ankush (@ankushKun_) \u003ca href=\"https://twitter.com/ankushKun_/status/1996342708559073581?ref_src=twsrc%5Etfw\"\u003eDecember 3, 2025\u003c/a\u003e\u003c/blockquote\u003e \u003cscript async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"\u003e\u003c/script\u003e\n\u003c/center\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-12-05T00:00:00Z","id":"https://ankush.one/projects/2025.megaplace/","summary":"A global, on-chain canvas where every pixel is a transaction on MegaETH.","tags":["projects","web3"],"title":"MegaPlace","url":"https://ankush.one/projects/2025.megaplace/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/x402bay\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ex402bay is a decentralized marketplace built on the x402 protocol, enabling censorship-resistant commerce on the Permaweb.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIt serves as a reference implementation for the x402 protocol, demonstrating how to build robust e-commerce experiences on Arweave.\u003c/p\u003e\n\u003ch2 id=\"-features\"\u003e🛍️ Features\u003ca class=\"heading-anchor\" href=\"#-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDecentralized Listings:\u003c/strong\u003e Items are stored permanently on Arweave.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTrustless Transactions:\u003c/strong\u003e Smart contracts ensure safe buying and selling.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ex402 Protocol:\u003c/strong\u003e Leverages the Universal Data License and monetization standards.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-tech-stack\"\u003e🔧 Tech Stack\u003ca class=\"heading-anchor\" href=\"#-tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eArweave\u003c/strong\u003e for storage.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReact\u003c/strong\u003e for the frontend.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ex402 Protocol\u003c/strong\u003e for commerce logic.\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-11-16T00:00:00Z","id":"https://ankush.one/projects/2025.x402bay/","summary":"A decentralized digital marketplace powered by the x402 protocol on Arweave.","tags":["projects","arweave","web3"],"title":"x402bay","url":"https://ankush.one/projects/2025.x402bay/"},{"content_html":"\u003cp\u003eWas about to complete 1 year at Forward\u003cbr\u003e\nbut unemployment arc begins, canon event?\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-10-24T00:00:00Z","id":"https://ankush.one/timeline/2025.fwd-layoff/","summary":"Laid off from Forward Research just before completing one year there.","tags":["timeline","job"],"title":"Laid off from Forward Research","url":"https://ankush.one/timeline/2025.fwd-layoff/"},{"content_html":"\u003cp\u003eOne more international trip\u003c/p\u003e\n\u003cp\u003eAttended Arweave Day Asia \u0026lsquo;25 in Singapore and then moved to Kuala Lumpur for 12 days of coworking with fellow founders\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-10-10T00:00:00Z","id":"https://ankush.one/timeline/2025.ada-kl/","summary":"Attended Arweave Day Asia ‘25 in Singapore, then moved to Kuala Lumpur for 12 days of coworking with fellow founders.","tags":["timeline","travel","arweave"],"title":"Singapore + Kuala Lumpur","url":"https://ankush.one/timeline/2025.ada-kl/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://ankushkun.itch.io/cat\"\u003ePlay on Itch.io\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYou are the last survivor of your kind. Can you withstand the dangers of the island?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eA survival RPG built for the web, where you explore, fight, and survive as a lone cat in a hostile world.\u003c/p\u003e\n\u003ch2 id=\"-gameplay-summary\"\u003e🎮 Gameplay Summary\u003ca class=\"heading-anchor\" href=\"#-gameplay-summary\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eExplore:\u003c/strong\u003e Traverse a mysterious island filled with secrets.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSurvive:\u003c/strong\u003e Manage resources and avoid dangers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFight:\u003c/strong\u003e Combat enemies using unique cat-like abilities.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-built-with\"\u003e🛠️ Built With\u003ca class=\"heading-anchor\" href=\"#-built-with\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePlatform:\u003c/strong\u003e WebGL (Itch.io).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/KtB2Iffa3xc\" data-yt=\"KtB2Iffa3xc\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/KtB2Iffa3xc/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-08-15T00:00:00Z","id":"https://ankush.one/projects/2025.last-cat-standing/","image":"https://ankush.one/media/projects/last-cat-standing-cover.jpg","summary":"A survival role-playing game where you play as the last cat on an island.","tags":["projects","game-dev"],"title":"Last Cat Standing","url":"https://ankush.one/projects/2025.last-cat-standing/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://wauth_subspace.arweave.net/\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://github.com/subspace-dev/wauth\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://www.npmjs.com/package/@wauth/sdk\"\u003enpm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWAuth is a social login system that lets users access Arweave webapps using Google, X, GitHub, or Discord — abstracting away complex wallet management.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIt bridges the gap between Web2 usability and Web3 power, making decentralized apps accessible to everyone.\u003c/p\u003e\n\u003ch2 id=\"-the-problem\"\u003e🌱 The Problem\u003ca class=\"heading-anchor\" href=\"#-the-problem\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eWeb3 onboarding is hard.\u003c/strong\u003e Users don\u0026rsquo;t want to manage private keys or install browser extensions just to try an app.\u003c/li\u003e\n\u003cli\u003eDevelopers lose potential users at the \u0026ldquo;Connect Wallet\u0026rdquo; screen due to friction.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-the-solution\"\u003e🚀 The Solution\u003ca class=\"heading-anchor\" href=\"#-the-solution\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSocial Login:\u003c/strong\u003e Users sign in with accounts they already have (Google, X, etc.).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInvisible Wallets:\u003c/strong\u003e WAuth generates and manages a non-custodial wallet in the background.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSeamless Integration:\u003c/strong\u003e Developers can add WAuth to their app with just a few lines of code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-tech-stack\"\u003e🛠️ Tech Stack\u003ca class=\"heading-anchor\" href=\"#-tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eArweave\u003c/strong\u003e for permanent storage.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOIT\u003c/strong\u003e for decentralized authentication.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReact/Next.js\u003c/strong\u003e SDK for easy frontend integration.\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-07-10T00:00:00Z","id":"https://ankush.one/projects/2025.wauth/","summary":"Social login system for Arweave webapps that abstracts wallet management for users.","tags":["projects","arweave","developer-tools"],"title":"WAuth","url":"https://ankush.one/projects/2025.wauth/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://subspace.arweave.net\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://github.com/subspace-dev\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://x.com/use_subspace/status/1982168682064212271\"\u003eDemo Video\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSubspace is a fully decentralized communication platform with a familiar, Discord-like experience — but permanent, censorship-resistant, and powered entirely by the AO network.\u003c/strong\u003e\u003cbr\u003e\nIt shows how real, everyday apps can run without central servers.\u003c/p\u003e\n\u003ch2 id=\"-what-subspace-is\"\u003e🌱 What Subspace Is\u003ca class=\"heading-anchor\" href=\"#-what-subspace-is\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA \u003cstrong\u003echat and community platform\u003c/strong\u003e, similar to Discord\u003c/li\u003e\n\u003cli\u003eFully \u003cstrong\u003edecentralized\u003c/strong\u003e — no servers, no central control\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCensorship-resistant\u003c/strong\u003e and \u003cstrong\u003epermanent\u003c/strong\u003e, thanks to Arweave\u003c/li\u003e\n\u003cli\u003eRuns entirely on the \u003cstrong\u003eAO compute network\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-key-features\"\u003e⚙️ Key Features\u003ca class=\"heading-anchor\" href=\"#-key-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eChannels and communities\u003c/strong\u003e you can join, just like Discord\u003c/li\u003e\n\u003cli\u003eMessages are \u003cstrong\u003epermanent\u003c/strong\u003e and stored on Arweave\u003c/li\u003e\n\u003cli\u003eNo company or admin can take it down\u003c/li\u003e\n\u003cli\u003eBuilt on a simple, message-based architecture powered by AO\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-why-it-matters\"\u003e🔐 Why It Matters\u003ca class=\"heading-anchor\" href=\"#-why-it-matters\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eGives people a communication app that \u003cstrong\u003ecan’t be shut down\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eShows that large, real-time apps can run fully on-chain\u003c/li\u003e\n\u003cli\u003eMakes decentralized tech feel familiar and easy to use\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTogether, these show what’s possible when you build real applications on the AO network.\u003c/p\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/8TsBYd6FcQc\" data-yt=\"8TsBYd6FcQc\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/8TsBYd6FcQc/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-06-10T00:00:00Z","id":"https://ankush.one/projects/2025.subspace/","image":"https://ankush.one/media/projects/subspace-cover.jpg","summary":"A censorship-resistant, permanent communication platform with a Discord-like UX, built on the AO computation network.","tags":["projects","arweave","web3"],"title":"Subspace – Decentralized Communication Platform","url":"https://ankush.one/projects/2025.subspace/"},{"content_html":"\u003cp\u003eFirst international trip! Finally got my passport and moved to Bangkok for a month of cowork with my fellow founders.\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-05-16T00:00:00Z","id":"https://ankush.one/timeline/2025.bangkok/","summary":"First international trip: got my passport and moved to Bangkok for a month of coworking with fellow founders.","tags":["timeline","travel"],"title":"Bangkok Workcation","url":"https://ankush.one/timeline/2025.bangkok/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/aoxpress\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBringing the familiar Express.js development experience to the Permaweb.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eaoxpress\u003c/code\u003e is a routing framework for AO that lowers the barrier to entry for Node.js developers, allowing them to build decentralized compute processes using syntax they already love.\u003c/p\u003e\n\u003ch2 id=\"-key-features\"\u003e⚡ Key Features\u003ca class=\"heading-anchor\" href=\"#-key-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eExpress-like Syntax:\u003c/strong\u003e Use \u003ccode\u003eapp.get()\u003c/code\u003e, \u003ccode\u003eapp.post()\u003c/code\u003e, and middleware just like in Web2.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eao-fetch:\u003c/strong\u003e Built-in utility for simple, HTTP-like inter-process communication on AO.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLightweight:\u003c/strong\u003e Designed specifically for the constraints and capabilities of the AO computer.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-why-it-matters\"\u003e🎯 Why It Matters\u003ca class=\"heading-anchor\" href=\"#-why-it-matters\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLow Learning Curve:\u003c/strong\u003e Node.js developers can start building on AO immediately without learning Lua or complex message passing patterns.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRapid Prototyping:\u003c/strong\u003e Build handlers and routes in minutes.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/biCIDfhGky0\" data-yt=\"biCIDfhGky0\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/biCIDfhGky0/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-05-01T00:00:00Z","id":"https://ankush.one/projects/2025.aoxpress/","image":"https://ankush.one/media/projects/aoxpress-cover.jpg","summary":"An Express.js-like framework for building process handlers on the AO network.","tags":["projects","arweave","developer-tools"],"title":"aoxpress","url":"https://ankush.one/projects/2025.aoxpress/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://x.com/arweaveindia/status/1916037195166552556\"\u003eRundown on X\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eMulti day summit + hackerhouse - keynotes, hands on, panels, interactive sessions\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-04-26T00:00:00Z","id":"https://ankush.one/timeline/2025.adi/","summary":"Organised a multi-day summit and hackerhouse with keynotes, hands-on sessions, panels and interactive sessions.","tags":["timeline","talk","arweave"],"title":"Organised Arweave Day India","url":"https://ankush.one/timeline/2025.adi/"},{"content_html":"\u003cp\u003eAPM package for writing jsx like syntax inside Lua\u003c/p\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/wvHACqSRDkw\" data-yt=\"wvHACqSRDkw\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/wvHACqSRDkw/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e\n\u003c!-- https://apm_betteridea.arweave.net/pkg?id=@apm/luax --\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-04-01T00:00:00Z","id":"https://ankush.one/projects/2023.luax/","image":"https://ankush.one/media/projects/luax-cover.jpg","summary":"A React-like library for Lua.","tags":["projects","arweave","developer-tools"],"title":"LuaX","url":"https://ankush.one/projects/2023.luax/"},{"content_html":"\u003cp\u003eSpoke about Arweave, the hackerhouses, launchpad and everything Arweave India in Gurgaon and Chandigarh\u003c/p\u003e\n\u003ccenter\u003e\n\u003cblockquote class=\"twitter-tweet\"\u003e\u003cp lang=\"en\" dir=\"ltr\"\u003eHere\u0026#39;s a video explainer for those who missed the ADI tours :)\u003cbr\u003e\u003cbr\u003e🔗 \u003ca href=\"https://t.co/Aym6mQkIVA\"\u003ehttps://t.co/Aym6mQkIVA\u003c/a\u003e \u003ca href=\"https://t.co/M6SxG0sOV1\"\u003ehttps://t.co/M6SxG0sOV1\u003c/a\u003e \u003ca href=\"https://t.co/tJzN43ejdr\"\u003epic.twitter.com/tJzN43ejdr\u003c/a\u003e\u003c/p\u003e\u0026mdash; ankush (@ankushKun_) \u003ca href=\"https://twitter.com/ankushKun_/status/1908797968917946787?ref_src=twsrc%5Etfw\"\u003eApril 6, 2025\u003c/a\u003e\u003c/blockquote\u003e \u003cscript async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"\u003e\u003c/script\u003e \n\u003c/center\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-03-30T00:00:00Z","id":"https://ankush.one/timeline/2025.adi-tours/","summary":"Spoke about Arweave, the hackerhouses, launchpad and everything Arweave India in Gurgaon and Chandigarh","tags":["timeline","talk","arweave"],"title":"Hosted ADI Tour at GGN \u0026 CHD","url":"https://ankush.one/timeline/2025.adi-tours/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/visual-ao\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVisual AO is a visual, flow-based programming tool for creating, exploring, and understanding AO processes — without writing code.\u003c/strong\u003e\u003cbr\u003e\nIt helps developers and beginners see how AO logic works through interactive blocks and connections.\u003c/p\u003e\n\u003ch2 id=\"-what-it-is\"\u003e🌱 What It Is\u003ca class=\"heading-anchor\" href=\"#-what-it-is\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eA \u003cstrong\u003evisual, flow-based programming tool\u003c/strong\u003e for building AO processes using drag-and-drop blocks instead of code.\u003c/p\u003e\n\u003ch2 id=\"-key-features\"\u003e⚙️ Key Features\u003ca class=\"heading-anchor\" href=\"#-key-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eVisual node editor\u003c/strong\u003e with blocks and connections\u003c/li\u003e\n\u003cli\u003eReal-time debugging and process exploration\u003c/li\u003e\n\u003cli\u003eExport flows to AO-ready logic\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-why-it-matters\"\u003e🎯 Why It Matters\u003ca class=\"heading-anchor\" href=\"#-why-it-matters\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eLowers the barrier to entry for AO development — making it easier to learn, prototype, and explain decentralized compute concepts.\u003c/p\u003e\n\u003ccenter\u003e\n\u003cblockquote class=\"twitter-tweet\"\u003e\u003cp lang=\"en\" dir=\"ltr\"\u003eIntroducing Visual AO - program your ao processes visually, without the need to write code\u003cbr\u003e\u003cbr\u003etry now at \u003ca href=\"https://t.co/YhZ3eDvF9N\"\u003ehttps://t.co/YhZ3eDvF9N\u003c/a\u003e 🚀 \u003ca href=\"https://t.co/LW127wwATC\"\u003epic.twitter.com/LW127wwATC\u003c/a\u003e\u003c/p\u003e\u0026mdash; ankush (@ankushKun_) \u003ca href=\"https://twitter.com/ankushKun_/status/1901870254290292938?ref_src=twsrc%5Etfw\"\u003eMarch 18, 2025\u003c/a\u003e\u003c/blockquote\u003e \u003cscript async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"\u003e\u003c/script\u003e \n\u003c/center\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-03-18T00:00:00Z","id":"https://ankush.one/projects/2025.visual-ao/","summary":"A visual, flow-based programming tool for building and understanding AO processes.","tags":["projects","arweave","developer-tools"],"title":"Visual AO","url":"https://ankush.one/projects/2025.visual-ao/"},{"content_html":"\u003cp\u003e\u003cem\u003eAlllll riiiight lets do this one more time\u003c/em\u003e, my name is Ankush and I\u0026rsquo;m your friendly neighbourhood web developer, here to educate your pathetic ass a bit about web APIs, how they can be used to connect your frontend with your backend and how you can write an API using NodeJS 🫡\u003c/p\u003e\n\u003cp\u003eAn API — Application Programming Interface, defines how different applications can talk to each other and exchange data through structured requests and responses.\u003c/p\u003e\n\u003ch2 id=\"api-types\"\u003eAPI Types\u003ca class=\"heading-anchor\" href=\"#api-types\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eWeb APIs\u003c/strong\u003e — the one we will be focusing on today\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOS APIs\u003c/strong\u003e — Windows APIs, Android APIs, etc\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLibrary APIs\u003c/strong\u003e — Like a python library\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHardware APIs\u003c/strong\u003e — allow accessing hardware functions (embedded libraries?)\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"restful-web-apis\"\u003eRESTful Web APIs\u003ca class=\"heading-anchor\" href=\"#restful-web-apis\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eA RESTful Web API is one that follows the \u0026ldquo;REST\u0026rdquo; (Representational State Transfer) principles, a software architecture style that ensures scalability, simplicity, and stateless communication between servers and clients.\u003c/p\u003e\n\u003ch3 id=\"common-rest-http-methods\"\u003eCommon REST HTTP Methods\u003ca class=\"heading-anchor\" href=\"#common-rest-http-methods\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eThere are 4 common methods/requests to call REST APIs:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eGET\u003c/strong\u003e — to get some data from the server\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePOST\u003c/strong\u003e — to send/create some data from the server and get a response\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePUT\u003c/strong\u003e — to update existing data on a server\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDELETE\u003c/strong\u003e — to remove some data from a server\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eWhen browsing the web, a GET request is used to fetch the client-side content that gets rendered by the browser.\u003c/p\u003e\n\u003ch3 id=\"api-response-codes\"\u003eAPI Response Codes\u003ca class=\"heading-anchor\" href=\"#api-response-codes\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h3\u003e\n\u003cp\u003eWhenever an API is called, it will always return a \u003cstrong\u003estatus code\u003c/strong\u003e (a number) to signify the result of the request.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e1XX\u003c/strong\u003e: Informational response\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e2XX\u003c/strong\u003e: Success codes (e.g., 200 — OK)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e3XX\u003c/strong\u003e: Redirects (e.g., 301 — Moved Permanently)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e4XX\u003c/strong\u003e: Client errors (e.g., 400 — Bad Request, 401 — Unauthorised, 404 — Not Found)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e5XX\u003c/strong\u003e: Server errors (e.g., 500 — Internal Server Error)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"lets-write-an-api-endpoint-ourselves\"\u003eLet\u0026rsquo;s Write an API Endpoint Ourselves\u003ca class=\"heading-anchor\" href=\"#lets-write-an-api-endpoint-ourselves\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eMake sure you have NodeJS + NPM already installed and follow these steps:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir api-test\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e api-test\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm init -y\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm i express\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etouch index.js\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis will create a folder for our express API backend and create a JS file to write code in. Edit the \u003ccode\u003eindex.js\u003c/code\u003e file and add this code:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eexpress\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003erequire\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;express\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eapp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eexpress\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Hello World\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elisten\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3000\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Server is running on port 3000\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWe have just initialised an express backend which is serving the \u0026ldquo;Hello World\u0026rdquo; string at the root (/) endpoint. i.e. on sending a GET request to \u003ccode\u003e/\u003c/code\u003e, \u0026ldquo;Hello World\u0026rdquo; will be returned.\u003c/p\u003e\n\u003cp\u003eRun this file with:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enode index.js\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnd open \u003ccode\u003ehttp://localhost:3000/\u003c/code\u003e on any browser. It should show \u0026ldquo;Hello World\u0026rdquo;.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1324/format:webp/1*fh2r1wAEpvcPK0bZpV9Y-Q.png\" alt=\"Hello World Demo\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003eHello World Demo\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eTry changing the string in \u003ccode\u003eres.send('this one')\u003c/code\u003e to simple HTML like \u003ccode\u003e\u0026lt;h1\u0026gt;Hi there\u0026lt;/h1\u0026gt;\u003c/code\u003e and restart the express server (ctrl+c and \u003ccode\u003enode index.js\u003c/code\u003e again), check if something changes on the served page.\u003c/p\u003e\n\u003ch2 id=\"defining-a-post-endpoint\"\u003eDefining a POST Endpoint\u003ca class=\"heading-anchor\" href=\"#defining-a-post-endpoint\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eBut this is just for GET requests, we cannot make POST requests to this endpoint.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1236/format:webp/1*cJV5BQAHvocrDKE_LO1jMQ.png\" alt=\"POST Request Error\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003ePOST Request Error\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eAs you can see it gives an error if we try to make a POST request to the GET endpoint we created. Let\u0026rsquo;s define a POST endpoint now. Add this after defining the app:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003euse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eexpress\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ejson\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// POST endpoint\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`Hello \u003c/span\u003e\u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sb\"\u003e!`\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRestart the app and let\u0026rsquo;s try sending a post request to the server. You can use any HTTP client like Thunder Client on VSCode or Postman. I am going to use cURL coz simple:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl http://localhost:3000/ -d \u003cspan class=\"s1\"\u003e\u0026#39;{ \u0026#34;name\u0026#34;: \u0026#34;Ankush\u0026#34; }\u0026#39;\u003c/span\u003e -H \u003cspan class=\"s2\"\u003e\u0026#34;Content-Type: application/json\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis should print the response as:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHello Ankush!\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eBOOM! You just created a GET and POST request! A majority of web APIs are comprised of just GET and POST requests.\u003c/p\u003e\n\u003ch2 id=\"time-to-write-some-frontend\"\u003eTime to Write Some Frontend\u003ca class=\"heading-anchor\" href=\"#time-to-write-some-frontend\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eNow let\u0026rsquo;s serve an HTML page that interacts with our POST endpoint:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ehtml\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;text\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eplaceholder\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Enter your name\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e \u003cspan class=\"na\"\u003eonclick\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;handleSubmit()\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eSubmit\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"kr\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nx\"\u003ehandleSubmit\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetElementById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;name\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kr\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"nx\"\u003emethod\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;POST\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s1\"\u003e\u0026#39;Content-Type\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;application/json\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          \u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kr\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etext\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003einnerHTML\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ehtml\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWe will serve this HTML through the GET endpoint we created earlier:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003esend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e    \u0026lt;html\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e      \u0026lt;body\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e        \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;name\u0026#34; name=\u0026#34;name\u0026#34; placeholder=\u0026#34;Enter your name\u0026#34;\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e        \u0026lt;button onclick=\u0026#34;handleSubmit()\u0026#34;\u0026gt;Submit\u0026lt;/button\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e        \u0026lt;script\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e          async function handleSubmit() {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e            const name = document.getElementById(\u0026#39;name\u0026#39;).value;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e            const response = await fetch(\u0026#39;/\u0026#39;, {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e              method: \u0026#39;POST\u0026#39;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e              headers: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e                \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e              },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e              body: JSON.stringify({ name })\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e            });\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e            const data = await response.text();\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e            document.body.innerHTML = data;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e          }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e        \u0026lt;/script\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e      \u0026lt;/body\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e    \u0026lt;/html\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sb\"\u003e  `\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRestart the express app and refresh \u003ccode\u003elocalhost:3000\u003c/code\u003e, and try entering your name.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*KhXDm_zbhiECJnoVBZE-XA.png\" alt=\"Frontend Demo with Network Tab\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003eFrontend Demo with Network Tab\u003c/figcaption\u003e\u003c/figure\u003e\u003cp\u003eNow if you open developer tools and go into the network tab, you can see the POST request being sent and the different data associated with it.\u003c/p\u003e\n\u003cfigure class=\"doc-figure\"\u003e\u003cimg src=\"https://miro.medium.com/v2/resize:fit:1400/format:webp/1*Wkbk2CrKOqYMwyQ11P9Eiw.png\" alt=\"Network Tab\" loading=\"lazy\" decoding=\"async\"\u003e\u003cfigcaption aria-hidden=\"true\"\u003eNetwork Tab\u003c/figcaption\u003e\u003c/figure\u003e\u003chr\u003e\n\u003cp\u003eAnd that\u0026rsquo;s a wrap! You\u0026rsquo;ve just learned the basics of web APIs, REST principles, and created your own simple API with both GET and POST endpoints. Now go build something cool! 🚀\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-02-19T00:00:00Z","id":"https://ankush.one/blogs/web-apis-guide/","summary":"Learn what APIs are, how they work, and how to create your own simple API using Node.js and Express. A complete beginner-friendly tutorial with practical code examples covering REST principles, HTTP methods, and building GET/POST endpoints.","tags":["blogs","web","tutorial"],"title":"A Noob's Guide to Web APIs","url":"https://ankush.one/blogs/web-apis-guide/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/random-retarded-3am-project\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://web.archive.org/web/20250214215917/https://www.callmemaybe.xyz/\"\u003eWebsite\u003c/a\u003e (archived snapshot)\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA video-first dating experiment that went viral overnight.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThis rapid prototype was launched just in time for Valentine\u0026rsquo;s Day, challenging conventional dating apps with a raw, video-only approach.\u003c/p\u003e\n\u003ch2 id=\"-viral-success\"\u003e🚀 Viral Success\u003ca class=\"heading-anchor\" href=\"#-viral-success\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e50,000+ Impressions\u003c/strong\u003e on X in the first 24 hours.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e1,000+ Signups\u003c/strong\u003e on launch day.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e#25 on ProductHunt\u003c/strong\u003e daily ranking.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-the-idea\"\u003e💡 The Idea\u003ca class=\"heading-anchor\" href=\"#-the-idea\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eMove away from polished profiles and text chats.\u003c/li\u003e\n\u003cli\u003eConnect people through authentic, short video snippets.\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2025-02-14T00:00:00Z","id":"https://ankush.one/projects/2025.callmemaybe/","summary":"A viral video dating app built for Valentine’s Day. 50k+ engagements.","tags":["projects","web"],"title":"Call Me Maybe","url":"https://ankush.one/projects/2025.callmemaybe/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/power-editor\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://web.archive.org/web/20241224113142/https://apps.apple.com/us/app/power-editor/id6739633465\"\u003eApp Store listing\u003c/a\u003e (archived snapshot)\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrustrated by subscription-wall photo editors on iOS, I built my own.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003ePower Editor provides professional-grade tools like curves, HSL, and filters completely for free. Built in a single weekend.\u003c/p\u003e\n\u003ch2 id=\"-key-features\"\u003e🎨 Key Features\u003ca class=\"heading-anchor\" href=\"#-key-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAdvanced Tools:\u003c/strong\u003e Curves, HSL adjustments, and selective color grading.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePrivacy Focused:\u003c/strong\u003e All processing happens on-device.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo Subscriptions:\u003c/strong\u003e 100% free and open source.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-tech-stack\"\u003e🍏 Tech Stack\u003ca class=\"heading-anchor\" href=\"#-tech-stack\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSwift / SwiftUI\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCoreImage\u003c/strong\u003e for GPU-accelerated processing.\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-12-23T00:00:00Z","id":"https://ankush.one/projects/2024.power-editor/","summary":"A feature-rich iOS image editor built in 48 hours.","tags":["projects","mobile","graphics"],"title":"Power Editor","url":"https://ankush.one/projects/2024.power-editor/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://devfolio.co/projects/hackadome-daf3\"\u003eHack-A-Dome on Devfolio\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eBuilt and shipped \u003cstrong\u003eHack-A-Dome\u003c/strong\u003e — a gamified, on-chain virtual venue for ETHIndia.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult:\u003c/strong\u003e Won 1 bounty and 2 prize pools from Coinbase.\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-12-09T00:00:00Z","id":"https://ankush.one/timeline/2024.ethindia/","summary":"Won 1 bounty and 2 prize pools from Coinbase","tags":["timeline","hackathon"],"title":"Bounties at ETHIndia '24","url":"https://ankush.one/timeline/2024.ethindia/"},{"content_html":"\u003cp\u003eJudged 40+ builds at the \u003ca href=\"https://fullstack-hack.devpost.com/\" data-external\u003eArweave Full Stack Hack\u003c/a\u003e, a hackathon to build permaweb apps,\u003cbr\u003e\norganised by \u003ca href=\"https://www.longviewlabs.co/\" data-external\u003eLongview Labs\u003c/a\u003e\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-11-27T00:00:00Z","id":"https://ankush.one/timeline/2024.fsh-judge/","summary":"Judged 40+ builds at the Arweave Full Stack Hack, a hackathon to build permaweb apps organised by Longview Labs.","tags":["timeline","hackathon","arweave"],"title":"Judge at Arweave FullStackHack","url":"https://ankush.one/timeline/2024.fsh-judge/"},{"content_html":"\u003cp\u003eStarted working as an Entrepreneur in Residence with \u003ca href=\"https://fwd.arweave.net/\" data-external\u003eForward Research\u003c/a\u003e to continue developing developer tools for the \u003ca href=\"https://arweave.org/\" data-external\u003earweave\u003c/a\u003e ecosystem 🥳\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-11-01T00:00:00Z","id":"https://ankush.one/timeline/2024.fwd/","summary":"Started working as an Entrepreneur in Residence with Forward Research, building developer tools for the Arweave ecosystem.","tags":["timeline","job","arweave"],"title":"EIR at FWD","url":"https://ankush.one/timeline/2024.fwd/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://x.com/Weavers_Org/status/1821262463897075745\"\u003eResults on X\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eBuilt a Vercel like deployment platform for permaweb apps. Won third place\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-08-01T00:00:00Z","id":"https://ankush.one/timeline/2024.weavers3/","summary":"Built a Vercel-like deployment platform for permaweb apps and won third place.","tags":["timeline","hackathon","arweave"],"title":"Weavers Raid III","url":"https://ankush.one/timeline/2024.weavers3/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://arlink.arweave.net/\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://x.com/arlinklabs\"\u003eArLink on X\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDumDumDeploy was a Vercel-like platform for deploying and managing permaweb applications.\u003c/strong\u003e\u003cbr\u003e\nIt won \u003cstrong\u003eWeavers Raid III\u003c/strong\u003e and later evolved into \u003cstrong\u003eArLink\u003c/strong\u003e, Arweave’s flagship deployment tool — now being developed by \u003cstrong\u003eArLinkLabs\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"-origin\"\u003e🌱 Origin\u003ca class=\"heading-anchor\" href=\"#-origin\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eInitially built as \u003cstrong\u003eDumDumDeploy\u003c/strong\u003e, focused on simple, fast permaweb deployments\u003c/li\u003e\n\u003cli\u003eDesigned to give developers a smooth experience similar to modern cloud platforms like Vercel\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-evolution-into-arlink\"\u003e🚀 Evolution Into ArLink\u003ca class=\"heading-anchor\" href=\"#-evolution-into-arlink\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eThe platform grew beyond its MVP and was rebranded as \u003cstrong\u003eArLink\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eBecame the primary deployment tool recommended across the Arweave ecosystem\u003c/li\u003e\n\u003cli\u003eThe team behind ArLink went on to raise a \u003cstrong\u003eseed round at a $2.5M valuation\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-my-role\"\u003e🧩 My Role\u003ca class=\"heading-anchor\" href=\"#-my-role\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBuilt the original DumDumDeploy platform\u003c/strong\u003e from the ground up\u003c/li\u003e\n\u003cli\u003eHelped shape the early architecture and developer experience that ArLink is now built on\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-why-it-matters\"\u003e🎯 Why It Matters\u003ca class=\"heading-anchor\" href=\"#-why-it-matters\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eMade deploying permaweb apps \u003cstrong\u003esimple, fast, and accessible\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eShowed how web3 deployment tooling can match (or exceed) the UX of modern web platforms\u003c/li\u003e\n\u003cli\u003eSet the foundation for ArLink — now a core part of the Arweave ecosystem\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-08-01T00:00:00Z","id":"https://ankush.one/projects/2024.dumdumdeploy/","summary":"A Vercel-like permaweb deployment platform that evolved into ArLink, Arweave’s flagship app deployment tool.","tags":["projects","arweave","developer-tools","hackathon"],"title":"DumDumDeploy","url":"https://ankush.one/projects/2024.dumdumdeploy/"},{"content_html":"\u003cp\u003eDemoed BetterIDEa and clickoor\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-07-01T00:00:00Z","id":"https://ankush.one/timeline/2024.demoday/","summary":"Demoed BetterIDEa and Clickoor at the ArweaveIndia Demo Day.","tags":["timeline","talk","arweave"],"title":"ArweaveIndia Demo Day","url":"https://ankush.one/timeline/2024.demoday/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://apm_betteridea.arweave.net\"\u003eWebsite\u003c/a\u003e · \u003ca href=\"https://github.com/betteridea-dev/ao-package-manager\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://github.com/ankushKun/apm-cli\"\u003eGitHub: apm-cli\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAPM is the package manager used in the AO computation network — similar to LuaRocks, but built as a web3-native system specifically for AO.\u003c/strong\u003e\u003cbr\u003e\nIt allows developers to install, publish, and share reusable AO packages, enabling a modular ecosystem for building decentralized compute applications.\u003c/p\u003e\n\u003ch2 id=\"-what-apm-does\"\u003e🌱 What APM Does\u003ca class=\"heading-anchor\" href=\"#-what-apm-does\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eProvides a \u003cstrong\u003epackage registry\u003c/strong\u003e for the AO ecosystem\u003c/li\u003e\n\u003cli\u003eAllows developers to \u003cstrong\u003einstall\u003c/strong\u003e packages directly into their AO processes\u003c/li\u003e\n\u003cli\u003eEnables publishing of reusable packages for the community\u003c/li\u003e\n\u003cli\u003eInspired by tools like \u003cstrong\u003eLuaRocks\u003c/strong\u003e, but adapted for AO’s permanent, decentralized compute model\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-core-features\"\u003e⚙️ Core Features\u003ca class=\"heading-anchor\" href=\"#-core-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eInstall packages\u003c/strong\u003e with a simple CLI\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePublish new packages\u003c/strong\u003e to the decentralized APM registry\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVersioning and permanence\u003c/strong\u003e powered by Arweave storage\u003c/li\u003e\n\u003cli\u003eWorks seamlessly across AO processes and tooling\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-impact\"\u003e📈 Impact\u003ca class=\"heading-anchor\" href=\"#-impact\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eMakes AO development modular and composable\u003c/li\u003e\n\u003cli\u003eEncourages open-source collaboration within the ecosystem\u003c/li\u003e\n\u003cli\u003eReduces duplication by allowing developers to share common logic and utilities\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNow adopted as part of the core AO infrastructure\u003c/strong\u003e, powering package distribution across the network\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-ecosystem-fit\"\u003e🔌 Ecosystem Fit\u003ca class=\"heading-anchor\" href=\"#-ecosystem-fit\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cp\u003eAPM integrates naturally with:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBetterIDEa\u003c/strong\u003e (IDE \u0026amp; dev suite)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCLI tools\u003c/strong\u003e for AO deployment\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTogether, these tools create a unified and efficient workflow for building on AO.\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-07-01T00:00:00Z","id":"https://ankush.one/projects/2024.apm/","summary":"The package manager for the AO computation network, enabling developers to install, publish, and share reusable AO packages.","tags":["projects","arweave","developer-tools"],"title":"APM – AO Package Manager","url":"https://ankush.one/projects/2024.apm/"},{"content_html":"\u003cp\u003eProvide technical mentorship to 100+ developers, helping teams ship on-chain projects\u003cbr\u003e\nCo-organise flagship events \u0026amp; 4+ hackerhouses to grow the Arweave community in India\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-06-01T00:00:00Z","id":"https://ankush.one/timeline/2024.community-manager/","summary":"Support developers, build mini apps, organise events","tags":["timeline","job","arweave"],"title":"DevRel Engineer @ ArweaveIndia","url":"https://ankush.one/timeline/2024.community-manager/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/clickoor-v2\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA point-and-shoot camera that never runs out of film.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eClickoor automatically uploads every photo to the Arweave Permaweb, ensuring your memories are stored forever on-chain without any configuration.\u003c/p\u003e\n\u003ch2 id=\"-features\"\u003e📸 Features\u003ca class=\"heading-anchor\" href=\"#-features\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eInfinite Storage:\u003c/strong\u003e Powered by Arweave\u0026rsquo;s permanent web.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eZero Config:\u003c/strong\u003e Just point and shoot.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOn-Chain:\u003c/strong\u003e Photos are immutable and censorship-resistant.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-hardwaresoftware\"\u003e🛠️ Hardware/Software\u003ca class=\"heading-anchor\" href=\"#-hardwaresoftware\" aria-label=\"Link to this section\"\u003e\u003c/a\u003e\n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eWeb App:\u003c/strong\u003e Runs on mobile devices acting as the camera interface.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eArweave:\u003c/strong\u003e Handles the decentralized storage backend.\u003c/li\u003e\n\u003c/ul\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-06-01T00:00:00Z","id":"https://ankush.one/projects/2024.clickoor/","summary":"A point-and-shoot camera powered by Arweave with infinite, permanent storage.","tags":["projects","arweave","hardware"],"title":"Clickoor Camera","url":"https://ankush.one/projects/2024.clickoor/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://github.com/ankushKun/sudo-apt-uninstall-sleep\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eProblem:\u003c/strong\u003e Phishing sites are becoming increasingly sophisticated.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSolution:\u003c/strong\u003e A browser that uses a lightweight client-side AI model to analyze DOM structure and content patterns to flag \u0026ldquo;sus\u0026rdquo; (suspicious) activity before the user enters data.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c!-- [View on GitHub](https://github.com/ankushKun/sudo-apt-uninstall-sleep) --\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-03-16T00:00:00Z","id":"https://ankush.one/timeline/2024.upes/","summary":"Built an AI enabled web browser for detecting sus activities on websites. One of the top 5 projects","tags":["timeline","hackathon"],"title":"UPES, Dehradun: Hackathon 8.0","url":"https://ankush.one/timeline/2024.upes/"},{"content_html":"\u003cp\u003eLaunched BetterIDEa\u003c/p\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/9snJSH9CdUs\" data-yt=\"9snJSH9CdUs\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/9snJSH9CdUs/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-02-27T00:00:00Z","id":"https://ankush.one/timeline/2024.ao-launch/","summary":"Launched BetterIDEa","tags":["timeline","arweave"],"title":"AO testnet Launch - 27th Feb, 2024!","url":"https://ankush.one/timeline/2024.ao-launch/"},{"content_html":"\u003cp\u003eThree month program\u003cbr\u003e\nBuilding BetterIDEa as a product\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-01-27T00:00:00Z","id":"https://ankush.one/timeline/2024.launchpad/","summary":"Joined the three month Arweave India Launchpad program, building BetterIDEa as a product.","tags":["timeline","arweave"],"title":"Arweave India Launchpad program!","url":"https://ankush.one/timeline/2024.launchpad/"},{"content_html":"\u003cp\u003eAttended the exclusive AO reveal event for Indian hackers and met Sam Williams (Founder of Arweave).\u003c/p\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2024-01-10T00:00:00Z","id":"https://ankush.one/timeline/2024.sam/","summary":"Attended the exclusive AO reveal event for Indian hackers and met Sam Williams, founder of Arweave.","tags":["timeline","arweave"],"title":"Met Sam Williams IRL at Bangalore!","url":"https://ankush.one/timeline/2024.sam/"},{"content_html":"\u003cp\u003eLinks: \u003ca href=\"https://x.com/arweaveindia/status/1734972911163961371\"\u003eAnnouncement on X\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eBuilt a web based IDE for Arweave Smartweave contracts\u003cbr\u003e\nArweaveIndia Cohort 1\u003cbr\u003e\nAmazing People, Amazing Experience\u003cbr\u003e\nLong term friends made\u003c/p\u003e\n\u003cfigure class=\"doc-video\"\u003e\n    \u003cdiv class=\"youtube-embed\" data-ratio=\"16:9\"\u003e\n        \u003ca class=\"yt-lite\" href=\"https://youtu.be/LWW4BCZPIUg\" data-yt=\"LWW4BCZPIUg\" data-title=\"YouTube video\" target=\"_blank\" rel=\"noopener\"\u003e\n            \u003cimg src=\"https://i.ytimg.com/vi/LWW4BCZPIUg/hqdefault.jpg\" alt=\"\" width=\"480\" height=\"360\" loading=\"lazy\" decoding=\"async\"\u003e\n            \u003cspan class=\"yt-lite-play\" aria-hidden=\"true\"\u003e\u003c/span\u003e\n            \u003cspan class=\"yt-lite-label\"\u003ePlay video\u003c/span\u003e\n        \u003c/a\u003e\n    \u003c/div\u003e\n\u003c/figure\u003e","date_modified":"2026-10-12T02:59:21+05:30","date_published":"2023-12-07T00:00:00Z","id":"https://ankush.one/timeline/2023.hackerhouse/","summary":"Built a web based IDE for Arweave Smartweave contracts","tags":["timeline","hackathon","arweave"],"title":"Won the Arweave ⓐ Hackerhouse at Bangalore","url":"https://ankush.one/timeline/2023.hackerhouse/"}],"language":"en-US","title":"Ankush Kun","version":"https://jsonfeed.org/version/1.1"}