Run Launch Proof from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads what the site serves (the response headers, the HTML head, robots.txt as crawlers read it, the sitemap, llms.txt and the manifest), runs every check of the skill's phases 4 to 7, and settles the AI-crawler policy for the site type, then hands the rows to the run as facts. The run writes the report or the files; the page then holds it to its own rows (launchreply.js): every fail row a blocker and every warn row a fix, each phase's status following from them, blockers ordered before fixes; titles of 50-60 and descriptions of 150-160 characters, llms.txt in its shape, Organization and WebSite JSON-LD, and no URL the site does not have.
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"launch-proof"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, site, served, facts) is missing. The page always sends all five of task, site, served, carry, facts; carry is an empty string for report. |
| 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 sign-in. |
| 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
Two lanes, chosen by task, in the order a launch runs - the verdict, then the fix:
report- the launch report: phases 4 to 7 (security, SEO and GEO, social, favicons), each with its status, every blocker and every recommended fix naming the exact header, file or tag, the checks to do by hand, and the order to do them in.files- the files that need words: llms.txt, a title and a description per page at the right length, the social card text and the JSON-LD for the home page.
A missing or unknown task is answered as report. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the report's blockers and fixes into carry for the files lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | report or files. If it is missing or neither, do report and set "task": "report". |
site | yes | the site's address, its type, the host, the CSP level, the languages, the brand, what the site is, the social handle and other pages; "not given" means not given. |
served | yes | what the site serves, as pasted: the response headers, the HTML head, robots.txt, sitemap.xml, llms.txt and manifest.json, each under its own heading; "(not pasted)" means not pasted. |
carry | send it (empty for report) | files task only: the report's blockers and fixes, when the report ran first; may be empty. |
facts | yes | the page's checks, one line each as phase N | id | status | evidence, the AI-crawler policy, and for the files task the URLs that exist and the pages to write for. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/site-launch-checklist - the only standard the run applies:
- Security headers: a Content-Security-Policy at the chosen tightness with no 'unsafe-inline' for scripts (nonces instead); HSTS max-age=31536000 with includeSubDomains and preload; X-Frame-Options DENY; X-Content-Type-Options nosniff; Referrer-Policy strict-origin-when-cross-origin; a Permissions-Policy that denies camera, microphone, geolocation and payment unless used. No secrets in the client.
- SEO and GEO: robots.txt present and pointing at the sitemap; a valid sitemap (per-language when multilingual); llms.txt per llmstxt.org; the AI-crawler policy for the site type encoded in robots.txt and confirmed, because it ships in a public file; JSON-LD with Organization and WebSite site-wide and per-page types where they apply (SoftwareApplication for a library or app, Article for a post, FAQPage for an FAQ, Person for an author); a unique title of 50-60 characters and a unique description of 150-160 characters on every page; a canonical link; hreflang on every page of a multilingual site, each declaring all alternates including itself.
- Open Graph: og:title, og:description, og:url, og:type, og:site_name; an absolute 1200 by 630 og:image with width, height and alt declared, per page rather than one global; og:locale for each language; twitter:card summary_large_image with title, description, image and the site handle.
- Favicons: favicon.ico, a favicon.svg that follows dark mode, a PNG fallback, a 180 by 180 opaque apple-touch-icon, 192 and 512 manifest icons, and a manifest with name, short_name, display, theme_color and background_color. Skip the deprecated mstile and safari-pinned-tab.
- End with a status report grouped by phase, with blockers, recommended fixes and optional improvements clearly separated.
House rules
- Use only the input. Never invent a URL, an origin, a page, a social handle, a score, a tool result, a customer, a number or a claim about the product. A URL you write appears in the facts' list of URLs that exist, or is one of the public checking tools the skill names (securityheaders.com, observatory.mozilla.org, hstspreload.org, the Rich Results Test, opengraph.xyz).
- The page's statuses are settled: never call a failed check passed or a passed check failed.
- Write in the site's language; keep the JSON keys in English.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
report
Write the launch report, grouped by phase.
summary- one sentence, at most 40 words: whether the site can launch and the one thing to fix first.phases- exactly four objects, phases 4, 5, 6 and 7 in order:phase(the number),status(blockedwhen the phase has a fail row,fixeswhen it has warn rows and no fail,cleanwhen every row passes,uncheckedwhen every row is n/a),blockers(one object per fail row of the phase:id- the row's id exactly;fix- one or two sentences naming the exact header, file or tag and its value),fixes(the same for every warn row),manual(the checks the page cannot run for this phase, as short strings).order- every id once: first all the blockers, across phases 4 to 7, then all the fixes; within each group, the order to do them (the most damaging first).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "report",
"summary": "one sentence",
"phases": [ { "phase": 4, "status": "blocked", "blockers": [ { "id": "csp", "fix": "one or two sentences" } ], "fixes": [ { "id": "hsts", "fix": "one or two sentences" } ], "manual": [ "a check" ] } ],
"order": [ "csp", "hsts" ],
"caveats": [ ]
}
The page holds it to its own rows (check() in launchreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL outside the site's own and the named checking tools; exactly phases 4, 5, 6 and 7 in order; in each phase the blockers are exactly the fail rows and the fixes exactly the warn rows, in the facts' order; each phase's status follows from them (blocked, fixes, clean, unchecked); every blocker and fix has its fix; the order lists every blocker and fix once, blockers first. A phase with no manual check is a note that does not flag.
files
Write the files that need words: llms.txt, the title and description of each page, the social preview text and the JSON-LD.
summary- one sentence, at most 40 words.llms_txt- the whole llms.txt: an H1 with the site name, a one-paragraph summary as a "> " blockquote, then "## " sections listing pages as "- [Name](URL): what it is", using only URLs that exist.meta- one object per page the facts list, in that order:path,title(50-60 characters),description(150-160 characters). Count the characters.og-title(at most 60 characters),description(at most 160 characters) andimage_altfor the home page's social card.jsonld- a list of JSON-LD objects for the home page, each with "@context": "https://schema.org": an Organization (or a Person for a personal site) and a WebSite, plus a SoftwareApplication when the site type is a SaaS app or a doc site for a library. Use only URLs that exist; add sameAs only for a social handle the site gives.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "files",
"summary": "one sentence",
"llms_txt": "# Name\n\n> One paragraph.\n\n## Docs\n\n- [Page](https://example.com/page): what it is",
"meta": [ { "path": "/", "title": "50-60 characters", "description": "150-160 characters" } ],
"og": { "title": "a title", "description": "a description", "image_alt": "what the image shows" },
"jsonld": [ { "@context": "https://schema.org", "@type": "Organization", "name": "Name", "url": "https://example.com" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; no URL the site does not have; llms.txt with an H1, a summary blockquote, a section and at least one link; one metadata entry per page the facts list, in that order, each title 50-60 characters and each description 150-160; a social card with a title, a description and the image's alt text; JSON-LD objects with a schema.org @context, an Organization or a Person and a WebSite, plus a SoftwareApplication for a SaaS app or a doc site; and no sameAs when the site gives no social handle. A relative link in llms.txt is a note that does not flag.
Worked examples
Three runs from the page's free samples (invented sites on the reserved .example domain): the report for a library's doc site, the files for that site carrying the report's to-do, and the report for a product's marketing site with a staging noindex left in. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the served text, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
report - A library's doc site
The page read it as a doc site or library homepage: launch blockers (4 fail, 5 warn, 16 pass). Fail rows: csp, ai, llms, twitter. Warn rows: hsts, jsonld, title, ogimage, manifest. AI crawlers: a doc site wants to be cited: allow the AI crawlers, block the low-value scrapers.
Input (served is 2957 characters; the first 1395 are shown; facts is 2871 characters; the first 1394 are shown):
{
"task": "report",
"site": "Site: https://quarlo.example\nSite type: Doc site or library homepage\nHost: Cloudflare Pages (_headers)\nCSP level: strict-default-src-none\nLanguages: single locale (en)\nBrand: Quarlo\nWhat the site is: Quarlo is an open-source TypeScript library for typed SQL query building; the site holds the guide, the API reference and the changelog.\nTwitter / X handle: @quarlolib\nOther pages:\n- /guide/getting-started | Getting started | Install Quarlo and write your first typed query.\n- /api | API reference",
"served": "=== Response headers ===\nHTTP/2 200\ncontent-type: text/html; charset=utf-8\nserver: cloudflare\ncontent-security-policy: default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'\nstrict-transport-security: max-age=31536000; includeSubDomains\nx-content-type-options: nosniff\nx-frame-options: DENY\nreferrer-policy: strict-origin-when-cross-origin\npermissions-policy: camera=(), microphone=(), geolocation=(), payment=()\n\n=== HTML head ===\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Quarlo - typed SQL</title>\n<meta name=\"description\" content=\"Quarlo is a TypeScript query builder that checks every SQL query against your schema at compile time, so a renamed column fails the build, not production.\">\n<link rel=\"canonical\" href=\"https://quarlo.example/\">\n<meta property=\"og:title\" content=\"Quarlo - typed SQL for TypeScript\">\n<meta property=\"og:description\" content=\"Every SQL query checked against your schema at compile time.\">\n<meta property=\"og:url\" content=\"https://quarlo.example/\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:site_name\" content=\"Quarlo\">\n<meta property=\"og:image\" content=\"https://quarlo.example/og.png\">\n<meta name=\"twitter:card\" content=\"summary\">\n<meta name=\"twitter:site\" content=\"@quarlolib\">\n\n[... 1562 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (4 fail, 5 warn, 16 pass, 0 not checkable from what was pasted):\n- phase 4 | csp | fail | 'unsafe-inline' in script-src - use nonces\n- phase 4 | hsts | warn | \"max-age=31536000; includeSubDomains\" - no preload\n- phase 4 | xfo | pass | \"DENY\"\n- phase 4 | nosniff | pass | \"nosniff\"\n- phase 4 | referrer | pass | \"strict-origin-when-cross-origin\"\n- phase 4 | permissions | pass | denies all four\n- phase 4 | fingerprint | pass | none\n- phase 4 | secrets | pass | none in what was pasted (check the JS bundle too)\n- phase 5 | robots | pass | Sitemap: https://quarlo.example/sitemap.xml\n- phase 5 | indexable | pass | Googlebot allowed (via the * group)\n- phase 5 | ai | fail | GPTBot is blocked via its own group, policy says allow; Bytespider is allowed via the * group, policy says block; Amazonbot is allowed via the * group, policy says block\n- phase 5 | sitemap | pass | 4 URLs, 4 lastmod\n- phase 5 | llms | fail | not pasted - paste /llms.txt\n- phase 5 | jsonld | warn | types: SoftwareApplication - add Organization and WebSite\n- phase 5 | title | warn | 18 characters: \"Quarlo - typed SQL\"\n- phase 5 | description | pass | 154 characters\n- phase 5 | canonical | pass | https://quarlo.example/\n- phase 5 | noindex | pass | indexable\n- phase 6 | og | pass | all five\n- phase 6 | ogimage | warn | https://quarlo.example/og.png - no width/height - no og:image:alt\n- phase 6 |\n\n[... 1477 more characters cut from this display ...]"
}
Reply:
{
"task": "report",
"summary": "Not ready to launch: four blockers remain, and the first to fix is the Content-Security-Policy, whose script-src still allows 'unsafe-inline' for the inline theme script.",
"phases": [
{
"phase": 4,
"status": "blocked",
"blockers": [
{
"id": "csp",
"fix": "Move the inline theme script in the head into a file under /assets/ (or give it a nonce from a Pages Function), then drop 'unsafe-inline' so _headers sends script-src 'self' with the rest of the default-src 'none' policy unchanged."
}
],
"fixes": [
{
"id": "hsts",
"fix": "Change the Strict-Transport-Security line in _headers to max-age=31536000; includeSubDomains; preload, then submit quarlo.example at hstspreload.org."
}
],
"manual": [
"securityheaders.com grade A+",
"observatory.mozilla.org score 90 or more",
"No secrets in the JS bundle (search the Network tab for sk_, pk_, AKIA, ghp_, Bearer)",
"HSTS submitted to hstspreload.org"
]
},
{
"phase": 5,
"status": "blocked",
"blockers": [
{
"id": "ai",
"fix": "In robots.txt delete the GPTBot group that sends Disallow: /, so GPTBot falls back to the * group's Allow: /, and add a group with User-agent: Bytespider and User-agent: Amazonbot followed by Disallow: /."
},
{
"id": "llms",
"fix": "Publish /llms.txt with an H1 of Quarlo, a one-paragraph > summary and a ## Docs section linking the getting-started guide, the API reference and the changelog, then paste it here to re-check."
}
],
"fixes": [
{
"id": "jsonld",
"fix": "Keep the SoftwareApplication block and add an Organization (name Quarlo, url https://quarlo.example/) and a WebSite (name Quarlo, url https://quarlo.example/) as JSON-LD in the head."
},
{
"id": "title",
"fix": "The title tag is 18 characters; lengthen it to 50-60, for example <title>Quarlo - type-safe SQL query builder for TypeScript</title> (51 characters)."
}
],
"manual": [
"Google Rich Results Test on the JSON-LD",
"Keyword shortlist per page (Google Trends for direction, Ahrefs for size)",
"AI visibility check of how the site appears in AI answers",
"Typo and grammar pass",
"Every important page within three clicks of the home page"
]
},
{
"phase": 6,
"status": "blocked",
"blockers": [
{
"id": "twitter",
"fix": "Change twitter:card from summary to summary_large_image and add twitter:title, twitter:description and twitter:image pointing at https://quarlo.example/og.png, keeping twitter:site @quarlolib."
}
],
"fixes": [
{
"id": "ogimage",
"fix": "Declare og:image:width 1200 and og:image:height 630 next to og:image (after confirming og.png is 1200 by 630), and add an og:image:alt that describes what the image shows."
}
],
"manual": [
"opengraph.xyz preview (Facebook, LinkedIn, Slack, Discord, WhatsApp)",
"Paste the URL in a Slack channel and a LinkedIn DM"
]
},
{
"phase": 7,
"status": "fixes",
"blockers": [],
"fixes": [
{
"id": "manifest",
"fix": "Add \"short_name\": \"Quarlo\" to /site.webmanifest; name, display, theme_color, background_color and the 192 and 512 icons are already there."
}
],
"manual": [
"/favicon.ico served as multi-resolution 16/32/48",
"apple-touch-icon is 180x180 with an opaque background"
]
}
],
"order": [
"csp",
"ai",
"llms",
"twitter",
"hsts",
"title",
"jsonld",
"ogimage",
"manifest"
],
"caveats": [
"llms.txt was not pasted; if it already exists, paste it and the llms row re-checks.",
"favicon.ico is not linked in the head; confirm /favicon.ico is served."
]
}
What step 5 prints for it: 1. csp | 2. ai | 3. llms | 4. twitter | 5. hsts | 6. title | 7. jsonld | 8. ogimage | 9. manifest
The page's check: kept - nothing to flag
files - A library's doc site
The input carries the saved report's blockers and fixes (R.laneForm fills carry from the report reply), as the page's handoff does.
Input (served is 2957 characters; the first 1395 are shown; facts is 2520 characters; the first 1394 are shown):
{
"task": "files",
"site": "Site: https://quarlo.example\nSite type: Doc site or library homepage\nHost: Cloudflare Pages (_headers)\nCSP level: strict-default-src-none\nLanguages: single locale (en)\nBrand: Quarlo\nWhat the site is: Quarlo is an open-source TypeScript library for typed SQL query building; the site holds the guide, the API reference and the changelog.\nTwitter / X handle: @quarlolib\nOther pages:\n- /guide/getting-started | Getting started | Install Quarlo and write your first typed query.\n- /api | API reference",
"served": "=== Response headers ===\nHTTP/2 200\ncontent-type: text/html; charset=utf-8\nserver: cloudflare\ncontent-security-policy: default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'\nstrict-transport-security: max-age=31536000; includeSubDomains\nx-content-type-options: nosniff\nx-frame-options: DENY\nreferrer-policy: strict-origin-when-cross-origin\npermissions-policy: camera=(), microphone=(), geolocation=(), payment=()\n\n=== HTML head ===\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Quarlo - typed SQL</title>\n<meta name=\"description\" content=\"Quarlo is a TypeScript query builder that checks every SQL query against your schema at compile time, so a renamed column fails the build, not production.\">\n<link rel=\"canonical\" href=\"https://quarlo.example/\">\n<meta property=\"og:title\" content=\"Quarlo - typed SQL for TypeScript\">\n<meta property=\"og:description\" content=\"Every SQL query checked against your schema at compile time.\">\n<meta property=\"og:url\" content=\"https://quarlo.example/\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:site_name\" content=\"Quarlo\">\n<meta property=\"og:image\" content=\"https://quarlo.example/og.png\">\n<meta name=\"twitter:card\" content=\"summary\">\n<meta name=\"twitter:site\" content=\"@quarlolib\">\n\n[... 1562 more characters cut from this display ...]",
"carry": "blocker (phase 4) csp: Move the inline theme script in the head into a file under /assets/ (or give it a nonce from a Pages Function), then drop 'unsafe-inline' so _headers sends script-src 'self' with the rest of the default-src 'none' policy unchanged.\nblocker (phase 5) ai: In robots.txt delete the GPTBot group that sends Disallow: /, so GPTBot falls back to the * group's Allow: /, and add a group with User-agent: Bytespider and User-agent: Amazonbot followed by Disallow: /.\nblocker (phase 5) llms: Publish /llms.txt with an H1 of Quarlo, a one-paragraph > summary and a ## Docs section linking the getting-started guide, the API reference and the changelog, then paste it here to re-check.\nblocker (phase 6) twitter: Change twitter:card from summary to summary_large_image and add twitter:title, twitter:description and twitter:image pointing at https://quarlo.example/og.png, keeping twitter:site @quarlolib.\nfix (phase 4) hsts: Change the Strict-Transport-Security line in _headers to max-age=31536000; includeSubDomains; preload, then submit quarlo.example at hstspreload.org.\nfix (phase 5) jsonld: Keep the SoftwareApplication block and add an Organization (name Quarlo, url https://quarlo.example/) and a WebSite (name Quarlo, url https://quarlo.example/) as JSON-LD in the head.\nfix (phase 5) title: The title tag is 18 characters; lengthen it to 50-60, for example <title>Quarlo - type-safe SQL query builder for TypeScript</title> (51 characters).\nfix (phase 6) ogimage: Declare og:image:width 1200 and og:image:height 630 next to og:image (after confirming og.png is 1200 by 630), and add an og:image:alt that describes what the image shows.\nfix (phase 7) manifest: Add \"short_name\": \"Quarlo\" to /site.webmanifest; name, display, theme_color, background_color and the 192 and 512 icons are already there.",
"facts": "The page's checks (4 fail, 5 warn, 16 pass, 0 not checkable from what was pasted):\n- phase 4 | csp | fail | 'unsafe-inline' in script-src - use nonces\n- phase 4 | hsts | warn | \"max-age=31536000; includeSubDomains\" - no preload\n- phase 4 | xfo | pass | \"DENY\"\n- phase 4 | nosniff | pass | \"nosniff\"\n- phase 4 | referrer | pass | \"strict-origin-when-cross-origin\"\n- phase 4 | permissions | pass | denies all four\n- phase 4 | fingerprint | pass | none\n- phase 4 | secrets | pass | none in what was pasted (check the JS bundle too)\n- phase 5 | robots | pass | Sitemap: https://quarlo.example/sitemap.xml\n- phase 5 | indexable | pass | Googlebot allowed (via the * group)\n- phase 5 | ai | fail | GPTBot is blocked via its own group, policy says allow; Bytespider is allowed via the * group, policy says block; Amazonbot is allowed via the * group, policy says block\n- phase 5 | sitemap | pass | 4 URLs, 4 lastmod\n- phase 5 | llms | fail | not pasted - paste /llms.txt\n- phase 5 | jsonld | warn | types: SoftwareApplication - add Organization and WebSite\n- phase 5 | title | warn | 18 characters: \"Quarlo - typed SQL\"\n- phase 5 | description | pass | 154 characters\n- phase 5 | canonical | pass | https://quarlo.example/\n- phase 5 | noindex | pass | indexable\n- phase 6 | og | pass | all five\n- phase 6 | ogimage | warn | https://quarlo.example/og.png - no width/height - no og:image:alt\n- phase 6 |\n\n[... 1126 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "files",
"summary": "The words Quarlo's launch still needs: an llms.txt for the docs, titles and descriptions for the three pages, the social card text and Organization, WebSite and SoftwareApplication JSON-LD.",
"llms_txt": "# Quarlo\n\n> Quarlo is an open-source TypeScript library for typed SQL query building: it checks every SQL query against your schema at compile time, so a renamed column fails the build, not production.\n\n## Docs\n\n- [Getting [... 276 more characters cut from this display]",
"meta": [
{
"path": "/",
"title": "Quarlo - type-safe SQL query builder for TypeScript",
"description": "Quarlo is a TypeScript query builder that checks every SQL query against your schema at compile time, so a renamed column fails the build, not production."
},
{
"path": "/guide/getting-started",
"title": "Getting started with Quarlo - your first typed SQL query",
"description": "Install Quarlo and write your first typed query with the TypeScript SQL builder that checks every query against your schema at compile time, not in production."
},
{
"path": "/api",
"title": "Quarlo API reference - the typed SQL builder for TypeScript",
"description": "The Quarlo API reference: the open-source TypeScript library for typed SQL query building, where every query is checked against your schema at compile time."
}
],
"og": {
"title": "Quarlo - typed SQL for TypeScript",
"description": "Every SQL query checked against your schema at compile time, so a renamed column fails the build, not production.",
"image_alt": "The Quarlo social card: typed SQL for TypeScript"
},
"jsonld": [
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Quarlo",
"url": "https://quarlo.example/",
"sameAs": [
"https://x.com/quarlolib"
]
},
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Quarlo",
"url": "https://quarlo.example/",
"inLanguage": "en"
},
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Quarlo",
"applicationCategory": "DeveloperApplication",
"url": "https://quarlo.example/",
"description": "An open-source TypeScript library for typed SQL query building that checks every query against your schema at compile time."
}
],
"caveats": [
"Match the image alt text to what og.png actually shows."
]
}
Cut from this display only (the saved reply and the check use the full text): llms_txt (498 characters, 222 shown).
What step 5 prints for it: / - Quarlo - type-safe SQL query builder for TypeScript (51) | /guide/getting-started - Getting started with Quarlo - your first typed SQL query (56) | /api - Quarlo API reference - the typed SQL builder for TypeScript (59)
The page's check: kept - nothing to flag
report - A product's marketing site
The page read it as a marketing / lead-gen: launch blockers (4 fail, 7 warn, 14 pass). Fail rows: csp, permissions, ai, noindex. Warn rows: xfo, referrer, fingerprint, sitemap, jsonld, manifest, deprecated. AI crawlers: the skill's decision matrix allows AI crawlers on a marketing site - citations on a sales page are good.
Input (served is 3262 characters; the first 1395 are shown; facts is 2974 characters; the first 1399 are shown):
{
"task": "report",
"site": "Site: https://www.brindlewyn.example\nSite type: Marketing / lead-gen\nHost: Vercel (vercel.json)\nCSP level: balanced-allow-self\nLanguages: single locale (en)\nBrand: Brindlewyn\nWhat the site is: Brindlewyn is shift-scheduling software for clinics; the marketing site has the product tour, pricing and a demo request form.\nTwitter / X handle: @brindlewynhq\nOther pages:\n- /pricing | Pricing - Brindlewyn\n- /demo | Book a demo",
"served": "=== Response headers ===\nHTTP/1.1 308 Permanent Redirect\nlocation: https://www.brindlewyn.example/\n\nHTTP/2 200\ncontent-type: text/html; charset=utf-8\nserver: Vercel\nx-powered-by: Next.js\ncontent-security-policy-report-only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; frame-ancestors 'none'\nstrict-transport-security: max-age=63072000; includeSubDomains; preload\nx-content-type-options: nosniff\nx-frame-options: SAMEORIGIN\nreferrer-policy: origin-when-cross-origin\n\n=== HTML head ===\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Brindlewyn - shift scheduling that clinics actually finish</title>\n<meta name=\"description\" content=\"Brindlewyn builds the clinic rota in minutes: availability, skills and labour rules in one place, with swaps that staff can approve from their phones.\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<link rel=\"canonical\" href=\"https://www.brindlewyn.example/\">\n<meta property=\"og:title\" content=\"Brindlewyn - shift scheduling for clinics\">\n<meta property=\"og:description\" content=\"Build the clinic rota in minutes, with swaps staff approve from their phones.\">\n<meta property=\"og:url\" content=\"https://www.brindlewyn.example/\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:site_name\" content=\"Brindlewyn\">\n<meta property=\"og:image\" content=\"https://www.brindlewyn.example/og/home.png\">\n<meta\n\n[... 1867 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (4 fail, 7 warn, 14 pass, 0 not checkable from what was pasted):\n- phase 4 | csp | fail | report-only - nothing is enforced yet\n- phase 4 | hsts | pass | \"max-age=63072000; includeSubDomains; preload\"\n- phase 4 | xfo | warn | \"SAMEORIGIN\"\n- phase 4 | nosniff | pass | \"nosniff\"\n- phase 4 | referrer | warn | \"origin-when-cross-origin\"\n- phase 4 | permissions | fail | missing\n- phase 4 | fingerprint | warn | X-Powered-By: Next.js\n- phase 4 | secrets | pass | none in what was pasted (check the JS bundle too)\n- phase 5 | robots | pass | Sitemap: https://www.brindlewyn.example/sitemap.xml\n- phase 5 | indexable | pass | Googlebot allowed (via the * group)\n- phase 5 | ai | fail | Bytespider is allowed via the * group, policy says block; Amazonbot is allowed via the * group, policy says block\n- phase 5 | sitemap | warn | 4 URLs, no lastmod; 1 on another origin, e.g. https://staging.brindlewyn.example/tour\n- phase 5 | llms | pass | H1 title, a summary blockquote, 1 ## section, 2 links\n- phase 5 | jsonld | warn | types: Organization - add WebSite\n- phase 5 | title | pass | 58 characters: \"Brindlewyn - shift scheduling that clinics actually finish\"\n- phase 5 | description | pass | 150 characters\n- phase 5 | canonical | pass | https://www.brindlewyn.example/\n- phase 5 | noindex | fail | meta robots \"noindex, nofollow\"\n- phase 6 | og | pass | all five\n- phase 6 | ogimage |\n\n[... 1575 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "report",
"summary": "Not ready to launch: the home page sends meta robots noindex, nofollow, so search engines will drop it - remove that tag before anything else.",
"phases": [
{
"phase": 4,
"status": "blocked",
"blockers": [
{
"id": "csp",
"fix": "The policy is sent as Content-Security-Policy-Report-Only, so nothing is enforced; once the reports are clean, send it from vercel.json as Content-Security-Policy, adding style-src 'self' fonts.googleapis.com for the Inter stylesheet the [... 12 more characters cut from this display]"
},
{
"id": "permissions",
"fix": "Add a Permissions-Policy header in vercel.json: camera=(), microphone=(), geolocation=(), payment=()."
}
],
"fixes": [
{
"id": "xfo",
"fix": "Change X-Frame-Options from SAMEORIGIN to DENY in vercel.json, matching the frame-ancestors 'none' already in the CSP."
},
{
"id": "referrer",
"fix": "Change Referrer-Policy from origin-when-cross-origin to strict-origin-when-cross-origin in vercel.json."
},
{
"id": "fingerprint",
"fix": "Stop sending X-Powered-By: Next.js by setting poweredByHeader: false in next.config.js."
}
],
"manual": [
"securityheaders.com grade A+",
"observatory.mozilla.org score 90 or more",
"No secrets in the JS bundle (search the Network tab for sk_, pk_, AKIA, ghp_, Bearer)",
"HSTS submitted to hstspreload.org"
]
},
{
"phase": 5,
"status": "blocked",
"blockers": [
{
"id": "ai",
"fix": "Add a robots.txt group with User-agent: Bytespider and User-agent: Amazonbot followed by Disallow: /; the AI crawlers you want cited stay allowed through the * group."
},
{
"id": "noindex",
"fix": "Remove <meta name=\"robots\" content=\"noindex, nofollow\"> from the production head - it hides every page that carries it - and confirm no X-Robots-Tag header replaces it."
}
],
"fixes": [
{
"id": "sitemap",
"fix": "Remove https://staging.brindlewyn.example/tour from sitemap.xml (list the production tour URL only if it exists) and give every url a lastmod date."
},
{
"id": "jsonld",
"fix": "Keep the Organization block and add a WebSite JSON-LD block with name Brindlewyn and url https://www.brindlewyn.example/."
}
],
"manual": [
"Google Rich Results Test on the JSON-LD",
"Keyword shortlist per page (Google Trends for direction, Ahrefs for size)",
"AI visibility check of how the site appears in AI answers",
"Typo and grammar pass",
"Every important page within three clicks of the home page"
]
},
{
"phase": 6,
"status": "clean",
"blockers": [],
"fixes": [],
"manual": [
"opengraph.xyz preview (Facebook, LinkedIn, Slack, Discord, WhatsApp)",
"Paste the URL in a Slack channel and a LinkedIn DM"
]
},
{
"phase": 7,
"status": "fixes",
"blockers": [],
"fixes": [
{
"id": "manifest",
"fix": "Paste /site.webmanifest so it can be checked for name, short_name, display, theme_color, background_color and the 192 and 512 icons."
},
{
"id": "deprecated",
"fix": "Delete <link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\">, <meta name=\"msapplication-TileColor\"> and <meta name=\"msapplication-TileImage\"> from the head."
}
],
"manual": [
"/favicon.ico served as multi-resolution 16/32/48",
"apple-touch-icon is 180x180 with an opaque background"
]
}
],
"order": [
"noindex",
"csp",
"permissions",
"ai",
"sitemap",
"referrer",
"xfo",
"jsonld",
"fingerprint",
"manifest",
"deprecated"
],
"caveats": [
"Check the browser console for CSP report-only violations from Google Tag Manager and Google Fonts before enforcing."
]
}
Cut from this display only (the saved reply and the check use the full text): phases[0].blockers[0].fix (249 characters, 237 shown).
What step 5 prints for it: 1. noindex | 2. csp | 3. permissions | 4. ai | 5. sitemap | 6. referrer | 7. xfo | 8. jsonld | 9. fingerprint | 10. manifest | 11. deprecated
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./launchkit.js"); require("./launchreply.js")), then:
const K = LaunchKit, R = LaunchReply;
const f = K.form(Object.assign(K.blank(), { url: "https://a.example", siteType: "doc", host: "netlify", cspLevel: "strict",
headers: "HTTP/2 200\nstrict-transport-security: max-age=31536000; includeSubDomains\nx-content-type-options: nosniff",
head: "<title>A - docs</title><link rel=\"canonical\" href=\"https://a.example/\">",
robots: "User-agent: *\nAllow: /\nSitemap: https://a.example/sitemap.xml" }));
// f.rows (every check: phase, id, label, status, evidence), f.verdict, f.policy, f.findings
// K.robotsTxt(f), K.headersConfig(f) (here _headers), K.manifestJson(f) - the files that need no judgement
const b = K.buildInput(f, "report"); // or "files" (R.laneForm(form, "files", report) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "report");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. 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="YOUR_TOKEN" # from https://launch-proof.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 = "YOUR_TOKEN" # from https://launch-proof.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"]
// launch-proof.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://launch-proof.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 = "YOUR_TOKEN" // from https://launch-proof.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
}
// Launch Proof.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 Launch Proof {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://launch-proof.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 = "YOUR_TOKEN" # from https://launch-proof.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 = "YOUR_TOKEN"; // from https://launch-proof.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 = "YOUR_TOKEN"; // from https://launch-proof.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":"launch-proof"}' | 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": "launch-proof"})["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: "launch-proof" })).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": "launch-proof"}, 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", "launch-proof"), 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: "launch-proof" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "launch-proof"])["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 = "launch-proof" }))!["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": "report", "site": "Site: https://quarlo.example\nSite type: Doc site or library homepage\n...", "served": "=== Response headers ===\nHTTP/2 200\n...", "carry": "", "facts": "The page's checks (4 fail, 5 warn, ...):\n- phase 4 | csp | fail | ..."}
# - see the input table
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": "report", "site": "Site: https://quarlo.example\nSite type: Doc site or library homepage\n...", "served": "=== Response headers ===\nHTTP/2 200\n...", "carry": "", "facts": "The page's checks (4 fail, 5 warn, ...):\n- phase 4 | csp | fail | ..."}
# - see the input table
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: "report" or "files", site, served, carry, facts} (see the input 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 launch-proof:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A report reply carries summary, phases, order and caveats; a files reply carries summary, llms_txt, meta, og, jsonld and caveats. The code below prints the gist of either: the order to fix things in, or each page's title.
KEY="launch-proof:$(jq -r .task input.json):$(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}'
jq -r 'if .task == "files" then (.meta[] | "\(.path) - \(.title)")
else (.order | to_entries[] | "\(.key + 1). \(.value)") end' reply.json
import hashlib, time
key = "launch-proof:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "files":
for m in reply["meta"]:
print(m["path"], "-", m["title"], len(m["title"]))
else:
for i, x in enumerate(reply["order"], 1):
print(i, x)
import { createHash } from "node:crypto";
const key = `launch-proof:${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.task);
if (reply.task === "files") for (const m of reply.meta) console.log(m.path, "-", m.title, m.title.length);
else reply.order.forEach((x, i) => console.log(i + 1, x));
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "launch-proof:" + 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 }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
if reply["task"] == "files" {
ms, _ := reply["meta"].([]any)
for _, m := range ms {
mm := m.(map[string]any)
fmt.Println(mm["path"], "-", mm["title"])
}
} else {
os, _ := reply["order"].([]any)
for i, x := range os {
fmt.Println(i+1, x)
}
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "launch-proof:" + 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.path("task").asText());
if (reply.path("task").asText().equals("files")) {
for (JsonNode m : reply.path("meta")) System.out.println(m.path("path").asText() + " - " + m.path("title").asText());
} else {
int i = 1; for (JsonNode x : reply.path("order")) System.out.println((i++) + ". " + x.asText());
}
require "digest"
key = "launch-proof:#{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["task"]].inspect
if reply["task"] == "files"
reply["meta"].each { |m| puts "#{m["path"]} - #{m["title"]} (#{m["title"].length})" }
else
reply["order"].each_with_index { |x, i| puts "#{i + 1}. #{x}" }
end
$key = "launch-proof:" . $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["task"], "\n";
if ($reply["task"] === "files") { foreach ($reply["meta"] as $m) echo $m["path"], " - ", $m["title"], "\n"; }
else { foreach ($reply["order"] as $i => $x) echo $i + 1, ". ", $x, "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"launch-proof:{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>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "files") { foreach (var m in reply["meta"]!.AsArray()) Console.WriteLine($"{m!["path"]} - {m["title"]}"); }
else { var i = 1; foreach (var x in reply["order"]!.AsArray()) Console.WriteLine($"{i++}. {x}"); }
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: launch-proof:$(jq -r .task input.json):$(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": "report", "site": "Site: https://quarlo.example\nSite type: Doc site or library homepage\n...", "served": "=== Response headers ===\nHTTP/2 200\n...", "carry": "", "facts": "The page's checks (4 fail, 5 warn, ...):\n- phase 4 | csp | fail | ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "launch-proof:" + rev["task"] + ":" + 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": `launch-proof:${rev.task}:${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", "launch-proof:"+input["task"].(string)+":"+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", "launch-proof:" + input.get("task") + ":" + 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" => "launch-proof:#{JSON.parse(rev)["task"]}:#{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: launch-proof:" . json_decode($rev, true)["task"] . ":" . 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", $"launch-proof:{input["task"]}:{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);