Open the app

Your idea, in copper.

Volt Zero is a circuit board editor that runs in your browser. You write the board in a small language with real units; Volt checks and routes it and draws it in 3D. Your agent can edit the same board while you work on it. There’s no account and nothing to install.

A six-layer circuit board in green solder mask with gold pads, drawn in 3D by Volt's renderer: a BGA processor, QFN chips, memory, a crystal, decoupling capacitors, LEDs, headers, rows of connector pads and plated mounting holes.
An open-hardware board, imported and drawn in 3D by Volt’s renderer.

From a sentence to a board, in 36 seconds.

One Rust core, on your machine and in your browser.

Everything Volt does lives in one Rust codebase, from the language to the renderer. It builds into the volt command line, and into WebAssembly for the app.

  • Give the router the same work budget and a route comes out byte for byte the same in the browser and on the command line, on any machine and with any number of threads. A test checks this on every fixture board.
  • The same renderer draws the board everywhere: on WebGPU or WebGL2 in the browser, and to PNG files from the command line.
  • An edit reaches the screen in 4–26 ms on boards up to 6 MB.
One Rust core (the language, checks, fill, router, renderer and command registry), with copper buses to its two ends: the command line and the browser, where it runs as WebAssembly.coreRustCLInativebrowserWebAssembly
In the core: language · checks · fill · router · renderer · commands.

A board is a text file you can review in a pull request.

A board is design.volt plus a few block files in a git repository. You say what’s on the board and how it’s connected, and Volt works out where the copper goes. The file below is a real one. It evaluates without errors and routes to completion.

design.volt2 layers · 14 parts · 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 has a dimension: 0.5mm, 100nF, 10kohm, 90deg. Add millimetres to nanofarads and you get an error on that line.

  • Typed nets and pins

    Nets, buses and differential pairs, with pins checked by name. Connect two nets by mistake and Volt reports a short.

  • Parts and blocks

    A part is something you buy. A block is a reusable circuit with parameters and its own coordinate frame.

  • Footprints, computed

    82 land patterns, computed from package dimensions per IPC-7351B. Of 14 checked against a reference library, 13 match pad for pad to within 1 µm.

  • Classes, rules, pours

    Net classes, typed rules with selectors, keepouts and rule areas. Copper pours get thermal reliefs.

  • Room to move

    Say where a part may move, how it may rotate and which pins may swap, and the tools stay within those limits.

There’s a formatter and a language server too, so you get errors, hover, completion and go-to-definition in your editor as well as in the app.

Route the same board twice and you get the same copper.

The router is a deterministic search. Most of its work comes from the six ideas below, and all of them are on by default.

  1. Negotiate: All nets are routed at once, overlapping at first. Each pass makes contested space more expensive, until no two nets share it. The search grid has eight directions and spans every layer.NET ANET BGAP COST ↑B DETOURS

    Negotiate

    All nets are routed at once, overlapping at first. Each pass makes contested space more expensive, until no two nets share it. The search grid has eight directions and spans every layer.

  2. Exact where it counts: The grid guides the search, but every clearance is checked against exact geometry, to a fraction of a micron. Fine-pitch escapes with zero slack still fit.exact gap0.4 mm pitch · 0.2 mm track · 0.3 µmgrid

    Exact where it counts

    The grid guides the search, but every clearance is checked against exact geometry, to a fraction of a micron. Fine-pitch escapes with zero slack still fit.

  3. Search the neighbourhood: Next it rips up a window around whatever is still unrouted, routes it again and keeps the result if it is better. Eight candidate states are searched side by side.RIP UP A WINDOWROUTE IT AGAINkept if better

    Search the neighbourhood

    Next it rips up a window around whatever is still unrouted, routes it again and keeps the result if it is better. Eight candidate states are searched side by side.

  4. Pours, live: Copper pours are modelled while routing: the fill, the clearance around other nets, islands and thermal spokes. The router knows which pads a pour actually connects.THERMAL SPOKESISLANDdroppedother net, cut out

    Pours, live

    Copper pours are modelled while routing: the fill, the clearance around other nets, islands and thermal spokes. The router knows which pads a pour actually connects.

  5. Pad access as a set: A pad on a dense part gets up to eight exact, full-width exits. Neighbouring pads negotiate for them instead of each getting a fixed stub.8 WAYS OUT, EXACT, FULL WIDTHneighbours negotiate for them

    Pad access as a set

    A pad on a dense part gets up to eight exact, full-width exits. Neighbouring pads negotiate for them instead of each getting a fixed stub.

  6. Respects the mask: It knows where the solder mask opens, which vias are untented and where thin mask webs sit between pads, and routes so the fab can keep them.MASK WEBUNTENTED VIATENTED

    Respects the mask

    It knows where the solder mask opens, which vias are untented and where thin mask webs sit between pads, and routes so the fab can keep them.

Measured against an independent checker.

We measure every change to the router, the checks and file import on public boards that people designed. A separate design-rule checker judges the result, and a board only counts if it comes out clean.

  • 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

Your agent works in the same app.

Use the agent you already have: Claude Code, Codex, OpenCode, Cursor, Gemini CLI, or anything else that can drive Chrome over the DevTools Protocol, an extension or a script. It doesn’t click around the interface or read pixels. It calls an API on the page with the same commands you run from the palette.

  • Each command has a JSON Schema for its input and output, and says what it changes.
  • A journal shows what changed since the agent’s last turn. Long tasks run as jobs. The agent can also see your view, down to what you’ve selected, and every change it makes can be undone.
  • The app’s /agents.md and /llms.txt are generated from the commands. Browsers with WebMCP get the commands as tools.
agent · on the page
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 turn

From a terminal, the volt command line runs the same commands.

No account and nothing to install.

  • Open it and start

    Go to app.volt0.ai. There’s no sign-up, and once the page has loaded it keeps working offline.

  • Your files stay with you

    Open a board’s git repository from a folder on your computer and edit it in place, or keep the project in the browser’s storage.

  • Nothing on our servers

    Checks, routing, import and export run on your machine in WebAssembly workers. Your project only leaves it when you export it.

From a sentence to a board you can hold.

Tell your agent what the board should do. It writes the design, Volt routes it, and you watch it take shape in 3D. Today a finished board leaves Volt in the standard open board file format, with its project and rules, so you can use the fab tools you already have.

We’re working on the rest: Gerber and drill files, a bill of materials and pick-and-place for assembly, and ordering from JLCPCB inside the app.

  1. Say what you need

    “A USB sensor node, 50 × 30 mm, two layers, an I2C port.”

  2. Your agent writes the design

    Parts and where they sit, nets and rules, in design.volt. Volt checks it as it is written.

  3. Volt routes it

    Every net, the same way every time, until the checks come out clean. You see the board in 3D.

  4. Fabrication filesnext

    Gerber for every layer and the outline, drill files, a bill of materials and pick-and-place.

  5. Order the boardnext

    Finish, mask colour, copper weight and quantity, checked against what the fab can make, and sent to JLCPCB from the app.

Your idea, in copper.

In your browser. No sign-up, nothing to install.

OPENTHE APPVOLT0 · U1

Who makes it

I’m Sergey Alekseev. I’m also building Rabbit, an autonomous rover, and I write about it atrabbit0.dev. Volt started as the tool I wanted for Rabbit’s boards, something my agent and I could both work in.