# HTTP API (https://blode.co/iconsmith/docs/api) POST /api/v1/icons is the whole generate contract. GET /api/v1/models describes the served model. The zone is `https://blode.co/iconsmith`. Every path below is under that prefix. ## Draw `POST /api/v1/icons` Body: `{ model?, prompt?, concept?, images?, cut?, n?, stream? }`, at least one of `prompt`, `concept` or `images`. - `prompt` is free text (500 characters) - `concept` is an explicit slug; otherwise it derives from the prompt - `images` is up to four `{ mime, base64 }` PNG, JPEG or WebP entries (base64 only) - `cut` defaults to the house cut (24px, stroke 2, radius 3, outlined) - `n` is 1 or 2 - unknown fields are refused with `param` naming them - any `model` other than `iconsmith-1` is `model_not_found` - `stream: true` answers `text/event-stream` Response: `{ id, object: "icon.draw", created, model, revision, concept, prompt, images, cut, data, usage, credits?, took_ms }`. Image bytes are never echoed. `usage` is `null` when the provider has not reported token counts; it does not mean the draw was free. Every response carries `X-Request-ID`. Errors are `{ status, code, message, request_id, param?, retry_after? }`. ## Models - `GET /api/v1/models` - `GET /api/v1/models/iconsmith-1` ## Keys - `ism_live_…` spends a credit when credits are on; otherwise `invalid_api_key` - `ism_test_…` is the sandbox on any deployment: no provider call, `credits: 0` - browser draws require sign-in and monthly USD usage billing # Benchmark (https://blode.co/iconsmith/docs/benchmark) # The icon benchmark `packages/iconsmith/scripts/icon-bench.ts` scores `iconsmith-1` as a product, through `POST /api/v1/icons`, on a frozen held-out task set. It exists so the model card can carry one number for the whole system that a stranger can re-run, and so a change to the author, the brief, the compiler or the route can be judged against a noise floor rather than an anecdote. For the configurable quality / speed / spend scorecard and improvement loop, see [Product evals](product-evals.md). ## Shape Every kind of generative model has converged on the same benchmark shape, and the icon version keeps to it. | Layer | What it is elsewhere | What it is here | | --- | --- | --- | | Task set | Held-out prompts with frozen splits | `bench/reconstruction.json`: 250 house concepts, `feedback` 60 / `selection` 130 / `sealed` 60, chosen by seed 1 and never re-dealt; named slices `smoke` 6 and `baseline` 30 sit inside `feedback`. Two tasks over the same entries: `text` sends the slug as the concept; `image` sends the house drawing rendered at 96 px and no words, so the author must read and name it | | Verifiable layer | Hidden tests a coding benchmark executes | Compile, exact replay and lint (`gate` = no error, `strict` = no finding), plus the six structural checks in `eval/blindspot.ts` | | Reference layer | A target image or ground-truth answer | The library's own outlined drawing for the slug; rendered cosine, plain and registered, on a scale with floor 0.482 (a random house icon), baseline 0.737 (two mature sets, same concept) and ceiling 1 | | Judgment layer | Human preference or a gated model judge | The API benchmark does not grade craft. The product-eval review bridge accepts independent human reviews or a recomputed sealed craft qualification; easy historical sanity-gate passes alone are insufficient | | Cost and latency | Tokens and seconds per item | The response's `usage` and `took_ms`, summed and medianed | | Noise floor | Seed-to-seed spread | `--replicates n`; the spread between replicate medians is reported and a delta below it means nothing | | Anti-gaming | Contamination control, sealed test sets | The target and its byte twins are excluded from the author's references by the route; the twelve pinned anchors are excluded from the task set; a best candidate above 0.95 plain cosine is flagged as leakage, not scored; `sealed` is opened once, by a person | ## Running it ```bash # the harness, against the sandbox: no provider call, not a score npx tsx scripts/icon-bench.ts --split smoke --key ism_test_<32 base-62> # a live run against a deployment with a provider key; record paid intent # in docs/foundry-log.md first npx tsx scripts/icon-bench.ts --split feedback --replicates 2 \ --base https://blode.co/iconsmith --concurrency 2 --live --max-draws 120 # the image-to-icon task: the reference rendered at 96 px, no words npx tsx scripts/icon-bench.ts --task image --split feedback \ --base https://blode.co/iconsmith --live --max-draws 60 ``` Receipts land in `packages/iconsmith/bench/runs/` as `iconsmith-1----.json` beside a `.board.png`. `environment` is `test` for sandbox runs and `live` otherwise; only live receipts reach the model card's Benchmark row (`scripts/model-card.ts`). Every receipt carries `qualified: false`. `model`, `revision` and `pipeline` are read from the deployment's model object, not from a probe draw: `pipeline` says whether prompt resolution and the concept sketch were on, and two receipts are compared only when it matches (footgun F44). Neither task exercises prompt resolution, since both name the concept; measuring it needs briefs in words, which no frozen set yet holds. ## Measuring prompt resolution The two tasks name their concepts, so they never exercise the stage that maps a brief in words to a house concept. That stage has its own set, `packages/iconsmith/bench/briefs.v1.json`: 104 briefs beside every library name that answers each, or null where the library draws no such thing. `apps/web/scripts/brief-eval.ts` runs the real shortlist and the real decision through it and scores each outcome with asymmetric penalties, as a triage rubric does: a right accepted pick 0, a fallback where a concept existed 1 (the slug was the status quo), a wrong accepted concept 3 (the author is sent to the wrong siblings). The receipt in `docs/log/` carries the rows, the tally, a threshold sweep from the recorded probabilities, and the tokens and dollars; `qualified: false` on every one. The labels were written by the assistant from the library's names and tags and want a designer's pass before the figure is quoted anywhere but the log. ## Reading a receipt `summary.delivered.rate` is the share of draws that returned at least one checked candidate. `summary.lint` is the gate and strict rate over checked candidates, the same two readings the house yardstick uses (blode-icons itself is strict-clean 40.9% of the time on the current linter). `summary.fit` is the median plain cosine of the first valid checked candidate over keyed rows; the same number registered is beside it, and a candidate whose plain gain does not survive registration won by sliding. `summary.structural` is the panel with its per-check floors. Read a change as real only when it clears `fit.spreadPlain`. ## What a live run costs A `feedback` run is 60 draws of two authors each at provider prices; two replicates double it. Nothing here estimates the dollar figure: the model card's cost row is filled from metered draws, and until it is, the receipt carries tokens only; missing usage remains null. The weighted scorecard requires actual request-level USD evidence and does not infer spend from token count alone. ## Designing benchmarks for other model kinds The same five layers, with what fills them per modality. Text. Held-out prompts with verifiable answers where possible (exact match, unit-testable extraction, constrained formats); rubric grading by a judge model that has passed a sanity gate with position swapping, because judges learn slot order; human pairwise preference for the rest; contamination checks against training data; cost and latency per task. Coding. The cleanest case: hidden tests are the verifiable layer, so pass@1 on held-out repositories is the number, with contamination control and a sealed set. Judgment is only needed for code quality, and it should be a gated judge or a human, never the same model that wrote the code. Image. Format validity and prompt adherence are the verifiable layer, using OCR for rendered text and a VQA model for object presence. Embedding similarity is weak: this repository discarded its own style metric at AUC 0.476 and found rendered cosine inverts on translation, so any embedding score needs a separation gate before it counts. Human pairwise preference decides aesthetics; per-image cost and time close the loop. Video. Everything from image plus temporal consistency, motion plausibility and adherence over time, scored per dimension rather than as one number; human pairwise preference on short clips; cost per second of output, which dominates the economics. Music. Format and structural checks code can measure (duration, tempo and key stability, clipping); prompt adherence through an audio-text embedding with its own separation gate; distributional distance to a reference corpus with the same caveat as image embeddings; listening tests for quality. Output seconds per dollar is the cost row. # Blode (https://blode.co/iconsmith/docs/blode) # Iconsmith documentation Iconsmith is an icon model served at [blode.co/iconsmith](https://blode.co/iconsmith). One model id, `iconsmith-1`. You name an icon in words, a slug or an image, and pick the cut: size, stroke, radius and finish. Two independent authors each write a program in a constrained DSL, a compiler and a linter check every line, and the SVG you get back is compiler output. The model never emits a coordinate. A candidate comes back `checked` or `draft`, never approved: you are the reviewer. ## Pages - [Introduction](blode/introduction.md): what the model takes, what it returns, and the two ways to call it. - [How it works](blode/concepts/how-it-works.md): brief, two authors, the DSL, the checker, the SVG. - [The family](blode/concepts/the-family.md): the pinned revision, the twelve anchors, the reference library and sibling lookup. - [The cut](blode/concepts/the-cut.md): size, stroke, radius and finish, and why each is an enumeration. - [Request](blode/primitives/request.md): `concept`, `prompt` and `images`, their shapes and limits. - [Candidate](blode/primitives/candidate.md): one item of `data`, its status, attempts, intent and findings. - [API reference](blode/api.md): the HTTP endpoints, request and response bodies, streaming frames and headers. - [Models](blode/models.md): the model object, the pinned revision, and every row of the model card. - [Limits and errors](blode/limits-and-errors.md): every limit constant, every error code, and what to retry. - [Your first icon](blode/cookbooks/first-icon.md): one curl with a test key, and how to read the response. - [Streaming](blode/cookbooks/streaming.md): a server-sent events client in TypeScript. - [Image to icon](blode/cookbooks/image-to-icon.md): attach a PNG, and what the author is told about it. - [Benchmark](blode/cookbooks/benchmark.md): running `icon-bench.ts`, and reading a receipt. - [Agent skill](blode/cookbooks/agent-skill.md): the npm package and the skill route, with no API key. - [Credits](blode/credits.md): prepaid credits, keys, and why no price is set. # Api (https://blode.co/iconsmith/docs/blode/api) # API reference > Index: [Iconsmith documentation](../blode.md) Base URL: `https://blode.co/iconsmith` Three endpoints. One of them draws. | Method | Path | | --- | --- | | `POST` | `/api/v1/icons` | | `GET` | `/api/v1/models` | | `GET` | `/api/v1/models/{model}` | Authentication is `Authorization: Bearer `, and is optional. No header at all requires a signed-in browser session and prepaid credits. See [Credits](credits.md). ## POST /api/v1/icons One draw. Input is optional text plus optional images; output is an icon. ```http POST /api/v1/icons HTTP/1.1 Host: blode.co Content-Type: application/json Authorization: Bearer ism_test_00000000000000000000000000000000 ``` ### Request body | Field | Type | Default | Notes | | --- | --- | --- | --- | | `concept` | string | derived | An explicit slug, 2 to 48 characters. | | `prompt` | string | absent | Free text, 1 to 500 characters. | | `images` | array | `[]` | At most 4 entries of `{ base64, mime }`. | | `cut` | object | the house cut | `{ finish, radius, size, stroke }`. | | `model` | string | absent | Must be `iconsmith-1` when present. | | `n` | integer | `2` | 1 or 2. | | `stream` | boolean | `false` | Server-sent events when true. | At least one of `concept`, `prompt` or `images`. The body is strict: an unrecognised field is `invalid_request` with `param` naming it. See [Request](primitives/request.md) and [The cut](concepts/the-cut.md). ```json { "concept": "bookmark-check", "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 }, "model": "iconsmith-1", "n": 2, "stream": false } ``` ### Response body ```json { "concept": "bookmark-check", "created": 1789603200, "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 }, "data": [ { "attempts": 1, "findings": [], "index": 0, "intent": "object: bookmark; modifier: check", "mime_type": "image/svg+xml", "program": "icon bookmark-check\n...", "status": "checked", "svg": "..." } ], "id": "draw_0f0ec5ea4d384e7f9c8f6f4c7e5a1d22", "images": 0, "model": "iconsmith-1", "object": "icon.draw", "prompt": null, "revision": "blode-icons-24-v1", "took_ms": 84213, "usage": { "input_tokens": 0, "output_tokens": 0, "reasoning_tokens": 0, "total_tokens": 0 } } ``` Field values in that block are illustrative. `took_ms` and every `usage` figure vary per draw, and `usage` is all zeros on a sandbox draw. | Field | Type | What it is | | --- | --- | --- | | `id` | string | `draw_` plus the request id with its hyphens removed. | | `object` | string | Always `icon.draw`. | | `created` | integer | Unix seconds. | | `model` | string | The product id. Always `iconsmith-1`. | | `revision` | string | The pinned style revision. | | `concept` | string | The slug the drawing is filed under: given, derived, or chosen by the author. | | `prompt` | string or null | Your words, echoed. | | `images` | integer | How many images the request attached. Never the images. | | `cut` | object | The cut that was drawn. | | `data` | array | One [candidate](primitives/candidate.md) per author. | | `usage` | object | `input_tokens`, `output_tokens`, `reasoning_tokens`, `total_tokens`. | | `credits` | integer | Present only when a key paid for the draw. | | `took_ms` | integer | Wall clock for the whole generation. | `total_tokens` is `input_tokens` plus `output_tokens`. ## Streaming Send `"stream": true`. The response is `text/event-stream; charset=utf-8`. Each frame is `event:` then `data:`, separated by a blank line. ``` event: status data: {"attempt":1,"index":0,"stage":"drafting"} event: candidate data: {"attempts":1,"findings":[],"index":0,"intent":null,"mime_type":"image/svg+xml","program":"...","status":"checked","svg":"..."} event: result data: {"concept":"bookmark-check","created":1789603200,...} data: [DONE] ``` | Event | Data | When | | --- | --- | --- | | `status` | `{ attempt, index, stage }` | Each stage change, per author. | | `candidate` | One candidate | Each author finishes. | | `result` | The whole draw response | Every author is done. | | `error` | The error envelope | The draw failed before a result. | `stage` is `drafting`, `checking`, `repairing` or `done`. The stream always ends with `data: [DONE]`, including after an `error` event. The stream's own HTTP status is 200, so read `error` frames, not the status code. A draw that fails after a live key spent a credit is refunded. See [Streaming](cookbooks/streaming.md) for a client. ## GET /api/v1/models The one model, in a list. ```bash curl https://blode.co/iconsmith/api/v1/models ``` ```json { "data": [{ "id": "iconsmith-1", "object": "model" }], "object": "list" } ``` Each entry is a full model object. ## GET /api/v1/models/{model} ```bash curl https://blode.co/iconsmith/api/v1/models/iconsmith-1 ``` Any other `{model}` is `model_not_found` with `param: "model"`. The full object and every field is documented on [Models](models.md). Both model endpoints answer with `Cache-Control: public, max-age=300`. ## Headers | Header | Direction | Notes | | --- | --- | --- | | `X-Request-ID` | response | On every response, success or error. Quote it in a report. | | `Cache-Control` | response | `no-store` on a draw, `public, max-age=300` on the model endpoints. | | `Retry-After` | response | Seconds, on any error carrying `retry_after`. | | `Content-Type` | response | `text/event-stream; charset=utf-8` on a stream. | | `X-Accel-Buffering` | response | `no` on a stream. | | `x-iconsmith-environment` | response | `test` when a test key served the draw. | | `Authorization` | request | `Bearer ism_live_...` or `Bearer ism_test_...`. | ## Error envelope Every error is one JSON object with the same fields. ```json { "code": "rate_limit_exceeded", "message": "You have drawn a lot recently. Try again in a few minutes.", "retry_after": 412, "request_id": "0f0ec5ea-4d38-4e7f-9c8f-6f4c7e5a1d22", "status": 429 } ``` | Field | Type | Notes | | --- | --- | --- | | `status` | integer | The HTTP status, repeated in the body. | | `code` | string | One of nine codes. | | `message` | string | What went wrong, in words. | | `request_id` | string | The same value as `X-Request-ID`. | | `param` | string | Present when a field is at fault. A dotted path from the body root, such as `cut.size`. | | `retry_after` | integer | Present when waiting will help. Seconds. | The codes and what to do about each one are on [Limits and errors](limits-and-errors.md). ## The sandbox A `ism_test_` key returns a deterministic draw. No provider call, no store access, nothing billed, `credits: 0`, and the response carries `x-iconsmith-environment: test`. Every SVG root from the sandbox is stamped: ``` data-iconsmith-sandbox="true" ``` The drawing is always the house `square-check`, `status: "checked"`, `attempts: 1`, no findings. A streaming sandbox draw emits the same `status` and `candidate` frames as a real one, so a client can be built against it. Test keys need no issuance. Any string matching `^ism_test_[0-9A-Za-z]{32}$` is accepted, on any deployment. A test key is metered like an anonymous caller, so it uses the sandbox rate window. # How It Works (https://blode.co/iconsmith/docs/blode/concepts/how-it-works) > Index: [Iconsmith documentation](../../blode.md) One draw is four steps: a brief, two authors, a checker, a compiler. ## 1. The brief The route turns your request into one brief. It carries the ask, your words when you sent any, the count of images you attached, the twelve style anchors as one sheet, and the related family drawings as a second sheet. When you named a concept the brief says which icon to draw; when you did not, it tells the author to name the icon itself with a short hyphenated slug. The brief also states the standing rule about meaning: > State the object and the modifier the brief asks for, and draw both. A > `search-check` needs a magnifying glass and a check, not a check alone. ## 2. Two independent authors `n` is 1 or 2, and defaults to 2. Each author works from the same brief and the same references, and neither sees the other. Two authors is the product; one is half the spend. An author does not answer with an SVG. It answers with a program in the constrained DSL, beginning with the `icon` name and the `finish` line: ``` # object: square; modifier: check icon square-check keyline square finish outlined rect 4,4 16x16 r3 line 8,12 11,15 16,10 fit ``` That is the whole vocabulary an author has. There is no coordinate in it that the canvas did not quantise, no corner radius outside the tier system, and no path data. ## 3. The checker Each author has one tool. It compiles the program, replays it and lints it, in the same process as the route. The tool budget is three checks per author: one draft and two repairs. The author reads the findings and repairs from them. The three stages you see on a stream are `drafting`, `checking` and `repairing`, then `done`. ``` program -> compile -> exact replay -> 17 lint rules -> findings ``` Replay is exact: the emitted program must recompile to a byte-identical document. Anything the DSL cannot say fails rather than being promised in prose. ## 4. The SVG Only a program the tool compiled reaches the response. The `svg` field is compiler output, never model output. The `program` field is the same program, editable, and it recompiles to the same SVG. A candidate whose last attempt still carries a structural error after the repair allowance comes back with `status: "draft"`, with its findings, so you can see what went wrong. One that compiled, replayed exactly and carries no structural error comes back `checked`. ## What this buys and what it does not The checks constrain geometry. They do not establish that an icon reads as the right object. `checked` is a structural statement. There is no approval field, because approval needs eyes. The provider route behind a draw is a deployment detail. It is not rendered, not returned, and not recorded on the model card. The response's `model` is always the product id. ## Next - [The family](the-family.md) - [The cut](the-cut.md) - [Candidate](../primitives/candidate.md) # The Cut (https://blode.co/iconsmith/docs/blode/concepts/the-cut) > Index: [Iconsmith documentation](../../blode.md) The cut is the four knobs you can turn. It travels in the request as `cut`, and comes back on the response as `cut`. ```json { "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 } } ``` Those four values are the house cut, and they are the defaults. Omit `cut` entirely and you get them. Omit one field and you get that field's default. ## The enumerations | Field | Values | Default | | --- | --- | --- | | `size` | `16`, `20`, `24` | `24` | | `stroke` | `1`, `1.5`, `2`, `2.5`, `3` | `2` | | `radius` | `0`, `1`, `2`, `3` | `3` | | `finish` | `"outlined"`, `"filled"` | `"outlined"` | `size` and `stroke` are numbers in the icon's own units, not CSS pixels. `radius` is the container corner radius, not a tier index. `finish` selects the paint. Any other value is `invalid_request`, with `param` naming the field and a message of the form `Expected one of 16, 20, 24`. Any field name outside the four is refused too, since `cut` is strict. ## Why they are enumerations and not numbers Each combination is a *cut* of the measured house spec. The compiler derives radius tiers, dot roles, minimum feature size and density from `size`, `stroke` and `radius`. A value outside the tested range would not fail. It would silently produce a style nobody has looked at. So the set of legal cuts is the set that has been measured. Three sizes, five strokes, four radii and two finishes. ## What a cut changes downstream At 24px the master sets grid 0.25, stroke 2, radius 3, clearance 2, minimum feature 1.5 and minimum gap 1, with radius tiers 0.5, 1, 2 and 3 and at most eight elements. A different cut moves those derived numbers. The linter reads the cut it was given, so the same program checked at two cuts can produce different findings, and the `cut` rule fires when a drawing does not match the cut it claims. ## Finish `outlined` and `filled` are one skeleton with two paints, not two drawings. The filled twin is expected to occupy the same visual extent as the outlined one. Measured across 2,085 pairs of the house set, it does in 94% of them, to within 0.01 units. A filled drawing cannot be checked by the off-axis rule, which is why fill-expanded sets are reported separately from the conformance floor on the [model card](../models.md). ## Next - [Request](../primitives/request.md) - [API reference](../api.md) # The Family (https://blode.co/iconsmith/docs/blode/concepts/the-family) > Index: [Iconsmith documentation](../../blode.md) Iconsmith draws into one existing family, not into a new style. The family is pinned, so a draw made today and a draw made next month are drawn against the same references. ## The revision `blode-icons-24-v1` is the pinned style revision. `GET /api/v1/models/iconsmith-1` returns it: ```json { "revision": { "anchors": 12, "compiler": "iconsmith-constrained-25-source-exact", "id": "blode-icons-24-v1", "sha256": "9876c68488816f34afb8b38bfffe3b5c99beff64d6165d60c603242c06e5bce0" } } ``` The `sha256` is the revision's own digest. Quote it beside any result you keep, and beside any benchmark receipt: two results are comparable only when the revision matches. The revision's `calibration` is `unvalidated`. The master at 24px sets the canvas: grid 0.25, stroke 2, radius 3, clearance 2, minimum feature 1.5, minimum gap 1, radius tiers 0.5, 1, 2 and 3, at most 8 elements. ## The twelve anchors Every author sees the same twelve drawings, as one sheet, in this order. | | | | | | --- | --- | --- | --- | | `folder-1` | `clock` | `calendar-1` | `magnifying-glass` | | `circle-check` | `bell` | `user` | `chevron-down` | | `trash-can` | `plus-medium` | `arrow-down-square` | `cloud` | They show the family's stroke, corners and proportions. They are the style statement, not a semantic one. The twelve are also excluded from the benchmark task set, because every author sees them. ## The reference set The whole `blode-icons` library is vendored in the repository at `packages/iconsmith/library/blode-icons`, under MIT. `SOURCE.json` records the upstream commit `5743371d942f7737e05ccaabc2aa54e5b9980175`, committed 2026-09-08. | Field | Value | | --- | --- | | `family.set` | `blode-icons` | | `family.drawings` | 4357 | | `family.license` | `MIT` | Of those, 2,221 are outlined. That subset is the house yardstick on the [model card](../models.md): the set passes its own strict reading 40.9% of the time, at a gate rate of 99.0%. A generator is not held to 1.0, because the designer's own set is not. ## Sibling lookup The anchors describe the style. Siblings describe the parts. For each concept the route ranks the library for related drawings: files that share a tag, share a cohort, or share name tokens with the concept. The ranking is deterministic. The shortlist becomes a second sheet in the brief, and each sibling's stroked elements are read back as the DSL ops that redraw them, so an author can reuse the tick that `circle-check` already carries rather than inventing a second one. The lookup excludes the target itself, its byte twins, and Lucide-derived drawings. When nothing in the library shares a tag, cohort or name, the brief says so and the author draws from the anchors alone. Recurring elements keep the same geometry across the set. The revision's rubric names them: the tick in `circle-check`, the arrowhead in `arrow-down-square`, the chevron, the plus, and the bell and bin bodies. ## Off-axis edges The family draws edges off 0, 45 and 90 degrees on purpose, and the spec permits them by name. 29.1% of stroked house icons carry one, 480 of 1,649. The linter warns rather than errors on them, because they are the set's own habit. ## Next - [The cut](the-cut.md) - [Models](../models.md) # Agent Skill (https://blode.co/iconsmith/docs/blode/cookbooks/agent-skill) > Index: [Iconsmith documentation](../../blode.md) The public skill calls the hosted model `iconsmith-1` over HTTP. Source in this checkout is `skills/iconsmith/SKILL.md`. Install from [mblode/iconsmith](https://github.com/mblode/iconsmith). `packages/iconsmith/SKILL.md` is the research local-authoring brief the harness scores. Do not install it as the product skill. ## Install Use one method. Claude Code: ```bash claude plugin marketplace add mblode/iconsmith claude plugin install iconsmith@iconsmith ``` Other agents: ```bash npx skills add mblode/iconsmith --skill iconsmith ``` Raw: [skills/iconsmith/SKILL.md](https://github.com/mblode/iconsmith/blob/main/skills/iconsmith/SKILL.md). Then ask the agent to draw a bookmark-check icon with iconsmith. Auth is a Bearer test key (`ism_test_` plus 32 characters), a live key, or the free allowance. See [the HTTP API](../api.md). The npm compiler (`prepare`, `check`, `draw`, `lint`) does not call the model. See [the CLI](../cli.md) in the product docs. ## Which route to use | | Public skill | HTTP API | | --- | --- | --- | | Model | `iconsmith-1` | `iconsmith-1` | | Key | Same as REST | Same | | Review | You | You | | Output | The draw response | The draw response | ## Next - [Introduction](../introduction.md) - [How it works](../concepts/how-it-works.md) # Benchmark (https://blode.co/iconsmith/docs/blode/cookbooks/benchmark) > Index: [Iconsmith documentation](../../blode.md) `packages/iconsmith/scripts/icon-bench.ts` scores `iconsmith-1` as a product, through `POST /api/v1/icons`, on a frozen held-out task set. It runs from a source checkout of the repository. The protocol is [docs/benchmark.md](../../benchmark.md). ## Running it ```bash # the harness against the sandbox: no provider call, not a score npx tsx scripts/icon-bench.ts --split smoke --key ism_test_<32 base-62> # a live run against a deployment npx tsx scripts/icon-bench.ts --split feedback --replicates 2 \ --base https://blode.co/iconsmith --concurrency 2 # the image-to-icon task: the reference rendered at 96 px, no words npx tsx scripts/icon-bench.ts --task image --split feedback \ --base https://blode.co/iconsmith ``` | Flag | Default | What it selects | | --- | --- | --- | | `--base` | ``localhost:3210/iconsmith`` | The deployment to draw through. | | `--split` | `smoke` | `smoke`, `baseline`, `feedback`, `selection` or `sealed`. | | `--task` | `text` | `text` sends the slug as the concept; `image` sends a 96 px raster and no words. | | `--n` | `2` | Candidates per draw. | | `--replicates` | `1` | Repeats of the whole slice, for the noise floor. | | `--concurrency` | `2` | Draws in flight. | | `--key` | `ICONSMITH_KEY` | The bearer key. A test key makes the run a sandbox run. | | `--out` | under `bench/runs` | Where the receipt lands. | The task set is `bench/reconstruction.json`: 250 house concepts, split `feedback` 60, `selection` 130, `sealed` 60, dealt once by seed 1. `smoke` is 6 and `baseline` is 30, both inside `feedback`. `sealed` is opened once, by a person. The twelve pinned anchors are excluded from every slice, because every author sees them. ## What a receipt holds Receipts land in `packages/iconsmith/bench/runs/` as `iconsmith-1----.json`, beside a `.board.png`. | Field | What it is | | --- | --- | | `builtAt` | When the run finished. | | `config` | Every flag the run used, including the cut. | | `environment` | `test`, `live` or `mixed`. | | `model`, `revision` | Read from the deployment's model object, not from a probe draw. | | `pipeline` | Whether prompt resolution and the concept sketch were on. | | `excluded` | The anchor slugs left out. | | `procedure` | The scoring procedure, in words, in the file. | | `qualified` | Always `false`. | | `rows` | One row per draw: candidates, usage, `tookMs`, `keyed`, `leakage`, errors. | | `summary` | `delivered`, `lint`, `fit`, `latency`, `structural`, `usage`. | Two receipts are comparable only when `revision` and `pipeline` match. `environment` is `test` for sandbox runs, and only a live receipt fills the model card's Benchmark row. ## Reading the summary `summary.delivered.rate` is the share of draws that returned at least one `checked` candidate. `summary.lint` is the gate and strict rate over checked candidates: `gate` is no error, `strict` is no finding at all. They are the same two readings the house yardstick uses, and blode-icons itself is strict-clean 40.9% of the time on the current linter. `summary.structural` is the structural panel with its per-check floors. `summary.latency` carries `medianTookMs` from the response's own `took_ms`, and `medianWallMs` measured by the harness. ## The fit scale `summary.fit` is rendered cosine of the best candidate against the library's own outlined drawing for that slug, median over keyed rows. | Point | Value | What it is | | --- | --- | --- | | Floor | 0.482 | A random house icon against the target. | | Baseline | 0.737 | Two mature icon sets drawing the same concept. | | Ceiling | 1 | The target against itself. | A score near 0.74 means as close as a different professional set's take. A reconstruction scoring 1.0 is a bug, and any best candidate above 0.95 plain cosine is flagged as a leakage diagnostic rather than scored. `medianPlain` sits beside `medianRegistered`. A candidate whose plain gain does not survive registration won by sliding, not by drawing. Read a change as real only when it clears `fit.spreadPlain`, the spread between replicate medians. A delta below the noise floor means nothing. ## What it does not measure Recognition and craft are not scored. No judge has passed the forced-choice gate, so the receipt carries `qualified: false` and writes a `board.png` of the reference beside the candidates for a person to look at. Neither task exercises prompt resolution, since both name the concept. That needs briefs in words, which no frozen set yet holds. The model card's Benchmark row reads `not yet run`. ## Prompt resolution Both tasks name their concept, so neither exercises the stage that maps a brief in words to a house concept when `pipeline.prompt_resolution` is on. That stage has its own set, `packages/iconsmith/bench/briefs.v1.json`: 104 briefs beside every library name that answers each, or null where the library draws no such thing. `apps/web/scripts/brief-eval.ts` runs it and scores each outcome with asymmetric penalties: a right accepted pick 0, a fallback where a concept existed 1, a wrong accepted concept 3. Its receipt in `docs/log/` carries the rows, the tally and a threshold sweep. The labels were written from the library's names and tags in one session, not by a designer, and the receipt says `qualified: false`. ## Cost A `feedback` run is 60 draws of two authors each at provider prices, and two replicates double it. Nothing here estimates the dollar figure: the cost row is filled from metered draws, and until it is, the receipt carries tokens only. ## Next - [Models](../models.md) - [API reference](../api.md) # First Icon (https://blode.co/iconsmith/docs/blode/cookbooks/first-icon) # Your first icon > Index: [Iconsmith documentation](../../blode.md) Draw one icon with a test key. Nothing is billed, no provider is called, and the drawing is deterministic, so this is the call to build a client against. ## The request ```bash curl https://blode.co/iconsmith/api/v1/icons \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ism_test_00000000000000000000000000000000' \ -d '{ "concept": "bookmark-check", "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 }, "model": "iconsmith-1", "n": 2 }' ``` The key can be any 32 base-62 characters after `ism_test_`. Test keys need no issuance. Drop the `Authorization` header and the same call runs for real on the free allowance: six requests per address per ten minutes. ## The response ```json { "concept": "bookmark-check", "created": 1789603200, "credits": 0, "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 }, "data": [ { "attempts": 1, "findings": [], "index": 0, "intent": "object: square; modifier: check", "mime_type": "image/svg+xml", "program": "# object: square; modifier: check\nicon square-check\nkeyline square\nfinish outlined\nrect 4,4 16x16 r3\nline 8,12 11,15 16,10\nfit", "status": "checked", "svg": "..." } ], "id": "draw_0f0ec5ea4d384e7f9c8f6f4c7e5a1d22", "images": 0, "model": "iconsmith-1", "object": "icon.draw", "prompt": null, "revision": "blode-icons-24-v1", "took_ms": 0, "usage": { "input_tokens": 0, "output_tokens": 0, "reasoning_tokens": 0, "total_tokens": 0 } } ``` ## Reading it Check the header first. `X-Request-ID` is the value to quote if anything is wrong, and `x-iconsmith-environment: test` confirms this was the sandbox. Then read `data`, in this order. 1. `status`. `checked` means it compiled, replayed exactly and carries no structural error. `draft` means it does not, and the findings say why. 2. `findings`. An empty array is clean. Otherwise each entry has a `rule`, a `message` and a `severity` of `error` or `warn`. A `checked` candidate can still carry warnings. 3. `intent`. The author's own reading of the object and modifier it drew. Compare it against what you asked for. 4. `svg`. Compiler output. Write it to a file and look at it. 5. `program`. The same drawing in the DSL, editable, and it recompiles to that same SVG. Extract the first candidate's SVG: ```bash curl -s https://blode.co/iconsmith/api/v1/icons \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ism_test_00000000000000000000000000000000' \ -d '{"concept":"bookmark-check","n":1}' \ | jq -r '.data[0].svg' > bookmark-check.svg ``` `cut` was omitted there, so the house cut was used: 24px, stroke 2, radius 3, outlined. ## Sandbox tells A sandbox draw is not a drawing of your concept. It is always the house `square-check`, every SVG root is stamped `data-iconsmith-sandbox="true"`, `took_ms` is 0, `credits` is 0 and every `usage` figure is 0. The `concept` you sent is echoed, so a client's filing logic can be tested end to end. ## Next - [Streaming](streaming.md) - [Image to icon](image-to-icon.md) - [Candidate](../primitives/candidate.md) # Image To Icon (https://blode.co/iconsmith/docs/blode/cookbooks/image-to-icon) > Index: [Iconsmith documentation](../../blode.md) Attach a raster image and the author draws the thing it shows, in the family's hand. Images can accompany words or stand in for them. ## Attach one PNG ```bash curl https://blode.co/iconsmith/api/v1/icons \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ism_test_00000000000000000000000000000000' \ -d "$(jq -n \ --arg b64 "$(base64 -w0 sketch.png)" \ '{ images: [{ base64: $b64, mime: "image/png" }], cut: { finish: "outlined", radius: 3, size: 24, stroke: 2 }, model: "iconsmith-1", n: 2 }')" ``` Base64 only. There is no URL field, because a URL would have this server fetch on a stranger's behalf. | Rule | Value | | --- | --- | | Types | `image/png`, `image/jpeg`, `image/webp` | | Images per request | 4 | | Base64 characters per image | 2000000, about 1.5 MB decoded | | Base64 characters across all images | 3500000 | | Encoding | Plain base64, matching `^[A-Za-z0-9+/]+={0,2}$` | A data URL prefix is not plain base64. Strip `data:image/png;base64,` before sending, or the request is `invalid_request` with the message `Send the image as plain base64.` ## What the author is told Your images are shown after the family sheets, in the order you attached them, and the brief says this about them: > The person attached 1 image, shown after the family sheets. Draw what they > show in the family's hand: read the object and its modifier, then construct > it from the primitives. Never trace an outline. So an attachment is a description of an object, not a drawing to copy. The author still writes a DSL program from the constrained primitives, and the compiler still chooses the geometry. An attached icon will not be reproduced line for line. The brief also carries the standing rule about meaning: state the object and the modifier, and draw both. ## Naming An images-only request has no word to look up. There is no slug, so there is no sibling sheet, and the author is told to name the icon itself with a short hyphenated slug on the program's `icon` line. The response's `concept` is that name. ```json { "concept": "watering-can", "images": 1, "prompt": null } ``` Send a `concept` or a `prompt` alongside the image to keep the filing in your hands. With a slug present, the sibling lookup runs as usual and the author sees related family drawings as well as your image. ## Privacy of the bytes Image bytes are never echoed and never logged. The response's `images` field is a count. The draw log carries the prompt and the count, nothing more. ## The image task, measured The benchmark's `image` task is this path: the house drawing rendered at 96 pixels, no words at all, scored against the library's own drawing for that slug. See [Benchmark](benchmark.md). ## Next - [Request](../primitives/request.md) - [Benchmark](benchmark.md) # Streaming (https://blode.co/iconsmith/docs/blode/cookbooks/streaming) > Index: [Iconsmith documentation](../../blode.md) A draw of two authors takes minutes. `"stream": true` answers with server-sent events, so you can show progress and each candidate as it lands. ## The frames ``` event: status data: {"attempt":1,"index":0,"stage":"drafting"} event: status data: {"attempt":1,"index":0,"stage":"checking"} event: candidate data: {"attempts":1,"findings":[],"index":0,...} event: result data: {"concept":"bookmark-check","data":[...],...} data: [DONE] ``` One frame is the text between blank lines. Frames are not ordered by author: two authors run at once, so `index` tells you which one a frame belongs to. | Event | Data | | --- | --- | | `status` | `{ attempt, index, stage }`, stage in `drafting`, `checking`, `repairing`, `done` | | `candidate` | One candidate, the same shape as an item of `data` | | `result` | The whole draw response | | `error` | The error envelope | The stream ends with `data: [DONE]`, after an `error` event as well as after a `result`. ## A client ```ts type Stage = "drafting" | "checking" | "repairing" | "done"; type Frame = | { event: "status"; data: { attempt: number; index: number; stage: Stage } } | { event: "candidate"; data: Candidate } | { event: "result"; data: DrawResponse } | { event: "error"; data: ApiError }; /** One complete frame, or null for a comment, an empty frame or [DONE]. */ const parseFrame = (frame: string): Frame | null => { let event = ""; const data: string[] = []; for (const line of frame.split("\n")) { if (line.startsWith("event:")) { event = line.slice(6).trim(); } else if (line.startsWith("data:")) { data.push(line.slice(5).trimStart()); } } const payload = data.join("\n"); if (!payload || payload === "[DONE]" || !event) { return null; } return { data: JSON.parse(payload), event } as Frame; }; export async function* draw(body: object, key?: string) { const response = await fetch("https://blode.co/iconsmith/api/v1/icons", { body: JSON.stringify({ ...body, stream: true }), headers: { "Content-Type": "application/json", ...(key ? { Authorization: `Bearer ${key}` } : {}), }, method: "POST", }); // A refusal before the stream opens is an ordinary JSON error envelope. if (!response.ok || !response.body) { throw Object.assign(new Error("draw refused"), await response.json()); } const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ""; for (;;) { const { done, value } = await reader.read(); if (done) { break; } buffer += value; // Frames are separated by a blank line; keep the trailing partial. const parts = buffer.split("\n\n"); buffer = parts.pop() ?? ""; for (const part of parts) { const frame = parseFrame(part); if (frame) { yield frame; } } } } ``` ## Using it ```ts for await (const frame of draw({ concept: "bookmark-check", n: 2 }, key)) { if (frame.event === "status") { console.log(`author ${frame.data.index}: ${frame.data.stage}`); } else if (frame.event === "candidate") { render(frame.data.svg); } else if (frame.event === "error") { throw new Error(`${frame.data.code}: ${frame.data.message}`); } } ``` ## Things to get right Split on the blank line, not on the newline. A `data:` value can span lines, and the parser joins them with a newline. Keep the trailing partial in the buffer. A chunk boundary falls wherever the network puts it, not on a frame boundary. Handle `error` frames. The stream's HTTP status is 200 even when the draw fails, so a status check alone will report success on a failed draw. Do not treat `[DONE]` as data. The parser returns `null` for it, and for comments and empty frames. `EventSource` will not work here: it cannot send a POST body or an `Authorization` header. Use `fetch`. A sandbox draw emits the same frames, so develop against a test key. ## Next - [API reference](../api.md) - [Limits and errors](../limits-and-errors.md) # Credits (https://blode.co/iconsmith/docs/blode/credits) > Index: [Iconsmith documentation](../blode.md) One credit starts one draw with up to two candidates. Browse examples and prepare a brief freely; clicking Draw requires email sign-in and prepaid account credit. The brief, cut and reference images survive sign-in and checkout in this browser. ## Buying credits Packs contain 10, 50 or 200 credits. Only configured, active, one-time Stripe prices are offered; the price and currency appear before checkout. There is no subscription or automatic recharge. Production prices still need to be set. Checkout returns to the draft. The server verifies payment and adds credits to the account exactly once, even when a webhook repeats or the return page reloads. Click Draw to spend a credit; checkout never starts a drawing automatically. Failed draws refund the credit. Cancelled checkout keeps the draft. A pending payment offers Check credits rather than asking for a second purchase. ## API keys Existing `ism_live_` keys retain their prepaid key balances. Send them as bearer credentials. New playground purchases credit the account and do not issue keys. The legacy `/keys` claim page remains for earlier purchases only. Any `ism_test_` plus 32 base-62 characters returns deterministic sandbox drawings, without a provider call, credit purchase or sign-in. Sandbox calls remain rate limited. Anonymous live generation is not offered. A browser request without a key needs a signed-in session, the same origin and account credits. Missing sign-in returns `authentication_required` (401); an empty balance returns `insufficient_credits` (402). Invalid keys are never silently downgraded to free usage. ## Billing availability `GET /api/v1/models/iconsmith-1` reports `billing.kind: fixed_credit` when billing is configured, with `credits_per_draw: 1`. Otherwise it reports `unavailable`; live generation refuses the request, while sandbox keys still work. The draw response's `credits` is the number charged for that draw: 1 for live, 0 for sandbox. It is not the remaining account balance. ## Next - [Limits and errors](limits-and-errors.md) - [API reference](api.md) # Introduction (https://blode.co/iconsmith/docs/blode/introduction) > Index: [Iconsmith documentation](../blode.md) Iconsmith draws icons in one family's hand and keeps to its grid. The model is `iconsmith-1`: twelve anchor drawings, one compiler, one house spec. One deployment serves one model, so there is no model list to choose from. Input is optional text plus optional images. Output is an icon. At least one of `concept`, `prompt` or `images` must be present. ```bash curl https://blode.co/iconsmith/api/v1/icons \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ism_test_00000000000000000000000000000000' \ -d '{ "concept": "bookmark-check", "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 }, "model": "iconsmith-1", "n": 2 }' ``` The response carries up to two candidates. Each one has an `svg`, the editable `.icon` `program` the SVG replays from, a `status` of `checked` or `draft`, and the checker's `findings`. ## What the model does not do The model writes a program, not geometry. It calls primitives (`rect`, `circle`, `arc`, `line`, `dot`, `part`); the canvas quantises construction inputs to the grid, takes corner radii from the tier system, and places parts at named quarter-turns. The model chooses what and where. The compiler chooses how. That constrains geometry. It does not establish that an icon reads as the right object. A `checked` candidate compiled, replayed exactly and carries no structural error. It has not been judged. No judge has passed the forced-choice gate in the repository's own evaluation, so nothing in the response is a quality score. Look at the drawing. ## The two ways to call it Over HTTP, as a model: `POST /api/v1/icons`, described by `GET /api/v1/models/iconsmith-1`. See the [API reference](api.md). In a coding agent, as a skill: the published `iconsmith` package installs a workflow that draws on your agent's own account, with no key and no calls to this deployment. See [Agent skill](cookbooks/agent-skill.md). ## Keys | Key | Behaviour | | --- | --- | | No `Authorization` header | Requires sign-in and prepaid account credits; anonymous live draws are refused. | | `ism_test_` plus 32 characters | The sandbox. A deterministic checked draw, no provider call, nothing billed. | | `ism_live_` plus 32 characters | Spends one credit, when credits are on for the deployment. | A test key needs no issuance. Any well-formed one is accepted. See [Credits](credits.md). ## Next - [How it works](concepts/how-it-works.md) - [Your first icon](cookbooks/first-icon.md) - [Limits and errors](limits-and-errors.md) # Limits And Errors (https://blode.co/iconsmith/docs/blode/limits-and-errors) > Index: [Iconsmith documentation](../blode.md) Every constant here is one value in the deployment's source. A request outside one of them is refused before any drawing starts. ## Input limits | Limit | Value | Field | | --- | --- | --- | | Prompt length | 500 characters, trimmed, at least 1 | `prompt` | | Concept length | 2 to 48 characters after slugification | `concept` | | Concept shape | `^[a-z0-9]+(?:-[a-z0-9]+)*$` | `concept` | | Images per request | 4 | `images` | | Base64 characters per image | 2000000, about 1.5 MB decoded | `images[].base64` | | Base64 characters across all images | 3500000 | `images` | | Image types | `image/png`, `image/jpeg`, `image/webp` | `images[].mime` | | Candidates | 1 or 2 | `n` | `limits` on the model object carries three of these: `prompt_chars` 500, `images` 4 and `image_chars` 2000000. ## Rate limits | Limit | Value | | --- | --- | | Requests per address per window | 6 | | Window | 600 seconds | | Requests in flight per instance | 4 | | `Retry-After` when the instance is full | 30 seconds | The window is per address and applies to sandbox calls. A live key skips the window, because the window exists to stop free use running a loop, and keeps the in-flight cap, because that cap exists to keep the instance alive. A test key is metered like an anonymous caller. The counters are held in process memory, so on a serverless host they are per warm instance rather than global. ## Time limits | Limit | Value | | --- | --- | | Route duration | 300 seconds | | Checks per author | 3 | | Repairs per author | 2 | A draw of two authors at high reasoning effort with repairs uses the room. The development set's median was 260 seconds per concept pair, for both paints. Set your client timeout above the route's own. ## Error codes | Code | Status | Meaning | | --- | --- | --- | | `invalid_request` | 400 | The body is not valid. `param` names the field. | | `invalid_api_key` | 401 | The `Authorization` header does not carry a usable Iconsmith key. | | `insufficient_credits` | 402 | The live key has no draws left. | | `model_not_found` | 404 | The `model` asked for is not the one this deployment serves. | | `rate_limit_exceeded` | 429 | The per-address window is full. Carries `retry_after`. | | `server_error` | 500 | Reserved. In the code set, not emitted by the v1 routes. | | `model_error` | 502 | The draw failed before a drawing came back. | | `model_offline` | 503 | No model credential is configured for this deployment. | | `model_unavailable` | 503 | Too many draws in flight. Carries `retry_after`. | ### Messages you will see | Code | Message | | --- | --- | | `invalid_request` | `Send a JSON body.` | | `invalid_request` | `Send a prompt, a concept, or at least one image.` | | `invalid_request` | `Unrecognized request argument supplied: ` | | `invalid_request` | `Name the icon in 2 to 48 characters.` | | `invalid_request` | `Use letters, numbers and hyphens, such as bookmark-check.` | | `invalid_request` | `Expected one of 16, 20, 24` | | `invalid_request` | `Send the image as plain base64.` | | `invalid_request` | `Attach at most 4 images.` | | `invalid_request` | `An image may be at most 2000000 base64 characters.` | | `invalid_request` | `Images may total at most 3500000 base64 characters.` | | `invalid_api_key` | `The Authorization header does not carry an Iconsmith key.` | | `invalid_api_key` | `The Authorization header does not carry an Iconsmith key.` | | `invalid_api_key` | `This key is not recognised.` | | `insufficient_credits` | `This key has no draws left. Buy another pack to continue.` | | `model_not_found` | `No model named "sketcher-2". This deployment serves iconsmith-1.` | | `rate_limit_exceeded` | `You have drawn a lot recently. Try again in a few minutes.` | | `model_offline` | `Generation is offline: no model credential is configured for this deployment.` | | `model_unavailable` | `The drawing room is full. Try again in half a minute.` | | `model_error` | `The model call failed before a drawing came back.` | ## Retry guidance | Code | Retry | | --- | --- | | `invalid_request` | No. Fix the body. `param` names the field. | | `invalid_api_key` | No. Fix the key, or sign in to draw with account credits. | | `insufficient_credits` | No. Buy another pack. | | `model_not_found` | No. Send `iconsmith-1`, or omit `model`. | | `rate_limit_exceeded` | Yes, after `retry_after` seconds. | | `model_unavailable` | Yes, after `retry_after` seconds, which is 30. | | `model_offline` | No, not by retrying. The deployment has no credential. | | `model_error` | Once, with the same body. A second identical failure is not transient. | | `server_error` | Once. | Read `retry_after` from the body, or `Retry-After` from the headers. Both carry seconds, and both are present only on the codes above. A live key spends its credit before admission, so a refused draw never spends. A draw that fails after admission is refunded. On a stream the HTTP status is 200 even when the draw fails, so handle the `error` event rather than the status code. Quote `request_id`, or the `X-Request-ID` header, in any report. They are the same value. ## Next - [API reference](api.md) - [Credits](credits.md) # Models (https://blode.co/iconsmith/docs/blode/models) > Index: [Iconsmith documentation](../blode.md) One deployment serves one model. There is no model list to choose between, and no aliases. | Model | Revision | Candidates | Input | Output | | --- | --- | --- | --- | --- | | `iconsmith-1` | `blode-icons-24-v1` | up to 2 | text, image | icon | Pricing is not set. The model card's cost row reads `not yet measured`, and the price per draw is a function of that measurement. See [Credits](credits.md). ## The model object ```bash curl https://blode.co/iconsmith/api/v1/models/iconsmith-1 ``` ```json { "billing": { "kind": "free_allowance", "per_window": 6, "window_seconds": 600 }, "card": "https://blode.co/iconsmith#model-card-heading", "checks": { "rules": [ "arrowhead-quality", "bleed", "centred", "cohort-align", "cut", "density", "empty", "enclosure-alignment", "extent", "feature", "finish", "gap", "hole", "keyline", "off-axis", "paint", "substance" ] }, "created": 1789603200, "cuts": { "finishes": ["outlined", "filled"], "radii": [0, 1, 2, 3], "sizes": [16, 20, 24], "strokes": [1, 1.5, 2, 2.5, 3] }, "family": { "drawings": 4357, "license": "MIT", "set": "blode-icons" }, "id": "iconsmith-1", "input_modalities": ["text", "image"], "limits": { "image_chars": 2000000, "images": 4, "prompt_chars": 500 }, "max_candidates": 2, "name": "Iconsmith", "object": "model", "output_modalities": ["icon"], "pipeline": { "concept_sketch": false, "prompt_resolution": false }, "revision": { "anchors": 12, "compiler": "iconsmith-constrained-25-source-exact", "id": "blode-icons-24-v1", "sha256": "9876c68488816f34afb8b38bfffe3b5c99beff64d6165d60c603242c06e5bce0" } } ``` | Field | What it is | | --- | --- | | `id`, `name`, `object` | The product id, its display name, and `model`. | | `created` | Unix seconds for the release of this framing. | | `revision` | The pinned style revision, its anchor count, compiler and digest. | | `family` | The reference set, its size and its licence. | | `cuts` | The four enumerations, as [The cut](concepts/the-cut.md) documents them. | | `limits` | Prompt characters, image count, base64 characters per image. | | `max_candidates` | The ceiling on `n`. | | `checks.rules` | The 17 lint rules a finding can name. | | `input_modalities`, `output_modalities` | `text` and `image` in, `icon` out. | | `billing` | `free_allowance` or `fixed_credit`, depending on the deployment. | | `card` | A link to the rendered model card. | | `pipeline` | Whether prompt resolution and the concept sketch are on for this deployment. | `billing` has two shapes. With credits off it is `{ kind: "free_allowance", per_window, window_seconds }`. With credits on it is `{ kind: "fixed_credit", credits_per_draw: 1, unit: "credits_per_draw" }`. `pipeline` is two booleans and nothing else. It says whether prompt resolution and the concept sketch are on for this deployment, so a benchmark receipt can record what it measured. What runs them is a deployment detail and is not reported. ## The model card Every number on the card is read from `packages/iconsmith/bench/model-card.v1.json`, which is generated from committed receipts. Nothing on it is typed in by hand. Each row carries its own `n`, population, sources and caveat. | Row | Reading | n | From | | --- | --- | --- | --- | | Family | blode-icons, 4,357 drawings, 12 pinned anchors, revision `blode-icons-24-v1` | 4357 | `library/blode-icons/SOURCE.json`, `examples/starter/revision.json` | | Checks | 17 rules, exact replay; only compiler output reaches the page | 17 | `src/tools`, `src/pipeline/style.ts` | | House yardstick | blode-icons passes its own strict spec 40.9% of the time (gate 99.0%) | 2221 | `bench/public-conformance.v1.json` | | Against other stroke packs | house strict 33.8% vs tabler 22.3%; pooled floor of 4 packs 20.3% | 8794 | `bench/calibration.v1.json` | | Generated, development set | 10/10 gate, 4/10 strict after repair; 7/10 review-clear first pass; median 260 s per concept pair | 10 | `output/ten-icons-2026-09-09/validation.json`, `output/ten-icons-repaired-2026-09-09/validation.json` | | Off-axis edges | 29.1% of stroked house icons carry one (480 of 1,649) | 1649 | `bench/public-conformance.v1.json`, `src/tools/lint.ts` | | Instruments | style similarity discarded at AUC 0.476; rendered cosine AUC 0.864 | 300 | `bench/calibration.v1.json`, `bench/stress-cosine.v1.json` | | Benchmark | not yet run | 0 | `scripts/icon-bench.ts` | | Cost and latency per draw | not yet measured | 0 | `docs/log/` | ### What each row does not say Checks. The checks constrain geometry. They do not establish that an icon reads as the right object. House yardstick. The set fails its own strict reading. A generator is not held to 1.0. This is the one number on the card that any checkout can reproduce, and it is re-derived from the vendored library with the current linter. Against other stroke packs. Measured on a private store with the August linter. The third-party packs are not vendored and cannot be re-measured from a public checkout. Generated, development set. n=10, 24px, repaired outputs, reviewed at the root rather than independently. Development yield, not a craft score. The receipt records `qualification: false`. Off-axis edges. The set's own habit, permitted by name, so the linter warns rather than errors. Instruments. Published because a metric that failed is evidence too. Judge accuracy there is a forced choice between a shipped icon and an unrelated one, n=30 per model, and passing an easy semantic control does not qualify subtle craft judgment. Benchmark. Filled from the newest live receipt under `packages/iconsmith/bench/runs` once one exists. Sandbox runs are not scores. Until then the row reads `not yet run`. Cost and latency per draw. Filled from `docs/log/web-draws-*.json` once 100 metered draws are on file. No estimate stands in for the measurement. Until then the row reads `not yet measured`. There is no quality score, no recognition rate, and no parity claim. Two independent reviewers on 10 stimuli disagreed on every one of them, and 6 of 10 were quarantined, so the critic is not qualified either. ## The house spec The card records the spec the compiler derives from, at the house cut. | Constant | Value | | --- | --- | | `grid` | 0.25 | | `stroke` | 2 | | `minFeature` | 1.5 | | `minGap` | 1 | | `clearance` | 2 | | `radiusTiers` | 0.5, 1, 2, 3 | ## Next - [The family](concepts/the-family.md) - [Benchmark](cookbooks/benchmark.md) - [Limits and errors](limits-and-errors.md) # Candidate (https://blode.co/iconsmith/docs/blode/primitives/candidate) > Index: [Iconsmith documentation](../../blode.md) `data` on a draw response is an array of candidates, one per author. `n` sets how many, 1 or 2, and 2 is the default. ```json { "attempts": 1, "findings": [], "index": 0, "intent": "object: square; modifier: check", "mime_type": "image/svg+xml", "program": "# object: square; modifier: check\nicon square-check\nkeyline square\nfinish outlined\nrect 4,4 16x16 r3\nline 8,12 11,15 16,10\nfit", "status": "checked", "svg": "..." } ``` | Field | Type | What it is | | --- | --- | --- | | `index` | integer | The author's position, from 0. | | `mime_type` | string | Always `image/svg+xml`. | | `svg` | string | Compiler output. Never model output. | | `program` | string | The editable `.icon` program the SVG replays from. | | `status` | string | `checked` or `draft`. | | `attempts` | integer | Attempts the author made, including the first draft. | | `intent` | string or null | The author's own one-line reading of the object and modifier it drew. | | `findings` | array | What the checker found. | ## svg and program `svg` is what the compiler emitted for the program. The model's text is never passed through. `program` is the same drawing in the DSL. It is the editable artifact: edit a line, recompile, and the SVG changes with it. Replay is exact, so the program in the response recompiles to the SVG in the response. ## status | Status | Meaning | | --- | --- | | `checked` | Compiled, exact replay, no structural errors. | | `draft` | Still carries a structural error after the repair allowance, or never compiled. | A draft is returned rather than dropped, with its findings, so you can see what went wrong. `checked` is a structural statement and nothing more. It does not say the icon reads as the right object, and there is no approval field. You are the reviewer. ## attempts One draft plus up to two repairs. The tool budget is three checks per author, so `attempts` is 1, 2 or 3. A higher number is not a worse drawing; it means the author read findings and repaired from them. ## intent The author states the object and the modifier it drew, as a comment on the program's first line, and that line is lifted into `intent`: ``` # object: square; modifier: check ``` It is the author's own reading, not a verification. Compare it against what you asked for. A `search-check` needs a magnifying glass and a check, not a check alone, and `intent` is where an author admits it drew only the check. `intent` is `null` when the program carried no such line. ## findings A finding is one rule's verdict on this drawing. ```json { "message": "Canvas is empty.", "rule": "empty", "severity": "error" } ``` | Field | Type | What it is | | --- | --- | --- | | `rule` | string | The rule that fired. One of the 17 named on the model object. | | `message` | string | What the rule saw, in words. | | `severity` | string | `error` or `warn`. | ## Errors and warnings An `error` is a structural fault. It blocks `checked`: a candidate that still has one after the repair allowance is a `draft`. Errors are also what the repair loop sends back to the author. A `warn` is a reading the linter will not call a fault. The family's own habits live here. Off-axis edges are the clearest case: they are 29.1% of stroked house icons, they are deliberate, and the canvas permits them by name, so the rule warns instead of erroring. A candidate can be `checked` and still carry warnings. Read them. Resolve each one with a visual reason or a repair. The 17 rules, as `GET /api/v1/models/iconsmith-1` lists them under `checks.rules`: `arrowhead-quality`, `bleed`, `centred`, `cohort-align`, `cut`, `density`, `empty`, `enclosure-alignment`, `extent`, `feature`, `finish`, `gap`, `hole`, `keyline`, `off-axis`, `paint`, `substance`. ## Next - [Request](request.md) - [API reference](../api.md) - [Models](../models.md) # Request (https://blode.co/iconsmith/docs/blode/primitives/request) > Index: [Iconsmith documentation](../../blode.md) One draw's input is optional text plus optional images. Three fields carry it, and at least one of the three must be present. | Field | Type | What it is | | --- | --- | --- | | `concept` | string | An explicit slug. The name the drawing is filed under, and the word the library is searched with. | | `prompt` | string | Free text. A brief in words. | | `images` | array | Up to four raster images, shown to the author after the family sheets. | Send none of the three and the request is refused: ```json { "code": "invalid_request", "message": "Send a prompt, a concept, or at least one image.", "param": "prompt", "request_id": "0f0ec5ea-4d38-4e7f-9c8f-6f4c7e5a1d22", "status": 400 } ``` ## concept A slug. The value is slugified first: lowercased, runs of non-alphanumeric characters replaced with a single hyphen, leading and trailing hyphens stripped. `Bookmark check!` becomes `bookmark-check`. The slugified value must be 2 to 48 characters and match `^[a-z0-9]+(?:-[a-z0-9]+)*$`. Outside that: | Failure | Message | | --- | --- | | Too short or too long | `Name the icon in 2 to 48 characters.` | | Wrong characters | `Use letters, numbers and hyphens, such as bookmark-check.` | ## prompt Free text, trimmed, 1 to 500 characters. `MAX_PROMPT` is 500. The brief quotes it to the author verbatim. It does not replace the concept: when `concept` is absent, the slug is derived from the prompt. ## images An array of at most four entries. `MAX_IMAGES` is 4. ```json { "images": [ { "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime": "image/png" } ] } ``` | Field | Rule | | --- | --- | | `mime` | One of `image/png`, `image/jpeg`, `image/webp`. | | `base64` | Plain base64, matching `^[A-Za-z0-9+/]+={0,2}$`. At least 1 character. | | Limit | Value | | --- | --- | | `MAX_IMAGES` | 4 | | `MAX_IMAGE_CHARS` | 2000000 base64 characters per image, about 1.5 MB decoded | | `MAX_IMAGES_CHARS` | 3500000 base64 characters across all images | Base64 only. A URL is not accepted, because that would have this server fetch on a stranger's behalf. Image bytes are never echoed and never logged. The response's `images` field is a count, not the images. The draw log carries the prompt and the count. ## How an unnamed brief is named The slug is resolved in this order. 1. `concept`, when you sent one. 2. Otherwise the slug derived from `prompt`: slugified, cut to the first 48 characters, trailing hyphens stripped. If that leaves fewer than 2 characters, it is empty. 3. Otherwise empty, which is also the case for an images-only request. When the slug is empty the author is told to draw one icon at the requested finish and size and to name it itself, with a short hyphenated slug on the `icon` line. The response's `concept` is that name. An images-only brief has no word to look up, so there is no sibling sheet for it. The author works from the anchors and your images. ## The whole body ```json { "concept": "bookmark-check", "cut": { "finish": "outlined", "radius": 3, "size": 24, "stroke": 2 }, "images": [], "model": "iconsmith-1", "n": 2, "prompt": "a bookmark with a tick on it", "stream": false } ``` `prompt` is a string or absent. It is not nullable on the way in; the response echoes it as `null` when you sent none. The body is strict. An unrecognised field is `invalid_request` with `param` naming it and a message of the form `Unrecognized request argument supplied: colour`. A `model` other than the served id is `model_not_found`. ## Next - [The cut](../concepts/the-cut.md) - [Candidate](candidate.md) - [API reference](../api.md) # CLI (https://blode.co/iconsmith/docs/cli) The published iconsmith package is a local compiler: prepare, check, render, draw, lint. The published npm package is the compiler CLI, not the hosted model. Drawing through `iconsmith-1` is the [HTTP API](https://blode.co/iconsmith/docs/api) or the [public skill](https://github.com/mblode/iconsmith). These commands never call a model. Requires Node.js 24.11 or newer. Prefix commands with `npx --yes iconsmith@0.1.0`. Use `--help` for options. | Command | Purpose | | --- | --- | | `prepare` | Save a request, pinned revision and related drawings | | `check` | Compile a candidate, verify exact replay and create preview images | | `render` | Preview an existing SVG at native size and on light/dark backgrounds | | `draw` | Compile an `.icon` drawing to SVG | | `lint` | Check SVG files or piped SVG against the house spec | | `schema` | Read command arguments, options, defaults and enums as JSON | A selected result is independently reviewed development work. The compiler still reports `craftApproved: false`: structural checks, not visual approval. # Evaluation Notes (https://blode.co/iconsmith/docs/evaluation-notes) The historical reconstruction evaluation reports floor, baseline, treatment and ceiling separately. Its recorded floor is 0.482, and its baseline is 0.737, the measured median rendered cosine between two mature sets drawing the same concept. The ceiling is 1.0, the target compared with itself. These are corpus measurements, not a calibrated score for newly generated artwork. High similarity requires provenance interpretation. An exact host reconstruction can legitimately approach 1.0. A model-generated reconstruction with no admitted parts and similarity above 0.95 is a leakage diagnostic. Similarity alone neither proves leakage nor approves craft. The historical strict-spec measurements were 33.8% for the house set and 22.3% for the best third-party stroke pack. These are dated development observations. Without the private corpus, their measurement tests skip and the figures are unverified locally. Do not construct a replacement corpus to make those gates pass. Read [the foundry log](foundry-log.md) for evidence and superseding decisions, and [the local foundry guide](local-foundry.md) for the current distinction between structural delivery, independent review and qualified acceptance. # Foundry Footguns (https://blode.co/iconsmith/docs/foundry-footguns) --- Content truncated to keep this response under crawler size limits. Use https://blode.co/iconsmith/docs/llms.txt for the full page index and fetch individual .md pages for uncapped content.