Pay-per-run Lua. Every call boots a hardware-isolated microVM running an
NT-compatible kernel, runs your code, settles a fraction of a cent of USDC on
Algorand, hands back the result, and then deletes the VM. All of that, to
compute 2 + 2.
It is "serverless" in the sense that there is, in fact, a server — it's just a MicroNT + LuaJIT QEMU microVM that exists for a few seconds and is then never spoken of again.
Grab the lualambda binary from the Releases page and make it
executable. It's one self-contained file — the Bun runtime and the MicroNT kernel
and initrd are baked in, so there's nothing else to download. The VM features
need qemu-system-x86_64 on your PATH; the wallet and client bits don't.
tar xzf lualambda-*-linux-x64.tar.gz
chmod +x lualambda
./lualambda --helpRun some Lua in a real microVM — no server, no wallet, no payment:
echo 'return 2 + 2' | ./lualambda invoke --local-test
# -> 4That booted a microVM, ran your Lua inside it, framed the result back over a
socket, and tore the VM down. By default you get just the result (so | jq is
happy); add -v for the id and timing.
A bare expression, a returned value, or a full function(args) all work —
whatever you return is the JSON output:
echo 'return { hello = "world" }' | ./lualambda invoke --local-test
echo 'return function(a) return "hi "..(a[1] or "there") end' \
| ./lualambda invoke --arg Algorand --local-testAdd --attach and the VM stays alive after running — you land in a Lua REPL on
its serial console and can poke around like it's a tiny computer, because it is
one:
lualambda invoke --local-test --attach # a bare Lua REPL in a local microVM
lualambda invoke --local-test --attach --pkg ./mylib # ...with your package there to require()LuaJIT 2.1 -- Copyright (C) 2005-2026 Mike Pall. https://luajit.org/
> require('nt.dll.ps').getpid()
1
> print(1 + 1)
2
Drop --local-test to run it as a paid session on an orchestrator instead. Those
are multi-attach: lualambda attach <id> joins a running one, so several people
can share the same terminal. Ctrl-] detaches. The session ends when its
wall-clock or output cap is hit (you're renting CPU, after all).
The same executable is the client, the wallet, and the orchestrator. The first argument picks the role:
lualambda invoke … # run code (locally with --local-test, or against a server)
lualambda serve # be the orchestrator — the HTTP API (needs QEMU)
lualambda wallet … # an Algorand wallet (create / fund / opt-in / status)The real loop: the client signs a USDC payment in response to an HTTP 402, the orchestrator verifies and settles it through the GoPlausible facilitator, boots the VM, and returns your result plus an on-chain receipt.
# 1. A throwaway payer wallet (prints an address + a QR you can fund from a phone).
lualambda wallet create
lualambda wallet opt-in # opt the payer into testnet USDC (ASA 10458941)
# fund it: ALGO https://lora.algokit.io/testnet/fund
# USDC https://faucet.circle.com/
lualambda wallet status # check balances
# 2. Start an orchestrator that enforces payment. The receiver must also be
# opted into USDC to receive it.
lualambda serve --pay-to <your-receiver-address>
# 3. In another terminal: 402 -> sign USDC -> settle -> VM -> result + receipt.
echo 'return function(a) return { greeting = "hello "..(a[1] or "world") } end' \
| lualambda invoke --arg Algorand --profile nano -v
# -> { "greeting": "hello Algorand" }
# settled: <txid> https://lora.algokit.io/testnet/tx/<txid>The facilitator fee-sponsors the payment group, so neither side needs ALGO for
the transfer (only a little for the one-time opt-in). Drop --pay-to and the
orchestrator runs free. The payer key lives at ~/.config/lualambda/wallet.json
(or LUALAMBDA_MNEMONIC) — testnet throwaway keys only, please.
There's no "deployed function." An invocation is a set of package zips + a module
to require + an array of args, keyed by an opaque id (a hash of the inputs, or
any nametag you pick). The zips land in the guest's \SystemRoot\pkg\, and
MicroNT's Lua loader resolves require('blah.dorp') to
\SystemRoot\pkg\blah.zip\blah\dorp.lua.
GET /invoke discovery: profiles, prices, URL scheme
GET /invoke/:id status (state + hashes + expiry)
POST /invoke/:id/:profile priced, x402-gated; one paid profile per id
multipart: package zips + JSON spec { require, args }
GET /invoke/:id/output retained output for the profile's window; 410 once gone
You choose the profile — and so the price, CPU/memory, retention window, and output cap — at pay time, via the URL. You can't pay twice for the same id (you get a 409).
Guests share QEMU's user-mode network, so each VM has to present a per-instance connect-back token before the orchestrator hands it any code or accepts a result. One guest can't read or poison another's invocation.
Everything runs inside the devcontainer (supply-chain isolation, QEMU in TCG mode, a throwaway testnet key).
bun install
bun test
bun run dev # orchestrator on :8402
bun run cli -- invoke --pkg ./examples/hello --require hello --arg Algorand --local-test--pkg takes a directory (zipped in-process) or an existing .zip (uploaded
verbatim). With no --pkg, Lua is read from stdin or a single .lua file.
build.sh compiles the single binary into build/<target>/ — Linux x64 by
default; pass a Bun target (e.g. darwin-arm64, windows-x64) for others:
./build.shCI builds and tests on every push. Pushing a v* tag publishes a GitHub Release
with the binary attached and a signed build-provenance attestation.
Design notes live in OUTLINE.md. There are also hackathon slides.