Tilesets: measured once per kind
Called out separately because these are the most capable endpoints for level art and they are relevant to the disc-golf game. pixelkiln plans a top-down set at PixelLab's own quote (Pro 15 for tiles up to 32px, 25 at 64px; standard 4, the top of its quoted 1 to 4). Every kind is measured below: Pro billed exactly its quote at 32px and 64px, and standard billed 3, under the plan.
POST /tilesets (top-down) and /tilesets-sidescroller are the current
versions; /create-tileset* are the older aliases with identical schemas.
Both are wrapped by pixelkiln's terrain generator
(skills/pixelkiln/references/pixellab.md), the sidescroller one with
terrainOrientation: "sidescroller"; the reference-image/color_image
knobs below are not.
They take lower_description + upper_description (+ optional
transition_description). You describe two terrains and the transition between
them, and get a tileset that blends them. Distinctively, they accept both
color_image and per-layer reference images (lower_reference_image,
upper_reference_image, transition_reference_image). They are the only
family that combines palette forcing with style anchoring.
Useful knobs: tile_size, tileset_adherence / tileset_adherence_freedom
(how strictly tiles must fit together), and the usual outline / shading /
detail. Pro's border controls are raggedness, roundness,
boundary_amplitude with boundary_seed, step_slope, and
connect_diagonals; spread_x and slope_size are documented as "No
effect" now and kept only so existing calls work.
lower_base_tile_id / upper_base_tile_id connect a new set to another
set's terrain. GET /tilesets/{id} (observed live) has a top-level
metadata.terrain_ids of { "lower": "<uuid>", "upper": "<uuid>" }: those
are the base tile ids, so set A's upper id sent as set B's
lower_base_tile_id makes the two share that terrain. pixelkiln records them
as terrainIds when it polls a set.
Pro at 64px, edge slope, and standard sets, measured
A second round on the same showcase project billed 46 generations in all (balance 5055 to 5009), each set exactly as below:
lava-floor: Pro, 64px,terrainTransitionSize: 0.5,terrainRaggedness: 0.4,terrainBoundaryAmplitude: 0.15,terrainBoundarySeed: 3, prompt1). glowing lava texture 2). dark dungeon stone floor texture 3). crumbling dungeon brick wall. Billed exactly 25 and returned 25 tiles covering 21 distinct corner combinations, which confirms that Pro switches to the tall-wall layout at 0.5, not only at 1, and that four tiles are corner twins. The third description became the brick wall dropping into the lava.swamp-marsh: Pro, 32px,terrainTransitionSize: 0.25,terrainStepSlope: 1.5,terrainRaggedness: 0.8. Billed exactly 15, 16 tiles, with a ragged bank between the water and the marsh grass.lake-grassandpath-grass: standard, 32px. Each billed 3, under the planned 4.path-grassusedconnect: { upper: { of: "lake-grass", terrain: "upper" } }, sentupper_base_tile_id, and its recordedterrain_ids.uppercame back identical tolake-grass's upper id. The standard sets look flatter, with a hard edge, as PixelLab's tilesets tutorial describes.
pixelkiln keeps connected sets in one style, and a style fixes
terrainMode, so a standard set cannot connect to a Pro set.
Pro top-down and a connected pair, measured
October 2026, through pixelkiln gen on the website's showcase project
(website/art/pixellab-0.4.130). Two Pro sets at 32px with
terrainRoundness: 2 and terrainConnectDiagonals: true:
ocean-sand("deep ocean water texture" to "plain smooth sand texture") billed exactly 15 generations, 16 tiles. Itsmetadata.terrain_idsheld both alowerand anupperid, recorded asterrainIds.sand-grass("plain smooth sand texture" to "plain short grass texture") withconnect: { lower: { of: "ocean-sand", terrain: "upper" } }stayed blocked untilocean-sandwas downloaded, then sentocean-sand's upper id aslower_base_tile_idand billed exactly 15.- Painted together on one vertex grid, the two sets join at the shared sand with matching colour. The second set's sand carries fewer pebbles, so the ring where it meets the first set's sand is visible up close. "plain smooth sand texture" still came back with pebbles in the first set.
Sidescroller, measured
One POST /create-tileset-sidescroller call, 16px tiles,
transition_size: 0.25, lower_description "mossy stone bricks texture",
transition_description "green grass texture", billed exactly 3
generations (balance 5107 to 5104). PixelLab's MCP tool quotes 2 or 3.
- The body has no
mode,view,upper_description, or shape controls.tile_sizeis 16 or 32 only,transition_sizeone of 0, 0.25, 0.5, 1.lower_base_tile_idis the only connection field. - Submit answers
{ tileset_id, background_job_id, status: "processing" }, withusage: null. GET /tilesets/{id}(and/tilesets-sidescroller/{id}) returns the top-down shape: 16 tiles namedwang_0towang_15, each withcornersandpattern_4x4, images base64 inline. Buttileset.terrain_typesis["central", "background"]while every corner still says"lower"(the platform) or"upper"(empty, transparent air; the all-upper tile is blank). pixelkiln records["lower", "upper"]so its corner masks work.metadata.terrain_idshasloweronly, andmetadata.viewis"side".metadata.transition_sizecame back0.0although 0.25 was sent.usageis null on the GET, so the billed amount comes fromGET /background-jobs/{background_job_id}(usage.generations: 3.0).
A trimmed copy of that response is test/fixtures/tileset/sidescroller-get.json.
create-tiles-pro is a different shape, a single description plus
style_images, tile_view, building_* fields for structures.
Its style_images is a fourth convention, and not the one two lines up:
TilesProStyleImage is flat, with all three fields required.
// create-tiles-pro
{ "style_images": [ { "base64": "...", "width": 32, "height": 32 } ] }
Sending generate-with-style-v2's nested {image: {...}} here is rejected as
an extra field. Passing style images at all makes the endpoint ignore
tile_type and tile_view and copy the reference's tile geometry instead.
That is the only way to land new art on an existing sheet's ground plane.
GET /tiles-pro/{id} carries no status field. It answers 423 while the set
is still drawing and 200 with storage_urls when it is done, so the HTTP code
is the status. Cost is reported at submit time and lands on the same 20/25/40
canvas tiers as 1dir, but the canvas is tile size x variation count, not one
sprite. A small tile in a large set still reaches the top tier.
All response shapes used by the client are validated at runtime. The TypeScript
interfaces alone are not trusted at the HTTP boundary: missing ids, invalid
object geometry, malformed tile URL maps, or balance fields with changed types
produce an Invalid PixelLab response error before pipeline state is updated.
outline_mode defaults to outline, which draws a dark border around every
tile. For ground tiles that is wrong: laid on a grid the borders read as
quilting, with a seam at every cell edge. segmentation omits them and the
same set tiles seamlessly, measured on a fairway-to-rough terrain set, where
it was the difference between usable and unusable.
tile_feature and style_images are mutually exclusive. The API says
"Connectable features (roads/tileset/building) cannot be combined with style
tiles". So the
geometry-anchoring trick above is unavailable for a connectable set, and its
tiles land on whatever ground plane the view angle implies (measured: 32x24
with the diamond midline at y=7, against a 32x32 sheet wanting y=15).
tile_feature turns it from independent variations into a connectable set:
roads (18-configuration path set), tileset (16-tile Wang corner set for a
terrain transition; describe it as the transition, not one terrain), and
building (floor/wall/doorway kit). These are sliced by index, so the returned
order is load-bearing. They are structural multi-output results, not candidates
to choose between: PixelKiln persists every URL in numeric order and labels the
downloaded outputs tile-00, tile-01, … .
The completed response also carries tile_rules for connectable sets. Its
placement contract is rule_type (edge, corner, or outline), arity (4
for square/isometric edges or corners, 6 for hex edges), connectivity
(same/other), two terrain labels, and a tiles map of provider tile key to
bitmask. Four-edge masks use N/E/S/W as bits 0/1/2/3. Four-corner masks use
NW/NE/SW/SE as bits 3/2/1/0. Tiles absent from that map are stamp-only. PixelKiln
stores the complete object in providerMetadata.pixellab.tileRules; exporters
normalize the documented subset but retain the raw object in the generic file.
Given that color_image is honoured on pixflux but silently ignored on
resize, verify the palette actually holds on a single tileset before
committing to a set.
