Your AI writes KernScript. You save money.
KernScript is a tiny language your model generates in a fraction of the tokens, then compiles to the exact JavaScript and TypeScript you already run, review and trust.
Every token your AI writes is billed and waited on. KernScript cuts the code it writes by up to 75%: lower cost, lower latency, fewer retries.
import { Hono } from "hono";
import Database from "better-sqlite3";
import { ok, dbAdd, dbAll, defineTable, openDb, readBody, readEnv, reply, serve } from "./server.js";
const env = readEnv(["PORT", "DATABASE_URL"]);
const _db = openDb(env.DATABASE_URL);
const users = defineTable(_db, "users", { columns: [
{ name: "name", type: "str" },
{ name: "email", type: "str", unique: true },
]});
const app = new Hono();
app.get("/users", async (c) => reply(c, ok(dbAll(users))));
app.post("/users", async (c) => {
const _b = await readBody(c, { name: "str", email: "str" });
if (!_b.ok) return c.json({ error: "invalid body", fields: _b.errors }, 400);
const { name, email } = _b.value;
return reply(c, ok(dbAdd(users, { name, email })));
});
serve(app, env.PORT);
(env PORT DATABASE_URL)
(server api :port PORT
(db users {name: str, email: str unique})
(route GET "/users" (ok (db.all users)))
(route POST "/users" {name email}
(ok (db.add users {name email}))))
In plain terms
One idea, three steps.
The model writes KernScript
A compact, uniform syntax the AI produces in far fewer tokens than JavaScript or TypeScript.
The compiler does the rest
It type-checks the program, then emits readable JavaScript or strict TypeScript. No build server, no magic.
You ship plain JavaScript
Your team reviews and runs the same code it would have written by hand, on the standard Node ecosystem.
Why it matters
The languages AI writes best are the ones it writes most expensively.
JavaScript and TypeScript dominate AI output
They are the most generated languages in coding assistants and agents. Their inefficiency is not a one-off cost. It repeats on every request your product makes.
Every token is paid for in cost and latency
await, Promise.all, res.ok, res.json, try and catch, imports, type annotations. All of that is generated, billed and waited on before your program does anything useful.
More syntax means more ways to get it wrong
Verbose, irregular grammar gives a model more surface for mistakes, and each fix is another full generation. The real cost is the round-trips to a working program.
See the power
The same program. A fraction of the tokens.
Pick a real program and watch what each language costs the model to write. Same behavior, same audited output, far fewer tokens.
(env PORT DATABASE_URL AUTH_TOKEN)
(server api :port PORT
(db authors {name: str, email: str unique, created: time auto})
(db posts {title: str, body: str, author: num, published: bool, created: time auto})
(db comments {post: num, text: str, created: time auto})
(auth :bearer)
(route POST "/authors" {name email} (ok (db.add authors {name email})))
(route GET "/authors" :public (ok (db.all authors)))
(route POST "/posts" {title body author published}
(ok (db.add posts {title body author published})))
(route GET "/posts" :public (ok (db.all posts :order created)))
(route GET "/posts/:id" :public
(let p (db.get posts :id)) (if p (ok p) (err 404 "post not found")))
(route PUT "/posts/:id" {title body} (ok (db.set posts :id {title body})))
(route DELETE "/posts/:id" (db.del posts :id) (ok))
(route POST "/posts/:id/comments" {text}
(ok (db.add comments {post: (toNum id), text})))
(route GET "/posts/:id/comments" :public
(ok (db.find comments {post: (toNum id)})))
(route GET "/stats" :public
(ok {authors: (len (db.all authors)), posts: (len (db.all posts)), comments: (len (db.all comments))})))
(env PORT DATABASE_URL AUTH_TOKEN)
(server api :port PORT
(db users {name: str, email: str unique, age: num, created: time auto})
(auth :bearer)
(route GET "/users" (ok (db.all users :order age)))
(route GET "/users/:id"
(let u (db.get users :id)) (if u (ok u) (err 404 "not found")))
(route POST "/users" {name email age} (ok (db.add users {name email age})))
(route PUT "/users/:id" {name age} (ok (db.set users :id {name age})))
(route DELETE "/users/:id" (db.del users :id) (ok)))
(tool weather
(desc "Current weather for a city")
(in {city: str}) (out {temp: num, desc: str})
(let d (fetch "https://api.example.com/weather" {q: city}))
{temp: d.temp, desc: d.text})
(agent helper
(model "claude-sonnet-4-6")
(system "Sales team assistant. Reply in Spanish.")
(tools [weather]) (memory chat :max 30) (permission [:net]))
(let r (ask helper "Weather in Madrid?"))
(print r.text)
Token counts measured with o200k_base against an idiomatic, hand-written JavaScript equivalent.
Try it yourself
Edit KernScript. Watch it compile, live.
This editor runs the real KernScript compiler in your browser. As you type, the compiled JavaScript, the token counts and the type-checker errors all update instantly.
The proof
Measured on a real run, not claimed.
Tokens per program: KernScript versus the hand-written JavaScript it compiles to
These are the real token counts of each program; the headline number is from a real run on Gemini. Results vary by model and session: the manual rides every request, so on a short session or a terse model that fixed cost can outweigh the savings. Always cache it as a stable prompt prefix, so it is paid once per session, not on every call.
See the code
The model writes KernScript. Your team reviews the JavaScript it compiles to.
(tool weather
(desc "Current weather for a city")
(in {city: str}) (out {temp: num, desc: str})
(let d (fetch "https://api.example.com/weather" {q: city}))
{temp: d.temp, desc: d.text})
(agent helper
(model "claude-sonnet-4-6")
(tools [weather]) (permission [:net]))
(let r (ask helper "Weather in Madrid?"))
(print r.text)
import http from 'node:http';
import Database from 'better-sqlite3';
const db = new Database(process.env.DATABASE_URL || ':memory:');
db.exec(`CREATE TABLE IF NOT EXISTS authors (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT UNIQUE, created TEXT);
CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, author REAL, published INTEGER, created TEXT);
CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY AUTOINCREMENT, post REAL, text TEXT, created TEXT);`);
const TOKEN = process.env.AUTH_TOKEN;
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(obj === undefined ? '' : JSON.stringify(obj)); };
const readBody = async (req) => { const c = []; for await (const x of req) c.push(x); return c.length ? JSON.parse(Buffer.concat(c).toString()) : {}; };
const authed = (req) => { const m = (req.headers['authorization'] || '').match(/^Bearer\s+(.+)$/i); return !!m && m[1] === TOKEN; };
const row = (t, id) => db.prepare('SELECT * FROM ' + t + ' WHERE id = ?').get(id);
const now = () => new Date().toISOString();
const server = http.createServer(async (req, res) => {
const p = new URL(req.url, 'http://x').pathname;
const method = req.method;
if ((method === 'POST' || method === 'PUT' || method === 'DELETE') && !authed(req)) return json(res, 401, { error: 'unauthorized' });
const mComments = p.match(/^\/posts\/(\d+)\/comments$/);
const mPost = p.match(/^\/posts\/(\d+)$/);
if (method === 'POST' && p === '/authors') {
const b = await readBody(req);
if (typeof b.name !== 'string' || typeof b.email !== 'string') return json(res, 400, { error: 'invalid' });
const i = db.prepare('INSERT INTO authors (name, email, created) VALUES (?, ?, ?)').run(b.name, b.email, now());
return json(res, 200, row('authors', i.lastInsertRowid));
}
if (method === 'GET' && p === '/authors') return json(res, 200, db.prepare('SELECT * FROM authors').all());
if (method === 'POST' && p === '/posts') {
const b = await readBody(req);
if (typeof b.title !== 'string' || typeof b.body !== 'string' || typeof b.author !== 'number' || typeof b.published !== 'boolean') return json(res, 400, { error: 'invalid' });
const i = db.prepare('INSERT INTO posts (title, body, author, published, created) VALUES (?, ?, ?, ?, ?)').run(b.title, b.body, b.author, b.published ? 1 : 0, now());
return json(res, 200, row('posts', i.lastInsertRowid));
}
if (method === 'GET' && p === '/posts') return json(res, 200, db.prepare('SELECT * FROM posts ORDER BY created').all());
if (method === 'GET' && mPost) { const r = row('posts', mPost[1]); return r ? json(res, 200, r) : json(res, 404, { error: 'post not found' }); }
if (method === 'PUT' && mPost) {
const b = await readBody(req);
if (typeof b.title !== 'string' || typeof b.body !== 'string') return json(res, 400, { error: 'invalid' });
db.prepare('UPDATE posts SET title = ?, body = ? WHERE id = ?').run(b.title, b.body, mPost[1]);
return json(res, 200, row('posts', mPost[1]));
}
if (method === 'DELETE' && mPost) { db.prepare('DELETE FROM posts WHERE id = ?').run(mPost[1]); return json(res, 204, undefined); }
if (method === 'POST' && mComments) {
const b = await readBody(req);
if (typeof b.text !== 'string') return json(res, 400, { error: 'invalid' });
const i = db.prepare('INSERT INTO comments (post, text, created) VALUES (?, ?, ?)').run(Number(mComments[1]), b.text, now());
return json(res, 200, row('comments', i.lastInsertRowid));
}
if (method === 'GET' && mComments) return json(res, 200, db.prepare('SELECT * FROM comments WHERE post = ?').all(Number(mComments[1])));
if (method === 'GET' && p === '/stats') return json(res, 200, { authors: db.prepare('SELECT COUNT(*) n FROM authors').get().n, posts: db.prepare('SELECT COUNT(*) n FROM posts').get().n, comments: db.prepare('SELECT COUNT(*) n FROM comments').get().n });
if (method === 'GET' && p === '/health') return json(res, 200, { status: 'up' });
json(res, 404, { error: 'not found' });
});
server.listen(Number(process.env.PORT), () => console.log(`serving on http://localhost:${server.address().port}`));
(env PORT DATABASE_URL AUTH_TOKEN)
(server api :port PORT
(db users {name: str, email: str unique, created: time auto})
(auth :bearer)
(route GET "/users"
(ok (db.all users :order name :limit 20)))
(route POST "/users" {name email}
(ok (db.add users {name email}))))
const app = new Hono();
const users = defineTable(db, "users", { columns: [
{ name: "name", type: "str" },
{ name: "email", type: "str", unique: true },
{ name: "created", type: "time", auto: true },
] });
const bearer = requireBearer(env.AUTH_TOKEN);
app.get("/users", bearer, async (c) =>
reply(c, ok(dbAll(users, { order: "name", limit: 20 }))));
app.post("/users", bearer, async (c) => {
const _b = await readBody(c, { name: "str", email: "str" });
if (!_b.ok)
return c.json({ error: "invalid body", fields: _b.errors }, 400);
const { name, email } = _b.value;
return reply(c, ok(dbAdd(users, { name, email })));
});
serve(app, env.PORT);
(env PORT DATABASE_URL AUTH_TOKEN)
(server api :port PORT
(db authors {name: str, email: str unique, created: time auto})
(db posts {title: str, body: str, author: num, published: bool, created: time auto})
(db comments {post: num, text: str, created: time auto})
(auth :bearer)
(route POST "/authors" {name email} (ok (db.add authors {name email})))
(route GET "/authors" :public (ok (db.all authors)))
(route POST "/posts" {title body author published}
(ok (db.add posts {title body author published})))
(route GET "/posts" :public (ok (db.all posts :order created)))
(route GET "/posts/:id" :public
(let p (db.get posts :id)) (if p (ok p) (err 404 "post not found")))
(route PUT "/posts/:id" {title body} (ok (db.set posts :id {title body})))
(route DELETE "/posts/:id" (db.del posts :id) (ok))
(route POST "/posts/:id/comments" {text}
(ok (db.add comments {post: (toNum id), text})))
(route GET "/posts/:id/comments" :public
(ok (db.find comments {post: (toNum id)})))
(route GET "/stats" :public
(ok {authors: (len (db.all authors)), posts: (len (db.all posts)), comments: (len (db.all comments))})))
import http from "node:http";
import Database from "better-sqlite3";
const db = new Database(process.env.DATABASE_URL || ":memory:");
db.exec(`CREATE TABLE authors (...);
CREATE TABLE posts (...);
CREATE TABLE comments (...);`);
const authed = (req) => /Bearer (.+)/.test(req.headers.authorization || "");
const server = http.createServer(async (req, res) => {
// bearer-gate every write, read + validate JSON bodies, hand-match
// /posts/:id, /posts/:id/comments and /stats, run the SQL, pick the
// right status code for each case, 404s, 401s...
// ~50 lines of routing, validation and SQL for the same three tables.
});
server.listen(Number(process.env.PORT));
Tutorials
From install to an AI agent, in five short steps.
Drop the cheat sheet into a model's context and it writes KernScript without ever having seen it. Or learn it yourself in a few minutes, since every concept fits on one screen.
Install and run your first program
Clone the repo, install once, and run a .ks file. The CLI type-checks, transpiles in memory and executes. There is no separate build step.
git clone https://github.com/lopezonchain/kernscript
cd kernscript && npm install
npx ks run hello.ks
(let who "world")
(print "Hello, {who}!") ; string interpolation is built in
Values, lists and pipelines
Thread a value through a series of transformations with the pipeline form. No intermediate variables, and no nested calls to read from the inside out.
(let orders [{total: 120 paid: true} {total: 80 paid: false} {total: 200 paid: true}])
(let revenue (-> orders
(filter (fn [o] o.paid)) ; keep only the paid orders
(map :total) ; pluck the total field
(sum))) ; add them up, result is 320
(print "revenue: {revenue}")
Fetch data and handle errors as values
fetch is built in. It parses JSON and throws on a non-2xx response. Wrap anything that can fail in try, and return a Result instead of throwing across your whole app.
(fn user-name [id]
(try
(let u (fetch "https://api.example.com/users/{id}"))
(ok u.name)
(catch e (err :fetch e.message))))
(match (user-name 7)
{ok: true} (print "found the user")
_ (print "lookup failed"))
A REST API with a database
A single server form gives you a Hono app on SQLite, with a typed schema, automatic body validation, bearer auth and additive migrations. No glue code to write.
(env PORT DATABASE_URL AUTH_TOKEN)
(server api :port PORT
(db tasks {title: str, done: bool, created: time auto})
(auth :bearer)
(route GET "/tasks" (ok (db.all tasks :order created :desc)))
(route POST "/tasks" {title done} (ok (db.add tasks {title done})))
(route GET "/tasks/:id"
(let t (db.get tasks :id))
(if t (ok t) (err 404 "not found"))))
An AI agent with a tool
Tools, agents and the tool-calling loop are part of the language. Inputs and outputs are validated, permissions are enforced, and ask drives the whole loop for you across Claude, GPT, Gemini or Ollama.
(tool add
(desc "Add two numbers")
(in {a: num, b: num}) (out {sum: num})
{sum: (+ a b)})
(agent helper
(model "claude-sonnet-4-6")
(tools [add]) (permission []))
(let r (ask helper "What is 19 + 23?"))
(print r.text)
What is in the box
Small to write. Complete enough to ship to production.
Uniform syntax
Everything is one shape, (form args). Lists, records and optional commas. There is nothing to memorize.
Async, inferred
The compiler infers async: it awaits the calls that suspend and leaves pure code synchronous. You never write await, then or Promise.all.
Pipelines and match
Thread values with a pipeline, branch with match, and handle failure with Result.
Type checker
Type inference with zero annotations, and one-line errors a model fixes in a single pass.
Server, database and auth
A full CRUD API on Hono and SQLite with validation, bearer auth and cron, in under 30 lines.
Agents and MCP
Tools, agents and the tool-calling loop across Claude, GPT, Gemini and Ollama.
Modules, npm and the browser
Split across .ks files with cross-file type checking, import any npm package, and target the browser.
Hardened runtime
An SSRF-guarded fetch, parameterized SQL, constant-time auth, and capability permissions.