Circuit board design, with your agent
Your idea, in copper.
Describe the board in a small language with units and typed nets. Volt checks it, routes it and draws it in 3D, in your browser, with your agent working beside you.

One Rust core, on your machine and in your browser.
The language, the checks, the pour fill, the router, the renderer and the commands are one Rust core. It runs natively as the volt command line, and compiled to WebAssembly it runs the app in your browser.
- The same answers. Under a work budget, a route is identical byte for byte in the browser and on the command line, whatever the machine and the number of threads. A test holds it on every fixture board.
- One renderer. The same code draws the board everywhere: 2D, and 3D as it will be made, on WebGPU or WebGL2 in the browser and to PNG on the command line.
- Fast where you work. An edit reaches the board on screen in 4–26 ms on boards up to 6 MB.
A board is text you can read, diff and review.
A board lives in design.volt and a few block files in a git repository. The Volt language is declarative and small: you say what the board is, and Volt works out where the copper goes. This one evaluates with no errors and routes to complete.
use "blocks/support.volt" as support
net GND, VBUS
net V3 named "+3V3"
net USB_DP, USB_DN
net STATUS
bus I2C[2]
pair USB = (USB_DP, USB_DN)
board = Board {
name = "sensor-node"
outline = rect(50mm, 30mm)
cutouts = [circle((3.5mm, 26.5mm), 1.6mm), circle((46.5mm, 26.5mm), 1.6mm)]
layers = [signal(Cu.top), signal(Cu.bottom)]
}
usb = NetClass {
trackWidth = 0.2mm
diffPairGap = 0.2mm
nets = [USB]
}
ldo = support.Ldo(VBUS, V3, GND, bulk = 4.7uF) { at = (14mm, 24mm) }
U1 = Mcu {
at = (25mm, 14mm)
pins { VDD: V3, DP: USB.p, DN: USB.n, SDA: I2C[0], SCL: I2C[1], LED: STATUS, GND: GND }
}
status = support.StatusLed(STATUS, GND) { at = (34mm, 6mm) }
sensor = support.SensorPort(V3, I2C[0], I2C[1], GND) { at = (46mm, 15mm) }
gnd = pour(net = GND, layers = [Cu.bottom]) { outline = board.outline }
rule UsbPair {
select = copper(pair = USB)
limits { trackWidth: limit(min = 0.2mm), diffPairGap: limit(min = 0.15mm, max = 0.25mm) }
}
Show the whole file (78 lines)
use "blocks/support.volt" as support
// USB-powered sensor node: 3.3 V regulator, QFN-32 MCU, I2C sensor port and SWD port.
net GND, VBUS
net V3 named "+3V3"
net USB_DP, USB_DN
net STATUS
bus I2C[2]
pair USB = (USB_DP, USB_DN)
board = Board {
name = "sensor-node"
outline = rect(50mm, 30mm)
cutouts = [circle((3.5mm, 26.5mm), 1.6mm), circle((46.5mm, 26.5mm), 1.6mm)]
layers = [signal(Cu.top), signal(Cu.bottom)]
}
supply = NetClass {
trackWidth = 0.5mm
nets = [VBUS, V3, GND]
}
usb = NetClass {
trackWidth = 0.2mm
diffPairGap = 0.2mm
nets = [USB]
}
part Mcu = device(footprint = qfn(32, size = (5mm, 5mm), ep = (3.1mm, 3.1mm)), pinout = {
VDD: ["1", "17"],
DN: "3",
DP: "4",
NRST: "8",
SDA: "21",
SCL: "22",
LED: "24",
SWDIO: "29",
SWCLK: "30",
GND: "33",
})
part C100n = cap(100nF, package = "0402") // one per supply pin
J1 = header(4) {
at = (4mm, 15mm)
pins = [VBUS, USB.n, USB.p, GND]
}
ldo = support.Ldo(VBUS, V3, GND, bulk = 4.7uF) { at = (14mm, 24mm) }
U1 = Mcu {
at = (25mm, 14mm)
pins { VDD: V3, DP: USB.p, DN: USB.n, SDA: I2C[0], SCL: I2C[1], LED: STATUS, GND: GND }
}
decoupling = {
for ((i, p) in enumerate([(21mm, 17mm), (29mm, 11mm)])) {
[i]: C100n { at = p; rotation = 90deg; pins { a: V3, b: GND } },
}
}
J3 = header(5, pitch = 1.27mm) {
// debug port, joined to the MCU's pins directly
at = (25mm, 3mm)
rotation = 90deg
pins = [V3, U1.pin.SWDIO, U1.pin.SWCLK, U1.pin.NRST, GND]
}
R1 = res(10kohm, package = "0402") {
at = (20mm, 8mm)
pins { a: V3, b: U1.pin.NRST }
}
status = support.StatusLed(STATUS, GND) { at = (34mm, 6mm) }
sensor = support.SensorPort(V3, I2C[0], I2C[1], GND) { at = (46mm, 15mm) }
gnd = pour(net = GND, layers = [Cu.bottom]) { outline = board.outline }
rule UsbPair {
select = copper(pair = USB)
limits { trackWidth: limit(min = 0.2mm), diffPairGap: limit(min = 0.15mm, max = 0.25mm) }
}
Units
Every value carries its dimension: 0.5mm, 100nF, 10kohm, 90deg. Mixing them up is an error, not a surprise.
Typed nets and pins
Nets, buses and differential pairs; pins checked by name. Two nets joined by accident are a short.
Parts and blocks
A part is what you buy; a block is a reusable circuit with parameters and its own frame.
Footprints, computed
82 land patterns computed from package dimensions per IPC-7351B, 13 of 14 checked against a reference library identical pad for pad to 1 µm.
Classes, rules, pours
Net classes, typed rules with selectors, keepouts, rule areas, and copper pours with thermal reliefs.
Freedom
Say where a part may move, turn or swap pins, and the tools stay inside it.
A formatter and a language server come with it: diagnostics, hover, completion and go-to-definition in your editor and in the app.
A router that negotiates, searches, and checks its own work.
The router is a search algorithm, not a model: it routes the same board the same way every time. Six ideas do most of the work; all of them are on by default.
Negotiate
Every net routes at once, overlaps and all. Space two nets want gets more expensive with every pass, until no two nets share it. The grid has eight directions and every layer.
Exact where it counts
The grid guides the search, but every clearance is decided by exact geometry, to a fraction of a micron. Zero-slack fine-pitch escapes fit.
Search the neighbourhood
Then it rips up a window around whatever is still open and routes it again, keeping what is better. Eight states search side by side.
Pours, live
It models each copper pour as it routes: the fill, the gaps around other nets, the islands, the thermal spokes. It knows which pads a pour really reaches.
Pad access as a set
A pad on a dense part gets up to eight exact, full-width ways out, and its neighbours negotiate for them instead of each taking a fixed stub.
Mindful of the mask
It knows the solder-mask openings, the untented vias and the thin green webs between pads, and routes so the fab can keep them.
Measured against an independent checker.
Every change to the router, the checks and the file import is measured on public boards people designed, and judged by an independent design-rule checker, not by Volt itself. A board counts only when it is fully clean: there is no partial credit.
96.1%
of 482 curated public boards import and export with every pad and every connection confirmed by an independent checker
standard open board files, both ways
232/235
human-designed boards where, after a refill, pour connectivity matches the independent checker
copper pours
4–26ms
from an edit to the board on screen, on boards up to 6 MB, in the browser
measured in a headless browser
13/14
reference footprints matched pad for pad to 1 µm by footprints computed per IPC-7351B
footprint library
You and your agent design it.
Your agent does not click through the interface or read the screen. It calls the app directly, through an API on the page: the same commands you run from the palette.
- Every command, described. Each has a JSON Schema for its input and result, and says what it changes.
- Work you can follow. A journal of what changed since the agent’s last turn, jobs for long work, and the view: what you see and what you selected. Every change says who made it and can be undone.
- Docs written for agents. The app’s
/agents.mdand/llms.txtare generated from the same commands. WebMCP where a browser offers it.
volt.hello("my-agent")
await volt.commands("route") // what it does, its schema
await volt.call("route", {}) // runs as a job
await volt.wait("job")
await volt.call("view.state", {}) // what the person sees
await volt.call("journal.since", {}) // what changed since my turnAny agent with a browser or a shell. From a terminal, the volt command line runs the same commands.
Nothing to install. Nothing stored on our side.
No account
Open the page and work. It keeps working offline once loaded.
Your files
Projects live in your browser’s storage or in a folder on your disk that you open in the app. A board is a folder of text files under git.
No server doing the work
Checking, routing, import and export run on your machine, in WebAssembly workers. No project data leaves it unless you export.
Describe it. Route it. Hold it.
Today a finished board leaves Volt in the standard open board file format, with its project and rules, ready for the fabrication tools you use now.
Coming next
- Fabrication outputs (Gerber, drill).
- Fab settings in the language: finish, mask and silkscreen colour, copper weight, checked against what the fab can make.
- Ordering boards from the fab, from the app.
Your idea, in copper.
Open the appWho makes it.
Volt Zero is made by Sergey Alekseev. He is building Rabbit, an autonomous rover, and keeps its build log at rabbit0.dev. Volt is the tool he wants for its boards: one he and his agent can work in together, from the idea to the factory.