← Pocket Rig
Pocket Rig API
Tokens

Drive Pocket Rig from your own code

Both metered lanes are available over HTTP: the build check (is this handheld build workable on this board, and on which pins) and the boot sketch (the first sketch wired to a given pin map). A natural use is a parts-list tool or a maker-space bot: your code sends the board, the parts and the page's reading, and gets back a verdict, a pin map and changes, or a complete sketch with libraries and bench checks.

The division of labour first: the page counts; the model judges and writes; the page re-checks every pin the model names. Reading the parts into buses and signals, the pin count, I2C address clashes, logic-level and strapping traps, the draft pin map, the rail loads and the runtime come from rigkit.js, which runs in node as well as in the browser. Every current there is a typical estimate for a common breakout module. Its check function holds each reply to the board: every pin must exist on the header, flash and input-only pins must not be misused, no pin may carry two parts unless it is a shared bus line, and the pins a sketch hard-codes must be on the map that was sent.

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"pocket-rig"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task / board / parts is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task: build or boot. Both share the rig fields; each adds its own. The model follows only the named task's contract.

TaskVerdictsReply fields
buildbuild (works as listed), adjust (works after should-level changes or with the page's warnings standing), rethink (cannot work as listed; at least one must change)title, verdict, summary, pins [{pin, part, signal, why}], changes [{level: must|should, what, why}], power (strings), order (strings), tips
bootflash (the sketch is ready), hold (the map has a must-fix problem; sketch is empty and blockers names each pin)title, verdict, summary, libraries [{name, why}], sketch (the full .ino source), pins [{pin, part, signal}], checks [{step, expect}], blockers (strings)

Pins are written the board's way: D7 / A0 on uno, GPIO23 on esp32, GP16 on pico. The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (RigKit.toWire does this). The schema requires task, board and parts.

Input fields

FieldLaneTypeNotes
taskbothstringbuild or boot.
today, rigbothstringYYYY-MM-DD; a nickname (may be empty).
boardbothstringpico, esp32 or uno.
wifibothstringon or off (counts only on esp32).
partsbothstringThe parts list as typed, one a line; past 3,000 characters the middle is cut with a marker.
factsbothstring (JSON){board, board_name, logic_v, wifi_on, usable_pins, pins_needed, shared_bus_pins, i2c, spi_devices, serial_devices, active_ma, peak_ma, rail_3v3, rail_5v, battery, mah, runtime_h, target_h, currents_are_estimates}.
part_listbothstring (JSON)[{id, line, name, qty, bus, signals, ma, peak_ma, logic, rail, addr}].
notesbothstringOptional; past 1,200 characters the middle is cut.
battery, mah, target_hbuildstringlipo, aa3, bank or usb; capacity; runtime wanted in hours.
page_map, unassignedbuildstring (JSON)The page's draft map [{pin, part, signal}] and the signals it could not place.
page_reads, page_verdictbuildstring (JSON), string[{level, code, at, text}] with level fix / warn / info; the page's own verdict.
pin_map, map_sourcebootstring (JSON), stringThe map to wire to, [{pin, part, signal}]; pasted map or page draft.
map_reads, map_verdictbootstring (JSON), stringThe page's check of that map; flash or hold.
goalbootstringWhat the first sketch should prove.
retry_notebothstringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with RigKit.parseReply, then normalize(reply, task), then check(reply, state, task), which returns {notes, rows, mapReads}: the page's notes (info, warn, fix) shown under "Checked by the page", the reply's pins in the board's names, and the map check of the builder's pins.

Worked examples

Replies produced against the app's prompt for the sample rigs (made up; lists shortened, sketches folded; the full replies ship in example-result.js). Together they cover every verdict. The page replays each of them free from its sample buttons.

task: build - RP2040 pocket pager - build

Input:

{
 "task": "build",
 "today": "2026-10-20",
 "rig": "Trail pager",
 "board": "pico",
 "wifi": "off",
 "parts": "SSD1306 OLED 128x64\nLoRa SX1276 module\n4 buttons\npiezo buzzer\nTP4056 charger board",
 "facts": "{\"board\":\"pico\",\"board_name\":\"RP2040 Pico-class board (3.3 V logic)\",\"logic_v\":3.3,\"wifi_on\":false,\"usable_pins\":26,\"pins_needed\":13,\"shared_bus_pins\":5,\"i2c\":[{\"part\":\"OLED 128x64 (I2C)\",\"addr\":\"0x3C\"}],\"spi_devices\":1,\"serial_devices\":0,\"active_ma\":75,\"peak_ma\":180,\"rail_3v3\":{\"load\":75,\"peak\":180,\"limit\":300},\"rail_5v\":{\"load\":0,\"peak\":0,\"limit\":450},\"battery\":\"single Li-ion / LiPo cell (3.7 V)\",\"mah\":1200,\"runtime_h\":12.8,\"target_h\":8,\"currents_are_estimates\":true}",
 "part_list": "[{\"id\":\"P1\",\"line\":\"SSD1306 OLED 128x64\",\"name\":\"OLED 128x64 (I2C)\",\"qty\":1,\"bus\":\"i2c\",\"signals\":[],\"ma\":20,\"peak_ma\":20,\"logic\":\"any\",\"rail\":\"3v3\",\"addr\":\"0x3C\"},{\"id\":\"P2\",\"line\":\"LoRa SX1276 module\",\"name\":\"LoRa radio (SPI)\",\"qty\":1,\"bus\":\"spi\",\"signals\":[\"CS\",\"RST\",\"IRQ\"],\"ma\":15,\"peak_ma\":120,\"logic\":\"3v3\",\"rail\":\"3v3\",\"addr\":null},\"... 3 more\"]",
 "notes": "Pocket messenger for hikes with a friend. Text on the OLED, four buttons to pick canned messages.",
 "battery": "lipo",
 "mah": "1200",
 "target_h": "8",
 "page_map": "[{\"pin\":\"GP4\",\"part\":\"I2C bus\",\"signal\":\"SDA\"},{\"pin\":\"GP5\",\"part\":\"I2C bus\",\"signal\":\"SCL\"},\"... 11 more\"]",
 "unassigned": "[]",
 "page_reads": "[]",
 "page_verdict": "build"
}

Reply:

{
 "task": "build",
 "title": "Trail Pager: Pico OLED and LoRa Build",
 "verdict": "build",
 "summary": "This build works on the Pico as listed, with no must-have changes. The OLED, LoRa radio, four buttons and buzzer are all 3.3 V parts, so logic levels match the board, and with one I2C address and one SPI bus there are no clashes. The 3.3 V rail has real headroom - facts show 75 mA typical and 180 mA peak against a 300 mA limit - and the estimated 12.8 h runtime clears your 8 h target by a wide margin. Wire the OLED first so you can see every later step, then bring up the radio, buttons and buzzer one at a time.",
 "pins": [
  {
   "pin": "GP4",
   "part": "I2C bus",
   "signal": "SDA",
   "why": "shared I2C data line for the OLED"
  },
  {
   "pin": "GP5",
   "part": "I2C bus",
   "signal": "SCL",
   "why": "shared I2C clock line for the OLED"
  },
  {
   "pin": "GP18",
   "part": "SPI bus",
   "signal": "SCK",
   "why": "hardware SPI clock shared by the radio"
  },
  {
   "pin": "GP19",
   "part": "SPI bus",
   "signal": "MOSI",
   "why": "hardware SPI data out to the radio"
  },
  "..."
 ],
 "changes": [
  {
   "level": "should",
   "what": "Add a 100-220 microfarad capacitor across 3V3 and GND right at the LoRa module",
   "why": "smooths the jump from its idle draw to its transmit peak so the OLED doesn't glitch when the radio keys up"
  },
  {
   "level": "should",
   "what": "Debounce the four buttons in software with a short time check",
   "why": "mechanical buttons bounce for a few milliseconds and could double-trigger a canned message"
  }
 ],
 "power": [
  "Facts put the 3.3 V rail at 75 mA typical and 180 mA peak, against a 300 mA limit - comfortable headroom for the radio's transmit spikes.",
  "Nothing here needs 5 V - the OLED, radio, buttons and buzzer are all 3.3 V parts - so the 5 V rail stays at 0 mA load and 0 mA peak.",
  "..."
 ],
 "order": [
  "Wire the OLED first on the I2C bus (GP4 SDA, GP5 SCL) and confirm it lights up - you'll use it to watch every later step.",
  "Add the LoRa radio on the shared SPI bus (GP18 SCK, GP19 MOSI, GP16 MISO) plus its own CS on GP17, RST on GP2 and IRQ on GP3.",
  "Wire the four buttons to GP6, GP7, GP10 and GP11 using the Pico's internal pull-ups, one at a time, checking each reads low when pressed.",
  "..."
 ],
 "tips": [
  "Keep the LoRa antenna lead short and away from the OLED ribbon to cut noise.",
  "..."
 ]
}

task: build - ESP32 field deck - adjust

Input:

{
 "task": "build",
 "today": "2026-10-20",
 "rig": "Field deck",
 "board": "esp32",
 "wifi": "on",
 "parts": "2.8in ILI9341 TFT\nI2C keyboard\nGPS NEO-6M\nmicroSD card\nBME280\nrotary encoder\nbuzzer\nWS2812 x4",
 "facts": "{\"board\":\"esp32\",\"board_name\":\"ESP32 DevKit (38-pin, 3.3 V logic, Wi-Fi)\",\"logic_v\":3.3,\"wifi_on\":true,\"usable_pins\":24,\"pins_needed\":17,\"shared_bus_pins\":7,\"i2c\":[{\"part\":\"I2C keyboard\",\"addr\":\"0x5F\"},\"...\"],\"spi_devices\":2,\"serial_devices\":1,\"active_ma\":428,\"peak_ma\":628,\"rail_3v3\":{\"load\":348,\"peak\":548,\"limit\":600},\"rail_5v\":{\"load\":80,\"peak\":80,\"limit\":450},\"battery\":\"single Li-ion / LiPo cell (3.7 V)\",\"mah\":2000,\"runtime_h\":3.7,\"target_h\":6,\"currents_are_estimates\":true}",
 "part_list": "[{\"id\":\"P1\",\"line\":\"2.8in ILI9341 TFT\",\"name\":\"TFT display (SPI)\",\"qty\":1,\"bus\":\"spi\",\"signals\":[\"CS\",\"DC\",\"RST\",\"LED\"],\"ma\":90,\"peak_ma\":90,\"logic\":\"3v3\",\"rail\":\"3v3\",\"addr\":null},{\"id\":\"P2\",\"line\":\"I2C keyboard\",\"name\":\"I2C keyboard\",\"qty\":1,\"bus\":\"i2c\",\"signals\":[],\"ma\":6,\"peak_ma\":6,\"logic\":\"any\",\"rail\":\"3v3\",\"addr\":\"0x5F\"},\"... 6 more\"]",
 "notes": "Clamshell deck for logging walks: map-less GPS readout, weather, a notes keyboard. Wi-Fi only to sync logs at home.",
 "battery": "lipo",
 "mah": "2000",
 "target_h": "6",
 "page_map": "[{\"pin\":\"GPIO21\",\"part\":\"I2C bus\",\"signal\":\"SDA\"},{\"pin\":\"GPIO22\",\"part\":\"I2C bus\",\"signal\":\"SCL\"},\"... 15 more\"]",
 "unassigned": "[]",
 "page_reads": "[{\"level\":\"warn\",\"code\":\"level-5v\",\"at\":[\"P8\"],\"text\":\"Addressable RGB LEDs expects a 5 V data signal; 3.3 V often works on a short wire but a level shifter makes it reliable.\"},{\"level\":\"warn\",\"code\":\"no-boost\",\"at\":[],\"text\":\"Some parts want a 5 V supply and one cell gives 3.0-4.2 V - add a boost converter for the 5 V rail.\"},\"... 2 more\"]",
 "page_verdict": "adjust"
}

Reply:

{
 "task": "build",
 "title": "ESP32 field-log deck: GPS, weather, notes",
 "verdict": "adjust",
 "summary": "This board can carry every part on your list, but two things need attention before it is solid. GPIO2 is the only free output pin left, so the LED strip's data line lands on a boot-strapping pin - give it a small safeguard so it cannot hold the board's boot pin high at power-on. A single LiPo cell has no built-in 5 V rail, so the WS2812 LEDs need a boost converter, and a level shifter on their data line makes them reliable rather than merely working on a short wire. Expect roughly 3.7 hours of the 6 you want on a 2000 mAh cell, so plan to recharge mid-walk or carry a bigger cell. Wire and prove the screen first, then the shared buses, and add the LED strip last once its supply and safeguard are in place.",
 "pins": [
  {
   "pin": "GPIO21",
   "part": "I2C bus",
   "signal": "SDA",
   "why": "hardware I2C data line, shared by the keyboard and BME280"
  },
  {
   "pin": "GPIO22",
   "part": "I2C bus",
   "signal": "SCL",
   "why": "hardware I2C clock line, shared by the keyboard and BME280"
  },
  {
   "pin": "GPIO18",
   "part": "SPI bus",
   "signal": "SCK",
   "why": "hardware SPI clock, shared by the TFT and microSD"
  },
  {
   "pin": "GPIO23",
   "part": "SPI bus",
   "signal": "MOSI",
   "why": "hardware SPI data out, shared by the TFT and microSD"
  },
  "..."
 ],
 "changes": [
  {
   "level": "should",
   "what": "Add a 1-10 kΩ pull-down resistor from GPIO2 to ground at the LED strip's data input, or leave the strip's data wire disconnected until after the board has booted",
   "why": "GPIO2 is a boot-strapping pin and it is the only free output left for the LED strip; the page's own check flags that the strip could hold it high and stop the board starting or flashing."
  },
  {
   "level": "should",
   "what": "Add a small 5 V boost converter for the WS2812 strip, fed from the LiPo cell",
   "why": "a single Li-ion/LiPo cell gives 3.0-4.2 V, but the LEDs are counted on a 5 V rail in facts; without a boost converter they have no proper supply once off USB."
  },
  {
   "level": "should",
   "what": "Add a small logic-level shifter (or a 74HCT125 buffer) between GPIO2 and the LED strip's data line",
   "why": "the LEDs expect a 5 V data signal; 3.3 V often works on a short wire, but a shifter makes it reliable."
  },
  "..."
 ],
 "power": [
  "Typical draw across the whole build is about 428 mA, with peaks to about 628 mA (estimate).",
  "3.3 V rail: about 348 mA typical, 548 mA peak, against roughly a 600 mA budget - headroom is tight at peak.",
  "..."
 ],
 "order": [
  "Wire and test the TFT display alone over SPI (CS, DC, RST, LED) before anything else shares the bus.",
  "Add the microSD card on the same SPI bus with its own CS pin and confirm both can be read together.",
  "Wire the I2C bus and confirm the keyboard and BME280 both answer at their addresses (0x5F, 0x76).",
  "..."
 ],
 "tips": [
  "Turn Wi-Fi on only to sync logs at home - the current estimates in facts don't break out the radio's draw, so treat 3.7 h as a best case away from Wi-Fi use.",
  "..."
 ]
}

task: build - Uno-class slab - rethink

Input:

{
 "task": "build",
 "today": "2026-10-20",
 "rig": "Slab deck",
 "board": "uno",
 "wifi": "off",
 "parts": "3.5in ILI9488 TFT\nkeyboard matrix 6x8\nGPS NEO-6M\nLoRa RFM95\nmicroSD card\nMAX98357 I2S amp + speaker\nWS2812 x8",
 "facts": "{\"board\":\"uno\",\"board_name\":\"Uno-class board (ATmega328P, 5 V logic)\",\"logic_v\":5,\"wifi_on\":false,\"usable_pins\":18,\"pins_needed\":31,\"shared_bus_pins\":8,\"i2c\":[],\"spi_devices\":3,\"serial_devices\":1,\"active_ma\":507,\"peak_ma\":962,\"rail_3v3\":{\"load\":182,\"peak\":357,\"limit\":50},\"rail_5v\":{\"load\":325,\"peak\":605,\"limit\":450},\"battery\":\"single Li-ion / LiPo cell (3.7 V)\",\"mah\":2500,\"runtime_h\":3.9,\"target_h\":10,\"currents_are_estimates\":true}",
 "part_list": "[{\"id\":\"P1\",\"line\":\"3.5in ILI9488 TFT\",\"name\":\"TFT display (SPI)\",\"qty\":1,\"bus\":\"spi\",\"signals\":[\"CS\",\"DC\",\"RST\",\"LED\"],\"ma\":90,\"peak_ma\":90,\"logic\":\"3v3\",\"rail\":\"3v3\",\"addr\":null},{\"id\":\"P2\",\"line\":\"keyboard matrix 6x8\",\"name\":\"Key matrix\",\"qty\":1,\"bus\":\"matrix\",\"signals\":[\"ROW1\",\"ROW2\",\"ROW3\",\"ROW4\",\"ROW5\",\"ROW6\",\"COL1\",\"COL2\",\"COL3\",\"COL4\",\"COL5\",\"COL6\",\"COL7\",\"COL8\"],\"ma\":2,\"peak_ma\":2,\"logic\":\"any\",\"rail\":\"3v3\",\"addr\":null},\"... 5 more\"]",
 "notes": "First build. I want a full keyboard, a big screen, sound and a radio, in a laptop-style case.",
 "battery": "lipo",
 "mah": "2500",
 "target_h": "10",
 "page_map": "[{\"pin\":\"D13\",\"part\":\"SPI bus\",\"signal\":\"SCK\"},{\"pin\":\"D11\",\"part\":\"SPI bus\",\"signal\":\"MOSI\"},\"... 16 more\"]",
 "unassigned": "[{\"part\":\"I2S amplifier + speaker\",\"signal\":\"BCLK\"},{\"part\":\"I2S amplifier + speaker\",\"signal\":\"LRC\"},\"... 11 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"pins-over\",\"at\":[],\"text\":\"The parts need 31 signal pins and the Uno-class has 18 free - 13 short.\"},{\"level\":\"warn\",\"code\":\"soft-serial\",\"at\":[\"P3\"],\"text\":\"The Uno-class has no spare hardware serial port - GPS module (UART) would run on software serial (keep it at 9600 baud).\"},\"... 11 more\"]",
 "page_verdict": "rethink"
}

Reply:

{
 "task": "build",
 "title": "Slab deck needs a bigger board",
 "verdict": "rethink",
 "summary": "Slab deck, as planned, doesn't fit the Uno-class board: it needs 31 signal pins and the board has only 18 free, and the Uno-class has no I2S peripheral to drive the MAX98357 speaker. The 3.3 V parts also pull about 182 mA (estimate) against the board's roughly 50 mA regulator limit, and a single 3.7 V cell browns out a 5 V board as it drains. On the 2500 mAh cell, the page estimates about 3.9 hours of runtime against the 10-hour target - under half. Move to a 3.3 V board with far more pins and a native I2S output, then re-check power and runtime there before wiring anything.",
 "pins": [],
 "changes": [
  {
   "level": "must",
   "what": "Move off the Uno-class board onto a 3.3 V board with far more pins, such as an ESP32 or RP2040-class board.",
   "why": "the build needs 31 signal pins and the Uno-class has only 18 free, and the Uno-class has no I2S peripheral for the MAX98357 speaker."
  },
  {
   "level": "must",
   "what": "Give the 3.3 V parts (TFT display, LoRa radio) their own regulator instead of relying on the board's onboard 3.3 V rail.",
   "why": "the page's estimate puts the 3.3 V rail at about 182 mA typical, several times the board's roughly 50 mA regulator limit."
  },
  {
   "level": "must",
   "what": "Size the battery and add a boost or buck circuit for the whole build before trusting a runtime target.",
   "why": "on the 2500 mAh cell the page estimates about 3.9 hours against the 10-hour target - under half."
  },
  "..."
 ],
 "power": [
  "The 3.3 V rail is estimated at about 182 mA typical and 357 mA peak, against the board's roughly 50 mA regulator limit - several times over.",
  "The 5 V side peaks near an estimated 605 mA, more than the 450 mA a USB supply gives, so the LEDs and amplifier need their own 5 V supply.",
  "..."
 ],
 "order": [
  "Bring up the new 3.3 V board alone first, with no parts attached, and confirm uploads work.",
  "Wire and test the TFT screen first on the board's hardware SPI bus.",
  "Add the key matrix next and confirm every row and column scans cleanly.",
  "..."
 ],
 "tips": [
  "A laptop-style case has room for a bigger board - prioritize the screen and keyboard feel before chasing every peripheral.",
  "..."
 ]
}

task: boot - RP2040 pocket pager - flash

Input:

{
 "task": "boot",
 "today": "2026-10-20",
 "rig": "Trail pager",
 "board": "pico",
 "wifi": "off",
 "parts": "SSD1306 OLED 128x64\nLoRa SX1276 module\n4 buttons\npiezo buzzer\nTP4056 charger board",
 "facts": "{\"board\":\"pico\",\"board_name\":\"RP2040 Pico-class board (3.3 V logic)\",\"logic_v\":3.3,\"wifi_on\":false,\"usable_pins\":26,\"pins_needed\":13,\"shared_bus_pins\":5,\"i2c\":[{\"part\":\"OLED 128x64 (I2C)\",\"addr\":\"0x3C\"}],\"spi_devices\":1,\"serial_devices\":0,\"active_ma\":75,\"peak_ma\":180,\"rail_3v3\":{\"load\":75,\"peak\":180,\"limit\":300},\"rail_5v\":{\"load\":0,\"peak\":0,\"limit\":450},\"battery\":\"single Li-ion / LiPo cell (3.7 V)\",\"mah\":1200,\"runtime_h\":12.8,\"target_h\":8,\"currents_are_estimates\":true}",
 "part_list": "[{\"id\":\"P1\",\"line\":\"SSD1306 OLED 128x64\",\"name\":\"OLED 128x64 (I2C)\",\"qty\":1,\"bus\":\"i2c\",\"signals\":[],\"ma\":20,\"peak_ma\":20,\"logic\":\"any\",\"rail\":\"3v3\",\"addr\":\"0x3C\"},{\"id\":\"P2\",\"line\":\"LoRa SX1276 module\",\"name\":\"LoRa radio (SPI)\",\"qty\":1,\"bus\":\"spi\",\"signals\":[\"CS\",\"RST\",\"IRQ\"],\"ma\":15,\"peak_ma\":120,\"logic\":\"3v3\",\"rail\":\"3v3\",\"addr\":null},\"... 3 more\"]",
 "notes": "Pocket messenger for hikes with a friend. Text on the OLED, four buttons to pick canned messages.",
 "pin_map": "[{\"pin\":\"GP4\",\"part\":\"I2C bus\",\"signal\":\"SDA\"},{\"pin\":\"GP5\",\"part\":\"I2C bus\",\"signal\":\"SCL\"},\"... 11 more\"]",
 "map_source": "page draft",
 "map_reads": "[]",
 "map_verdict": "flash",
 "goal": "Show a hello line on the OLED, beep once at start, print each button press over USB serial and send one LoRa test packet ... (146 characters)"
}

Reply:

{
 "task": "boot",
 "title": "Trail Pager First Boot Test",
 "verdict": "flash",
 "summary": "This sketch brings up the OLED, the LoRa radio, all four buttons and the buzzer on the pins Trail pager already has mapped, so you can see the whole bench is alive before writing any message logic. It shows a hello line on the screen, beeps once at power-up, and prints every button press over USB serial. Press button 1 to also send one LoRa test packet. Flash it, open the serial monitor at 115200, and check the OK lines first.",
 "libraries": [
  {
   "name": "Adafruit_SSD1306",
   "why": "drives the SSD1306 OLED over the I2C bus"
  },
  {
   "name": "Adafruit_GFX",
   "why": "text and drawing core that Adafruit_SSD1306 needs"
  },
  {
   "name": "LoRa (sandeepmistry)",
   "why": "drives the SX1276 LoRa radio over the SPI bus"
  }
 ],
 "sketch": "// Pocket Rig - Trail pager - first boot sketch (RP2040 Pico)\n// Proves: a hello line on the OLED, one beep at start, every button press\n// printed over USB serial, and one LoRa test packet when button 1 is pressed.\n\n#include <Wire.h>\n#include <SPI.h>\n#include <Adafruit_GFX.h>\n#include <Adafruit_SSD1306.h>\n#include <LoRa.h>\n\n// I2C bus - OLED\nconst uint8_t I2C_SDA = 4;\nconst uint8_t I2C_SCL = 5;\n\n// ... 115 more lines",
 "pins": [
  {
   "pin": "GP4",
   "part": "I2C bus",
   "signal": "SDA"
  },
  {
   "pin": "GP5",
   "part": "I2C bus",
   "signal": "SCL"
  },
  {
   "pin": "GP18",
   "part": "SPI bus",
   "signal": "SCK"
  },
  {
   "pin": "GP19",
   "part": "SPI bus",
   "signal": "MOSI"
  },
  "..."
 ],
 "checks": [
  {
   "step": "Flash the sketch and open the serial monitor at 115200.",
   "expect": "OLED: OK, LoRa radio: OK, Button 1-4: OK (no readback), and Buzzer: OK (no readback), one line each."
  },
  {
   "step": "Watch the OLED right after power-up.",
   "expect": "The screen shows \"Trail pager\" and \"hello - boot ok\"."
  },
  {
   "step": "Listen right after power-up.",
   "expect": "One short beep from the buzzer, then silence."
  },
  "..."
 ],
 "blockers": []
}

task: boot - ESP32 field deck - flash

Input:

{
 "task": "boot",
 "today": "2026-10-20",
 "rig": "Field deck",
 "board": "esp32",
 "wifi": "on",
 "parts": "2.8in ILI9341 TFT\nI2C keyboard\nGPS NEO-6M\nmicroSD card\nBME280\nrotary encoder\nbuzzer\nWS2812 x4",
 "facts": "{\"board\":\"esp32\",\"board_name\":\"ESP32 DevKit (38-pin, 3.3 V logic, Wi-Fi)\",\"logic_v\":3.3,\"wifi_on\":true,\"usable_pins\":24,\"pins_needed\":17,\"shared_bus_pins\":7,\"i2c\":[{\"part\":\"I2C keyboard\",\"addr\":\"0x5F\"},\"...\"],\"spi_devices\":2,\"serial_devices\":1,\"active_ma\":428,\"peak_ma\":628,\"rail_3v3\":{\"load\":348,\"peak\":548,\"limit\":600},\"rail_5v\":{\"load\":80,\"peak\":80,\"limit\":450},\"battery\":\"single Li-ion / LiPo cell (3.7 V)\",\"mah\":2000,\"runtime_h\":3.7,\"target_h\":6,\"currents_are_estimates\":true}",
 "part_list": "[{\"id\":\"P1\",\"line\":\"2.8in ILI9341 TFT\",\"name\":\"TFT display (SPI)\",\"qty\":1,\"bus\":\"spi\",\"signals\":[\"CS\",\"DC\",\"RST\",\"LED\"],\"ma\":90,\"peak_ma\":90,\"logic\":\"3v3\",\"rail\":\"3v3\",\"addr\":null},{\"id\":\"P2\",\"line\":\"I2C keyboard\",\"name\":\"I2C keyboard\",\"qty\":1,\"bus\":\"i2c\",\"signals\":[],\"ma\":6,\"peak_ma\":6,\"logic\":\"any\",\"rail\":\"3v3\",\"addr\":\"0x5F\"},\"... 6 more\"]",
 "notes": "Clamshell deck for logging walks: map-less GPS readout, weather, a notes keyboard. Wi-Fi only to sync logs at home.",
 "pin_map": "[{\"pin\":\"GPIO18\",\"part\":\"SPI bus\",\"signal\":\"SCK\"},{\"pin\":\"GPIO23\",\"part\":\"SPI bus\",\"signal\":\"MOSI\"},\"... 15 more\"]",
 "map_source": "pasted map",
 "map_reads": "[{\"level\":\"info\",\"code\":\"strap-pin\",\"at\":[\"GPIO5\"],\"text\":\"IO5 is a boot strapping pin: if TFT display pulls it at power-on the board may not start or flash.\"},{\"level\":\"warn\",\"code\":\"strap-pin\",\"at\":[\"GPIO15\"],\"text\":\"IO15 is a boot strapping pin: if microSD slot pulls it at power-on the board may not start or flash.\"}]",
 "map_verdict": "flash",
 "goal": "Boot to a status screen on the TFT: keyboard keys echo on the bottom line, GPS satellite count and BME280 temperature on ... (164 characters)"
}

Reply:

{
 "task": "boot",
 "title": "ESP32 field deck status boot",
 "verdict": "flash",
 "summary": "This sketch brings up all eight parts on the pasted map and shows the status screen the goal asks for: GPS satellite count and BME280 temperature on the top line, keyboard keys echoed on the bottom line, and the encoder moving a small cursor bar under them. GPIO5 and GPIO15 are boot-strapping pins the page already flagged (info and warn); wiring still works, but unplug the microSD card if the board will not enter flash mode. Wi-Fi join and log sync are left for a later sketch - flash this one first and check the serial monitor.",
 "libraries": [
  {
   "name": "Adafruit_GFX",
   "why": "graphics core the ILI9341 driver needs"
  },
  {
   "name": "Adafruit_ILI9341",
   "why": "drives the 2.8in TFT status screen"
  },
  {
   "name": "Adafruit_Sensor",
   "why": "common sensor interface the BME280 library needs"
  },
  "..."
 ],
 "sketch": "// Pocket Rig boot sketch - Field deck (ESP32)\n// Proves the TFT, I2C keyboard, GPS, microSD, BME280, encoder, buzzer and\n// RGB LEDs on the pasted pin map are alive, then puts up the status screen.\n// Wi-Fi join and log sync come later - this sketch only proves the sensors\n// and the screen.\n\n#include <SPI.h>\n#include <Wire.h>\n#include <Adafruit_GFX.h>\n#include <Adafruit_ILI9341.h>\n#include <Adafruit_Sensor.h>\n#include <Adafruit_BME280.h>\n#include <TinyGPSPlus.h>\n#include <SD.h>\n// ... 155 more lines",
 "pins": [
  {
   "pin": "GPIO18",
   "part": "SPI bus",
   "signal": "SCK"
  },
  {
   "pin": "GPIO23",
   "part": "SPI bus",
   "signal": "MOSI"
  },
  {
   "pin": "GPIO19",
   "part": "SPI bus",
   "signal": "MISO"
  },
  {
   "pin": "GPIO5",
   "part": "TFT display",
   "signal": "CS"
  },
  "..."
 ],
 "checks": [
  {
   "step": "Open the serial monitor at 115200 baud right after flashing.",
   "expect": "Eight OK/FAIL lines, one per part, TFT display through RGB LEDs."
  },
  {
   "step": "If the board will not enter flash mode with the microSD card wired, unplug the SD module and retry, then reconnect it.",
   "expect": "Upload completes normally - GPIO15 is a boot-strapping pin."
  },
  {
   "step": "Look at the TFT right after boot.",
   "expect": "Top line reads something like 'Sats: 0  Temp: 21.3 C'; the bottom line is blank."
  },
  "..."
 ],
 "blockers": []
}

task: boot - Uno-class slab - hold

Input:

{
 "task": "boot",
 "today": "2026-10-20",
 "rig": "Slab deck",
 "board": "uno",
 "wifi": "off",
 "parts": "3.5in ILI9488 TFT\nkeyboard matrix 6x8\nGPS NEO-6M\nLoRa RFM95\nmicroSD card\nMAX98357 I2S amp + speaker\nWS2812 x8",
 "facts": "{\"board\":\"uno\",\"board_name\":\"Uno-class board (ATmega328P, 5 V logic)\",\"logic_v\":5,\"wifi_on\":false,\"usable_pins\":18,\"pins_needed\":31,\"shared_bus_pins\":8,\"i2c\":[],\"spi_devices\":3,\"serial_devices\":1,\"active_ma\":507,\"peak_ma\":962,\"rail_3v3\":{\"load\":182,\"peak\":357,\"limit\":50},\"rail_5v\":{\"load\":325,\"peak\":605,\"limit\":450},\"battery\":\"single Li-ion / LiPo cell (3.7 V)\",\"mah\":2500,\"runtime_h\":3.9,\"target_h\":10,\"currents_are_estimates\":true}",
 "part_list": "[{\"id\":\"P1\",\"line\":\"3.5in ILI9488 TFT\",\"name\":\"TFT display (SPI)\",\"qty\":1,\"bus\":\"spi\",\"signals\":[\"CS\",\"DC\",\"RST\",\"LED\"],\"ma\":90,\"peak_ma\":90,\"logic\":\"3v3\",\"rail\":\"3v3\",\"addr\":null},{\"id\":\"P2\",\"line\":\"keyboard matrix 6x8\",\"name\":\"Key matrix\",\"qty\":1,\"bus\":\"matrix\",\"signals\":[\"ROW1\",\"ROW2\",\"ROW3\",\"ROW4\",\"ROW5\",\"ROW6\",\"COL1\",\"COL2\",\"COL3\",\"COL4\",\"COL5\",\"COL6\",\"COL7\",\"COL8\"],\"ma\":2,\"peak_ma\":2,\"logic\":\"any\",\"rail\":\"3v3\",\"addr\":null},\"... 5 more\"]",
 "notes": "First build. I want a full keyboard, a big screen, sound and a radio, in a laptop-style case.",
 "pin_map": "[{\"pin\":\"D13\",\"part\":\"SPI bus\",\"signal\":\"SCK\"},{\"pin\":\"D11\",\"part\":\"SPI bus\",\"signal\":\"MOSI\"},\"... 7 more\"]",
 "map_source": "pasted map",
 "map_reads": "[{\"level\":\"warn\",\"code\":\"usb-serial\",\"at\":[\"D0\"],\"text\":\"D0 is the USB serial RX line - sharing it breaks uploads and the serial monitor.\"},{\"level\":\"fix\",\"code\":\"double-pin\",\"at\":[\"D13\"],\"text\":\"D13 is given to SPI bus SCK and Status LED LED.\"},\"... 11 more\"]",
 "map_verdict": "hold",
 "goal": "Get the screen to say hello and blink the status LED."
}

Reply:

{
 "task": "boot",
 "title": "Pin conflicts hold this pin map",
 "verdict": "hold",
 "summary": "This map can't go to the bench yet. D13 is asked to be both the SPI clock (shared by the TFT, LoRa radio and microSD card) and the status LED, and D10 is asked to be the chip-select for both the TFT display and the LoRa radio. The TFT's DC line is also on D0, the board's USB-serial RX pin, so uploading the sketch and the Serial monitor the boot checks need would both break. Clear these three pin clashes, then send the fixed map back for a boot sketch.",
 "libraries": [],
 "sketch": "",
 "pins": [],
 "checks": [],
 "blockers": [
  "D13 is given to both SPI bus SCK (the shared clock for the TFT, LoRa radio and microSD card) and the status LED. Move the status LED off D13 to a free pin, e.g. D4, and leave D13 for SCK alone.",
  "D10 is given to both the TFT display CS and the LoRa radio CS. Give the LoRa radio its own CS pin, e.g. D7.",
  "D0 is the board's USB-serial RX line, and the map puts the TFT display DC there. That blocks uploading the sketch and the Serial monitor the boot checks rely on - move DC to a free pin, e.g. D2."
 ]
}

Build the input yourself

Load the script in node (global.window = {}; require("./rigkit.js")), then:

const K = RigKit;
const st = K.state({
  board: "pico", wifi: "", battery: "lipo", mah: "1200", target: "8",
  parts: "SSD1306 OLED\nLoRa SX1276 module\n4 buttons\npiezo buzzer",
  notes: "", pinmap: "", goal: "Show hello on the OLED and beep once."
});
console.log(st.demand.total, st.usable, st.power.runtime_h, st.reads, st.verdict);
const wire = K.toWire(K.buildInput(st, "build", { today: "2026-10-20" }).input);  // POST this to /estimate and /run
// boot lane: K.toWire(K.buildInput(st, "boot").input) - uses st.map (your pasted pinmap, else the page's draft)
// later: const reply = K.normalize(K.parseReply(job.output.output), "build");
// const { notes, rows } = K.check(reply, st, "build");

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://pocket-rig.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"pocket-rig"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "build", "today": "2026-10-20", "board": "pico", "wifi": "off", "parts": "SSD1306 OLED 128x64\nLoRa SX1276 module ...", "facts": "{...}", "part_list": "[...]", "page_map": "[...]", "page_reads": "[...]", "page_verdict": "build"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses pocket-rig:<task>:<inputHash>:a<attempt> (the examples below use the build lane), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="pocket-rig:build:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: pocket-rig:build:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json