Drive Core Collage from your own code
Both metered lanes are available over HTTP: name my core (a list of things someone loves turned into a named micro-aesthetic - its verdict, a five-colour palette with roles, type direction, one motif per line, the 2026 visual languages it blends and the ones to keep out) and style one thing (that core applied to a shelf, an outfit, a table, a screen ... with a keep / tweak / drop call on every item the person already owns, the look, ordered steps, photo shots and what to avoid). A natural use is a wardrobe, room-planning or party app that wants a "find your aesthetic" step: your code sends the list, draws the board, then sends the core with an inventory.
The division of labour first: the page reads and draws; the model names and directs; the page re-checks every quote, item and colour. The twelve visual languages with their pair-well / avoid table, the word-list reading of each line, the clash check, a local palette with an ink-on-paper contrast check and the keep / drop guess per owned item come from corekit.js, which runs in node as well as in the browser; boardview.js draws the mood board as SVG. CoreKit.check holds each reply to the person's own words: every motif must quote one of their lines, every item must be one they listed, every colour named must be in the palette, a blend of two fighting languages must be called split, no internal field names in prose. The word-list reading is a guess, and the page says so.
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":"core-collage"}), 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": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or task / loves is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Every run sends task: core or apply. Both send the loves list; each lane adds its own fields. The model follows only the named task's contract.
| Task | Verdicts | Reply fields |
|---|---|---|
core | cohesive (most lines point to one language or two that may be combined), split (two strong groups whose languages fight - the tension names both and a bridge), scattered (a grab bag - the core is built from two or three seed lines) | task, verdict, title, core_name (ends in "core"), tagline, summary, languages [{id, weight, why}] (1-3), palette [{hex, name, role}] (5: paper, ink, accent, pop, shadow), type {display, text, treatment}, motifs [{from, motif, language}] (one per line, from copied exactly), textures, we_are, we_are_not, avoid [{id, why}], tension, next |
apply | go (most items keep, at most one drop), tweak (several tweaks or one or two drops), rethink (half or more drop, or the thing fights the core) | task, verdict, title, summary, premise, items [{item, call, how}] (one per owned line, call keep / tweak / drop), look {composition, light, palette_use, texture}, steps (3-6), add (0-3), shots (3), avoid (2-4) |
The twelve language ids: naive, type_collage, blueprint, trinket, punk_grunge, future_medieval, distorted_portrait, surveillance, grainy_blur, signal, frutiger_aero, kidcore (CoreKit.LANGS holds names, palettes and the pair / avoid table).
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (CoreKit.toWire does this). The schema requires task and loves.
Input fields
| Field | Lane | Type | Notes |
|---|---|---|---|
task | both | string | core or apply. |
loves | both | string | Numbered lines, one thing each (1. ballet flats with ribbons). The first 12 lines, cut on whole lines past 1,600 characters. |
nots | core | string | Things that feel not-them, one per line (may be empty). |
colors | core | string | Colour words they keep choosing (may be empty). |
page_board | core | string (JSON) | {lines_read, lines_recognised, leading: [{language, share_pct, lines}], clash, per_line: ["1. ... -> trinket", ...]} from CoreKit.readLoves. |
page_verdict | both | string | The page's own guess: cohesive / split / scattered for a core, go / tweak / rethink for apply, or empty. |
core | apply | string (JSON) | {name, tagline, languages: [ids], palette: [{hex, name, role}], motifs, we_are, we_are_not, avoid: [ids], source} - from a core reply (CoreKit.coreFromReply) or the page's guess. |
thing | apply | string | What is being styled: "a shelf or a corner", "an outfit", "a party or dinner table", "a desk", "a whole room", "a phone home screen", "gift wrapping", "an invitation or poster". |
occasion | apply | string | One line about it (may be empty). |
owned | apply | string | Numbered lines: what they already have for it (up to 14). |
page_items | apply | string (JSON) | ["1. pink satin ribbon -> keep (pink is close to ...)", ...] from CoreKit.readOwned. |
retry_note | both | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with CoreKit.parseReply, then normalize(reply, task), then check(reply, state, task), which returns {notes}: the page's notes (info, warn) shown under "The page's check".
Worked examples
Replies produced against the app's prompt for the three sample lists (made up; lists shortened; the full replies ship in example-result.js). Together they cover every verdict of both lanes. The page replays each of them free from its sample buttons.
task: core - ribbons and rain - cohesive
Input:
{
"task": "core",
"loves": "1. ballet flats with ribbons\n2. rainy afternoons by the window\n3. my grandmother's button tin\n4. old film photos with li ... (220 characters)",
"nots": "anything neon\nchrome",
"colors": "dusty pink, cream, sage",
"page_board": "{\"lines_read\":7,\"lines_recognised\":7,\"leading\":[{\"language\":\"grainy_blur\",\"share_pct\":50,\"lines\":4},{\"language\":\"trinket ... (562 characters)",
"page_verdict": "cohesive"
}
Reply:
{
"task": "core",
"verdict": "cohesive",
"title": "Grandmother's Tin in Soft Rain Light",
"core_name": "Rainlight core",
"tagline": "Small kept things, softened by rain, film grain and slow tea mornings",
"summary": "Every line here is something small and kept - a ribbon, a tin of buttons, a shell from one trip - looked at through rained-on glass and old film grain. The core sits in cream, sage and dusty pink, with museum-drawer objects laid out soft and a little blurred, like light leaking across a table on a slow morning.",
"languages": [
{
"id": "trinket",
"weight": 3,
"why": "The ribbons, the button tin, the pressed flowers and the seashells are all small objects kept and laid out, which is exactly Trinket Design's museum-drawer feeling."
},
{
"id": "grainy_blur",
"weight": 2,
"why": "The rain by the window, the old film's light leaks and the slow tea mornings all read soft, desaturated and a little blurred, which is Grainy Blur's territory."
}
],
"palette": [
{
"hex": "#f2e9dc",
"name": "Tea-Stained Page",
"role": "paper"
},
{
"hex": "#2b241d",
"name": "Film-Leak Ink",
"role": "ink"
},
{
"hex": "#9caf88",
"name": "Pressed Sage",
"role": "accent"
},
{
"hex": "#c98a92",
"name": "Ribbon Pink",
"role": "pop"
},
{
"hex": "#6b5d52",
"name": "Rainy Windowpane",
"role": "shadow"
}
],
"type": {
"display": "a soft, slightly uneven rounded serif for headlines, like handwriting on the back of an old photo",
"text": "a plain, gentle sans for body copy, small and unfussy",
"treatment": "Headlines sit a little loose and tilted like a hand-written label, while body text stays quiet so the kept objects do the talking."
},
"motifs": [
{
"from": "ballet flats with ribbons",
"motif": "ribbon trailing from a flat's strap",
"language": "trinket"
},
{
"from": "rainy afternoons by the window",
"motif": "raindrops blurring a windowpane view",
"language": "grainy_blur"
},
{
"from": "my grandmother's button tin",
"motif": "buttons sorted loose inside a tin",
"language": "trinket"
},
"..."
],
"textures": [
"rain-fogged glass",
"button-tin rattle",
"..."
],
"we_are": [
"soft",
"kept",
"unhurried"
],
"we_are_not": [
"glossy",
"loud",
"synthetic"
],
"avoid": [
{
"id": "signal",
"why": "Signal Graphics leans on chrome type and maximum saturation, the opposite of this soft, kept-things mood - and chrome is one of the things you ruled out."
},
{
"id": "punk_grunge",
"why": "Punk Grunge's neon pink and xerox noise would clash with the muted naturals here, and neon is one of the things you ruled out."
},
"..."
],
"tension": "",
"next": "Start with the button tin itself - clear a small shelf, line it with something soft, and let it be the first object the rest of the core gathers around."
}
task: core - cathedrals and crayons - split
Input:
{
"task": "core",
"loves": "1. gothic cathedrals\n2. tarot cards\n3. candlelit dinners\n4. stained glass windows\n5. velvet\n6. dinosaur stickers\n7. rain ... (174 characters)",
"nots": "beige minimalism",
"colors": "burgundy, gold, bright yellow",
"page_board": "{\"lines_read\":9,\"lines_recognised\":9,\"leading\":[{\"language\":\"future_medieval\",\"share_pct\":48,\"lines\":5},{\"language\":\"kid ... (615 characters)",
"page_verdict": "split"
}
Reply:
{
"task": "core",
"verdict": "split",
"title": "Cathedral Hush Meets Birthday Riot",
"core_name": "Velvet Dinosaur core",
"tagline": "Stained glass and tarot decks share a shelf with dinosaur stickers and bright balloons",
"summary": "Every line here goes all-in on rich, saturated color instead of anything muted - gold, burgundy, and bright yellow, never beige. Half the list wants gothic hush (cathedrals, tarot, candlelight, stained glass, velvet) and half wants loud kid joy (dinosaurs, crayons, plushies, balloons), so the core puts a gilded, arched room around a pile of sticker-covered toys.",
"languages": [
{
"id": "future_medieval",
"weight": 3,
"why": "Gothic cathedrals, tarot cards, candlelit dinners, stained glass windows, and velvet all lean into gilded, moody grandeur."
},
{
"id": "kidcore",
"weight": 2,
"why": "Dinosaur stickers, rainbow crayons, plushies, and birthday balloons all lean into bright, construction-paper playfulness."
}
],
"palette": [
{
"hex": "#140b12",
"name": "Cathedral Dark",
"role": "paper"
},
{
"hex": "#fdf1c7",
"name": "Candle Wax",
"role": "ink"
},
{
"hex": "#7a1f3d",
"name": "Cathedral Velvet",
"role": "accent"
},
{
"hex": "#ffe135",
"name": "Crayon Sun",
"role": "pop"
},
{
"hex": "#7c531a",
"name": "Candlelit Gold",
"role": "shadow"
}
],
"type": {
"display": "a tall, sharp blackletter-style display face, slightly cracked at the edges",
"text": "a plain, easy-to-read rounded sans for the fine print",
"treatment": "Headlines get gothic arch flourishes while the body stays plain, so the crayon colors do the loud talking."
},
"motifs": [
{
"from": "gothic cathedrals",
"motif": "pointed arch window frame",
"language": "future_medieval"
},
{
"from": "tarot cards",
"motif": "small gilt-bordered card illustration",
"language": "future_medieval"
},
{
"from": "candlelit dinners",
"motif": "flickering candle flame glow",
"language": "future_medieval"
},
"..."
],
"textures": [
"gilt edges on worn card stock",
"candle wax drips",
"..."
],
"we_are": [
"reverent",
"playful",
"jewel-bright"
],
"we_are_not": [
"beige",
"muted",
"serious"
],
"avoid": [
{
"id": "naive",
"why": "Naive Design's crayon wobble reads as kid stuff and undercuts the hushed gothic grandeur the cathedral and candlelight lines want."
},
{
"id": "surveillance",
"why": "Surveillance Design's cold HUD brackets would flatten the warm, playful kid-sticker side of this core."
}
],
"tension": "Lines 1-5 (gothic cathedrals, tarot cards, candlelit dinners, stained glass windows, velvet) want hushed, gilded gothic weight - that's Future Medieval. Lines 6-9 (dinosaur stickers, rainbow crayons, plushies on my bed, birthday balloons) want loud Kid Core play, and the two are a listed clash. The bridge: neither side goes near anything muted - burgundy, gold, and bright yellow all stay fully saturated, so the thread holding them together is never-beige, always-vivid color. The choice: let the cathedral set the room and the kid stuff be the surprise inside it, or flip it and let the crayon energy lead with gothic details as trim.",
"next": "Light actual candles from a candlelit dinner and ring them with a cluster of your dinosaur stickers, so the hush and the kid joy share one object first."
}
task: core - a bit of everything - scattered
Input:
{
"task": "core",
"loves": "1. spreadsheets that balance\n2. aquarium gift shops\n3. chess on Sundays\n4. karaoke nights\n5. my hot sauce collection\n6. ... (199 characters)",
"nots": "",
"colors": "",
"page_board": "{\"lines_read\":9,\"lines_recognised\":9,\"leading\":[{\"language\":\"blueprint\",\"share_pct\":11,\"lines\":1},{\"language\":\"trinket\", ... (568 characters)",
"page_verdict": "scattered"
}
Reply:
{
"task": "core",
"verdict": "scattered",
"title": "A Case File for Quiet Obsessions",
"core_name": "True Crime core",
"tagline": "Grid lines, manila folders, and one red thread pulled taut.",
"summary": "These nine loves keep circling back to careful cataloguing: a spreadsheet that has to balance, a documentary that labels every bone, a podcast that works a case by its timestamps. This core turns that habit into an evidence board crossed with graph paper - ruled grids and manila folders, with one red thread pulled taut across the middle. The aquarium gift shop, chess Sundays, karaoke nights, the hot sauce shelf, skate videos and the foggy harbour are still pinned at the edges, pulling toward other moods this particular board doesn't try to hold.",
"languages": [
{
"id": "surveillance",
"weight": 3,
"why": "The true-crime podcasts bring the timestamps, case files and watchful quiet this core is named for."
},
{
"id": "blueprint",
"weight": 2,
"why": "The balanced spreadsheets and the labeled dinosaur-bone diagrams add the ruled grids and exploded diagrams that back the case."
}
],
"palette": [
{
"hex": "#f3ead3",
"name": "Manila Folder",
"role": "paper"
},
{
"hex": "#1c1f1a",
"name": "Ledger Ink",
"role": "ink"
},
{
"hex": "#355c7a",
"name": "Blueprint Steel",
"role": "accent"
},
{
"hex": "#c0392b",
"name": "Red String",
"role": "pop"
},
{
"hex": "#10141c",
"name": "Stakeout Shadow",
"role": "shadow"
}
],
"type": {
"display": "a confident slab-serif, stamped like a case label",
"text": "a plain monospace, like typewritten field notes",
"treatment": "Headlines sit boxed in ruled blueprint lines while body copy runs loose and typewritten, as if clipped straight from a case file."
},
"motifs": [
{
"from": "spreadsheets that balance",
"motif": "neat grid columns balancing to zero",
"language": "blueprint"
},
{
"from": "aquarium gift shops",
"motif": "a gift-shop shelf of glossy glass bubbles",
"language": "frutiger_aero"
},
{
"from": "chess on Sundays",
"motif": "chess knights cast in gilded heraldry",
"language": "future_medieval"
},
"..."
],
"textures": [
"ruled grid-paper lines",
"manila folder grain",
"..."
],
"we_are": [
"methodical",
"curious",
"quietly obsessive"
],
"we_are_not": [
"glossy",
"careless",
"loud"
],
"avoid": [
{
"id": "naive",
"why": "Crayon-bright wobbly lines would break the deadpan, evidence-board calm this core keeps."
},
{
"id": "kidcore",
"why": "Construction-paper brights clash with the hushed, timestamped mood of a stakeout."
},
"..."
],
"tension": "The seed is three lines of careful record-keeping - \"spreadsheets that balance,\" \"dinosaur documentaries\" and \"true crime podcasts\" - whose shared love of grids, labels and timestamps built this whole board. The rest are still yours, just pulling toward other cores: the aquarium gift shop wants a glassy, bubble-glossed shelf; chess on Sundays wants gilded heraldic knights; karaoke nights want a chrome, glowing broadcast; the hot sauce collection wants its own museum-drawer shelf; skate videos want torn stickers and tape; and the foggy harbour wants a soft, grainy hush.",
"next": "First rule a thin blue grid across a manila folder and pin one red thread straight down the middle."
}
task: apply - a shelf - go
Input:
{
"task": "apply",
"loves": "1. ballet flats with ribbons\n2. rainy afternoons by the window\n3. my grandmother's button tin\n4. old film photos with li ... (220 characters)",
"core": "{\"name\":\"Rainlight core\",\"tagline\":\"Small kept things, softened by rain, film grain and slow tea mornings\",\"languages\":[ ... (860 characters)",
"thing": "a shelf or a corner",
"occasion": "The narrow shelf above my desk - I see it every day while I work.",
"owned": "1. white ceramic bud vase\n2. stack of old paperbacks\n3. pink satin ribbon\n4. framed film photo of my mum\n5. glass jar of ... (154 characters)",
"page_items": "[\"1. white ceramic bud vase -> keep (white is close to Tea-Stained Page #f2e9dc)\",\"2. stack of old paperbacks -> unread\" ... (416 characters)",
"page_verdict": "go"
}
Reply:
{
"task": "apply",
"verdict": "go",
"title": "A Shelf Built From Kept Things",
"summary": "Nearly everything on this shelf already lives in Rainlight core - the vase, the jar of shells, the photo and the lavender all read as quiet, kept objects. Only the ribbon needs a little softening so its shine doesn't fight the mood.",
"premise": "This narrow shelf is a small museum drawer of what you already love, softened like a photograph left out in the rain.",
"items": [
{
"item": "white ceramic bud vase",
"call": "keep",
"how": "Its plain white sits warmly against Tea-Stained Page, so place it bare and let it hold the lavender stem."
},
{
"item": "stack of old paperbacks",
"call": "keep",
"how": "Lay them flat as a low base for the shelf - old paperbacks already carry the kept, unhurried mood this core wants."
},
{
"item": "pink satin ribbon",
"call": "tweak",
"how": "Skip the crisp bow - drape it loosely over the vase's neck so the satin's shine softens into a faint glow of Ribbon Pink instead of gleaming."
},
"..."
],
"look": {
"composition": "A low line of paperbacks anchors the shelf, the framed photo leaning at one end and the jar of shells at the other, with the vase and ribbon gathered just off-center so the eye has somewhere soft to land.",
"light": "Keep it soft and diffuse - no direct sun or overhead glare - so the photo's grain and the ribbon's sheen both stay gentle instead of sharp.",
"palette_use": "Tea-Stained Page (#f2e9dc) is the bare background space behind the objects; Film-Leak Ink (#2b241d) shows up in the paperback spines and the photo frame's edge; Pressed Sage (#9caf88) is the lavender; Ribbon Pink (#c98a92) is the ribbon itself; Rainy Windowpane (#6b5d52) falls as the soft shadow each object casts along the shelf.",
"texture": "Worn paper edges, a little glass fog on the jar, soft satin instead of stiff, and the photo's own film grain holding it all together."
},
"steps": [
"Clear the shelf and lay the paperbacks flat first, as a low base.",
"Lean the framed photo of your mum against the back, off to one side.",
"..."
],
"add": [
"a pressed flower slipped between two of the paperbacks, just peeking out",
"a single loose button from the tin set beside the vase"
],
"shots": [
"Straight-on at eye level, the whole shelf in soft, even light.",
"..."
],
"avoid": [
"bright overhead light or flash that flattens the grain",
"a tight, shiny bow in the ribbon",
"..."
]
}
task: apply - a birthday table - tweak
Input:
{
"task": "apply",
"loves": "1. gothic cathedrals\n2. tarot cards\n3. candlelit dinners\n4. stained glass windows\n5. velvet\n6. dinosaur stickers\n7. rain ... (174 characters)",
"core": "{\"name\":\"Velvet Dinosaur core\",\"tagline\":\"Stained glass and tarot decks share a shelf with dinosaur stickers and bright ... (861 characters)",
"thing": "a party or dinner table",
"occasion": "My 30th birthday dinner for eight friends at home.",
"owned": "1. black taper candlesticks\n2. rainbow paper plates\n3. gold-rimmed wine glasses\n4. inflatable dinosaur balloons\n5. plain ... (224 characters)",
"page_items": "[\"1. black taper candlesticks -> keep (black is close to Cathedral Dark #140b12)\",\"2. rainbow paper plates -> keep (read ... (583 characters)",
"page_verdict": "tweak"
}
Reply:
{
"task": "apply",
"verdict": "tweak",
"title": "A Candlelit, Dinosaur-Bright Birthday Table",
"summary": "This table pairs cathedral hush with birthday brightness for eight guests: taper candles, a gold rim and a velvet runner carry the reverent half of the core, while dinosaur balloons and a little rainbow carry the playful half. Two pieces belong to a different mood altogether and sit this one out, while the plain cloth and the full rainbow plates just need softening so the two sides meet instead of fight.",
"premise": "A table set like a small candlelit chapel for a birthday, where gothic hush meets a grinning dinosaur and a scribble of crayon color.",
"items": [
{
"item": "black taper candlesticks",
"call": "keep",
"how": "Keep these as the spine of the table, running candlelight down its length the way light runs down the center of a cathedral."
},
{
"item": "rainbow paper plates",
"call": "tweak",
"how": "Turn them face-down so only a rainbow sliver shows at the rim, instead of the full spread of color taking over the gold-and-velvet palette."
},
{
"item": "gold-rimmed wine glasses",
"call": "keep",
"how": "Keep one at every place - that gold rim is the same gleam the whole table is built on."
},
"..."
],
"look": {
"composition": "The candlesticks and velvet runner run down the center like a long spine, with the eight place settings flanking either side and the dinosaur balloons kept low and close rather than floating above everything.",
"light": "Low and flickering - candlelight wherever possible - so every gold rim catches a gleam and the plain cloth and any leftover brightness recede into the dark.",
"palette_use": "#140b12 is the dim room and the sliver of tablecloth still visible at the edges, kept mostly in shadow; #fdf1c7 is the warm pool of candlelight spreading across the plates and cloth; #7a1f3d is the velvet runner down the center; #ffe135 flashes out of the dinosaur balloons and whatever rainbow edge is still allowed to peek through the plates; #7c531a is the gold rim of every wine glass and the glint at the base of each candlestick.",
"texture": "Soft velvet against hard glass and wax, the slight crinkle of balloon and paper plate, candle drips pooling as the night goes on."
},
"steps": [
"Dim the overhead lights and set the taper candlesticks down the center of the table.",
"Lay the burgundy velvet runner over the tablecloth under the candlesticks, then set a gold-rimmed wine glass at each of the eight places.",
"..."
],
"add": [
"hand-made tarot-style place cards for the eight guests, one per seat",
"a few amber or cream glass votives standing in for a little stained-glass glow",
"a handful of rainbow crayons set at each place as a favor or name marker"
],
"shots": [
"A long shot down the table from one end, framing the candlesticks and velvet runner leading toward the dinosaur balloons, lit only by candle flame.",
"..."
],
"avoid": [
"bright, flat overhead lighting that kills the candle glow",
"neon or screen-bright green tones anywhere on the table",
"..."
]
}
task: apply - a museum-date outfit - rethink
Input:
{
"task": "apply",
"loves": "1. spreadsheets that balance\n2. aquarium gift shops\n3. chess on Sundays\n4. karaoke nights\n5. my hot sauce collection\n6. ... (199 characters)",
"core": "{\"name\":\"True Crime core\",\"tagline\":\"Grid lines, manila folders, and one red thread pulled taut.\",\"languages\":[\"surveill ... (940 characters)",
"thing": "an outfit",
"occasion": "A quiet first date at a small art museum on a Saturday afternoon.",
"owned": "1. neon green windbreaker\n2. glitter platform sneakers\n3. flame-print bowling shirt\n4. chrome visor sunglasses\n5. novelt ... (159 characters)",
"page_items": "[\"1. neon green windbreaker -> drop (neon green is far from Manila Folder #f3ead3)\",\"2. glitter platform sneakers -> unr ... (356 characters)",
"page_verdict": "tweak"
}
Reply:
{
"task": "apply",
"verdict": "rethink",
"title": "A Case File Walks Into a Museum",
"summary": "Three of the six pieces on hand - the windbreaker, the sneakers, the bucket hat - are bright, glittery, or novelty, which is exactly the loud, glossy, careless territory this quietly obsessive core is built against, and a fourth, the flame-print shirt, only survives with a change. The chrome visor and the black jeans already fit; build the rest around that same dark, manila-and-ledger quiet, with a soft grey-blue layer standing in for the windbreaker and a single red accent doing the work the platforms and the bucket hat can't.",
"premise": "The story here is someone quietly building a case: a dark, even base with one red thread of colour, nothing bright enough to interrupt the hush of the room.",
"items": [
{
"item": "neon green windbreaker",
"call": "drop",
"how": "Neon brights are loud and glossy in a core that says neither, and they'd clash hard against the manila and ledger-ink hush of the room."
},
{
"item": "glitter platform sneakers",
"call": "drop",
"how": "Glitter platform soles throw light and sparkle into a quiet, methodical core and a hushed first date, so they read as the wrong kind of shine."
},
{
"item": "flame-print bowling shirt",
"call": "tweak",
"how": "Worn open over a plain layer with the sleeves rolled, the flame print turns into a flash of texture at the edges instead of the main statement."
},
"..."
],
"look": {
"composition": "Keep it narrow and vertical: a dark base, one quiet layer on top, the visor as the single glint, so the eye has nowhere loud to land.",
"light": "Soft, even gallery light rather than anything flashing or neon, so the dark tones hold their shape and the one warm accent still reads clearly.",
"palette_use": "#1c1f1a carries the base in the black jeans, #f3ead3 shows up in a top kept close to the face, #355c7a sits in a quiet outer layer or accessory, #c0392b is saved for one small accent like a cord or a pin, and #10141c stays low, in shoes or a bag, never spreading past the ground.",
"texture": "Matte and flat everywhere except the one glint off the visor, so that single shine reads like a photo under glass rather than glitter."
},
"steps": [
"Start from the black jeans as the dark, ledger-ink base.",
"Add a plain top in a manila or cream tone, buttoned high and kept simple near the face.",
"..."
],
"add": [
"a plain manila-toned top buttoned high",
"a soft steel-grey outer layer with the hush of foggy harbours, standing in for the windbreaker",
"a slim red cord or pin to carry the one red-string accent"
],
"shots": [
"A close three-quarter shot at the door, soft window light catching a glint off the chrome visor.",
"..."
],
"avoid": [
"neon brights",
"glitter or sequins",
"..."
]
}
Build the input yourself
Load the script in node (global.window = {}; require("./corekit.js")), then:
const K = CoreKit;
const form = { loves: "ballet flats with ribbons\nrainy afternoons by the window\nmy grandmother's button tin", nots: "anything neon", colors: "dusty pink, cream" };
const st = K.state(form); // the reading, the clash check, a local palette, the page's core
console.log(st.R.verdict, st.R.top.map(x => x.name + " " + x.share + "%"), st.P.palette.map(p => p.hex));
const wire = K.toWire(K.buildInput(st, "core").input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output), "core");
// style one thing: form.core = JSON.stringify(K.coreFromReply(reply)); form.thing = "shelf";
// form.owned = "white ceramic bud vase\npink satin ribbon"; K.toWire(K.buildInput(K.state(form), "apply").input)
// const { notes } = K.check(applyReply, K.state(form), "apply");
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://core-collage.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://core-collage.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// core-collage.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://core-collage.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://core-collage.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// CoreCollage.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class CoreCollage {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://core-collage.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://core-collage.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://core-collage.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://core-collage.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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":"core-collage"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "core-collage"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "core-collage" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "core-collage"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "core-collage"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "core-collage" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "core-collage"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "core-collage" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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": "core", "loves": "1. ballet flats with ribbons\n2. rainy afternoons by the window\n...", "nots": "anything neon", "colors": "dusty pink, cream, sage", "page_board": "{...}", "page_verdict": "cohesive"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "core", "loves": "1. ballet flats with ribbons\n2. rainy afternoons by the window\n...", "nots": "anything neon", "colors": "dusty pink, cream, sage", "page_board": "{...}", "page_verdict": "cohesive"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "core" | "apply", loves, nots, colors, page_board, page_verdict, ... (see the field table)}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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 core-collage:<task>:<inputHash>:a<attempt> (the examples below use the core 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="core-collage:core:$(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}'
import hashlib, time
key = "core-collage:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `core-collage:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "core-collage:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "core-collage:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "core-collage:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "core-collage:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"core-collage:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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: core-collage:core:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "core", "loves": "1. ballet flats with ribbons\n2. rainy afternoons by the window\n...", "nots": "anything neon", "colors": "dusty pink, cream, sage", "page_board": "{...}", "page_verdict": "cohesive"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "core-collage:core:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `core-collage:core:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "core-collage:core:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "core-collage:core:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "core-collage:core:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: core-collage:core:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"core-collage:core:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);