Run Stripe Pin Desk from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page masks the comments, finds the API version pins in the file, compares each with the target, bumps the safe ones itself and lists what needs a decision, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans the upgraded file with the same scanner (pinreply.js).
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":"stripe-pin-desk"}), 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, unit, code, facts) is missing. The page always sends all six of task, unit, code, manifest, carry, facts; carry is an empty string for review and manifest may be empty. |
| 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 the work runs - inspect, then produce:
review- each of the page's findings confirmed or cleared, the target and its source, up to five issues a scan cannot see on real lines, the fixes in order and the tests that send the target in a Stripe-Version header.rewrite- the whole file on the target, with what changed and what was kept, every function name unchanged.
A missing or unknown task is answered as review. Both lanes send the same six fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.
The input
| Field | Required | What |
|---|---|---|
task | yes | review or rewrite (the page calls it Upgrade). If it is missing or neither, do review and set "task": "review". |
unit | yes | the file's path, the language and the target with its source. |
code | yes | the file as pasted (before the page's changes). A line starting "// [... N lines cut by the page" (or "# [..." or "<!-- [...") marks code the page scanned but did not send. |
manifest | send it (may be empty) | the package manifest as pasted; may be empty. |
carry | send it (empty for review) | rewrite task only: the review's decisions, one per line as id | confirmed or cleared or extra | where | fix or reason; may be empty. |
facts | yes | the page's scan: the language, the target and its source, every pin (line, kind, where, value, the page's action), thin event handling, then one finding per line as id | area | severity | line | where | evidence | source | what the page did (changed or left). "changed" means the page made the change the finding's fix names in its own copy, which the person already has; the code field is still the original paste, so judge it as written. Lines starting "must:" apply 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, @stripe/upgrade-stripe - the only standard the run applies:
- The target: the person's version when they give one; otherwise the current version from the docs; the bundled fallback only when the docs cannot be reached - and then it is unverified, never "the latest". Never guess a newer version number. State the target and its source.
- Compare the target with every pin: client configuration, per-request overrides, webhook endpoints. Never move a pin to an older version, or a stable pin to a preview, unless the person asks. A pin already on the target is reported unchanged.
- Versions are dates with a release name (2024-12-18.acacia, 2025-08-27.basil, 2026-10-28.endive). Since acacia, monthly versions carry no breaking changes; twice a year a new major release (acacia, basil, clover, dahlia, endive) starts with breaking changes - field renames or removals, behaviour changes, removed endpoints or parameters. Read the changelog between the current and target versions.
- Dynamically typed SDKs (Node.js, Python, Ruby, PHP): set the target in the client configuration and in every per-request override; always pin explicitly, never rely on the account's default version. Strongly typed SDKs (Java, Go, .NET): never set a different version - responses may not match the SDK's types; upgrade the SDK to a release that targets the version. A preview target needs the matching beta SDK release in every language.
- Tests: send the target in a Stripe-Version header (curl -H "Stripe-Version: ...") before changing the default, and replace bundled versions in examples with the target.
- Webhooks: snapshot payloads follow the endpoint's version; thin event payloads are unversioned and need no upgrade (fetching the full event or resource stays versioned). For each snapshot destination, compare its version with the target; a v2 destination's snapshot_api_version cannot change, so create and test a replacement; a webhook endpoint moves the same way - a replacement created on the target (in code, the call that creates the endpoint pinned to the target; retiring the old one is a Dashboard step). For a replacement webhook endpoint save its new signing secret and accept both secrets during testing and cutover (a second env var such as STRIPE_WEBHOOK_SECRET_PREVIOUS is fine), then disable or delete the old one. The listener handles unfamiliar event types gracefully: an explicit default or else branch that logs the type and still answers 2xx.
- Stripe.js: a release per major (acacia ... endive), loaded from js.stripe.com/<release>/stripe.js or an @stripe/stripe-js npm major; each release pairs with its API version and the pairing cannot be overridden. v3 stays supported indefinitely; move the API version first, then switch the Stripe.js release.
- Mobile SDKs: iOS and Android use semantic versioning (a major bump is breaking); React Native uses 0.x.y (a minor change carries breaking changes and features). They work with any backend version.
- Store Stripe object ids in columns of up to 255 characters with a case-sensitive collation (name a collation only when the code shows the database; otherwise say so in a caveat). After an account upgrade, a rollback stays open for 72 hours in Workbench.
House rules
- Use only the input. Never invent a Stripe call, a resource, a parameter, a field rename or a version the input does not hold; you cannot read the changelog, so name the calls a breaking release may touch and say the changelog decides, never what it says. When a step needs the person (the Dashboard or Workbench, an endpoint outside this file, the account upgrade), say so in a caveat.
- The page's ids and lines are measured facts: never renumber an id. Every finding is confirmed or cleared with a reason tied to the code: a changed one is confirmed with the page's change as the fix (say what the page did), or cleared when that change is wrong here; a left one is confirmed with the hand fix (one decision, never "X, or Y"), or cleared when the code shows it does not apply. A left pin the person decides on (a newer or preview pin) is confirmed with "keep it" as the decision; a finding whose fix is reading the changelog, a Dashboard step or the manifest is confirmed with that check as the fix.
- Never put a secret key, a signing secret or a token in any field; read keys from env vars (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET).
- Never name an SDK package version number: say the release that targets the version ("the stripe-python release that targets 2026-10-28.endive").
- No web links (http or https) in prose; the tests and every code field may hold URLs (the Stripe API, js.stripe.com).
- Write in English. Keep the JSON keys and the ids exactly as given.
- Never use backticks in prose (the code fields are code). Name calls plainly (stripe.webhookEndpoints.create, stripe.Webhook.construct_event). Words from the rules above are free to use; what you never do is paste a whole sentence from this prompt.
- Never write a gendered pronoun for a user; write they, the user or the person.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
review
Decide each finding, state the target, add what a scan cannot see, put the steps in order and write the tests.
summary- one sentence, at most 40 words: which pins are off the target today, what the page already changed, and the first step.target- one sentence: the target version, its source (entered by the person, or the bundled fallback - unverified), and whether it crosses a major release from the oldest pin.findings- one object per page finding you confirm, in id order:id,widget(where it sits, e.g. "createCheckout"),issue(one sentence),fix(one sentence, one decision),code(the lines for that piece only, at most 8; "" when fix says it all).cleared- one object per page finding that is not a problem here, withidandwhy(one sentence tied to the code). Every page id appears exactly once, in findings or in cleared.extra- at most 5 issues the scan cannot see, each tied to a line of the code:id(X1, X2, ...),area(pins, client, typed, changelog, tests, webhooks, events, stripejs, sdk or storage),line(a number),widget,issue,fix. Examples: a response field the code reads that a major release may rename (name the call, the changelog decides), a pin passed through a helper, a handler that verifies with one signing secret where cutover needs two, an object id cached in a fixed-width field, a retry that replays a request on the old version.[]when there are none.plan- 2 to 10 short strings, the steps in order following the skill's checklist (the changelog, the SDK package, the client pin and overrides, the tests with the Stripe-Version header, webhook endpoints, Stripe.js and mobile, id storage), each naming the ids it covers (e.g. "Pin the reporting client to the target (P3)"). Every confirmed id appears in a step.[]only when nothing is confirmed.tests- 3 to 12 strings, each one command or one check: curl calls to the Stripe API for the resources this file uses, each with -u "$STRIPE_SECRET_KEY:" and -H "Stripe-Version: <the target>"; the webhook check (stripe trigger or a test event to the replacement endpoint); one check per changed call path.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "review",
"summary": "one sentence",
"target": "one sentence",
"findings": [ { "id": "P1", "widget": "the file", "issue": "one sentence", "fix": "one sentence", "code": "" } ],
"cleared": [ { "id": "P2", "why": "one sentence" } ],
"extra": [ { "id": "X1", "area": "changelog", "line": 52, "widget": "syncSubscription", "issue": "one sentence", "fix": "one sentence" } ],
"plan": [ "Pin the reporting client to the target (P3)" ],
"tests": [ "curl https://api.stripe.com/v1/customers -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in pinreply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; the target is stated and is the page's, and the bundled fallback is never called the latest without saying it is unverified; at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; 3 to 12 tests, every API call among them sending Stripe-Version with the target and the key from STRIPE_SECRET_KEY; a summary; no web link in the prose and no key in any field.
rewrite
Rewrite the whole file on the target.
summary- one sentence, at most 40 words: what the upgrade changed and what it left for the person.code- the complete file. Keep every function, export, class, import still used and every call; change what the findings and the carried decisions name, plus every carried extra whose edit is made in this file (list it in changes; one that needs another file or the Dashboard goes in a caveat). Every pin the file keeps is the target, or the pasted value of a pin whose finding is in kept (a newer pin, a preview pin, a typed SDK the person upgrades). Never write a secret. Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line marking a cut by the page, keep that line exactly where it is.changes- one object per page finding you acted on (including the ones the page already changed, which the upgrade keeps), in id order:id,change(one sentence naming where). Carried extra ids (X1, ...) go after the page's ids.kept- one object per page finding you did not change:id,why(one sentence - the review cleared it, it needs the changelog or the Dashboard, or the manifest the person edits). A confirmed finding whose fix is a check rather than a line in this file (the changelog, a replacement endpoint made in the Dashboard, the manifest) belongs in kept; a carried extra with no edit in this file goes in a caveat. The page's re-scan still lists what stays true after a correct upgrade (a snapshot handler, a pin kept on purpose). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "rewrite",
"summary": "one sentence",
"code": "the complete file",
"changes": [ { "id": "P1", "change": "one sentence" } ],
"kept": [ { "id": "P2", "why": "one sentence" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every function survives; the file is not shortened and elides nothing; no key in the code; and the upgraded code is scanned again against the same target - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged; so is a pin on a version neither pasted nor the target, and a pin newer than the target that the upgrade moved down.
Worked examples
Three runs from the page's free samples (invented apps): the review of a Node.js billing service on 2024-06-20, the upgrade of the same file carrying that review, and the review of a checkout page on Stripe.js basil. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, 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.
review - A Node.js billing service on 2024-06-20
The page read it as: Needs decisions - 0 blocker, 1 major, 1 minor; findings P1 major-jump, P2 stale-pin, P3 no-pin, P4 id-length, P5 major-jump, P6 stale-pin, P7 webhook-version, P8 snapshot-handler, P9 unknown-event.
Input (code is 2151 characters; the first 1380 are shown; facts is 3192 characters; the first 1395 are shown):
{
"task": "review",
"unit": "File: src/billing.ts\nLanguage: Node.js / TypeScript (stripe-node)\nTarget: 2026-10-28.endive (bundled fallback, unverified)",
"code": "import Stripe from \"stripe\";\nimport express from \"express\";\nimport knex from \"knex\";\nimport { config } from \"./config\";\n\n// One client for the app, one for the nightly reports job.\nconst stripe = new Stripe(config.stripeSecretKey, {\n apiVersion: \"2024-06-20\",\n maxNetworkRetries: 2,\n});\nconst reporting = new Stripe(config.reportingKey);\n\nexport async function migrate(db: knex.Knex) {\n await db.schema.createTable(\"accounts\", (table) => {\n table.increments(\"id\");\n table.string(\"email\").notNullable();\n table.string(\"stripe_customer_id\", 32);\n table.string(\"stripe_subscription_id\", 255);\n });\n}\n\nexport async function createCheckout(priceId: string, customer: string) {\n return stripe.checkout.sessions.create(\n { mode: \"subscription\", customer, line_items: [{ price: priceId, quantity: 1 }], success_url: config.appUrl + \"/thanks\" },\n { apiVersion: \"2025-03-31.basil\" }\n );\n}\n\nexport async function monthlyRevenue() {\n const charges = await reporting.charges.list({ limit: 100, created: { gte: config.monthStart } });\n return charges.data.reduce((sum, c) => sum + c.amount, 0);\n}\n\nexport async function registerWebhook(url: string) {\n return stripe.webhookEndpoints.create({\n url,\n api_version: \"2024-06-20\",\n enabled_events: [\"invoice.paid\", \"customer.subscription.updated\", \"customer.subscription.deleted\"],\n });\n}\n\nconst app = express();\n\n[... 771 more characters cut from this display ...]",
"manifest": "",
"carry": "",
"facts": "The page's scan of 66 lines (Node.js / TypeScript (stripe-node), detected) - 3 version pins (1 client, 1 per-request, 1 webhook endpoint); 4 of 9 findings changed by the page; verdict: Needs decisions.\nTarget: 2026-10-28.endive - the skill's bundled fallback, a snapshot from 2026-11-03; not verified against the live docs, so never call it the latest; the endive release, stable.\nPins (line | kind | where | pinned | the page's action):\n- line 8 | client | the file | 2024-06-20 | bumped to 2026-10-28.endive\n- line 25 | per-request | createCheckout | 2025-03-31.basil | bumped to 2026-10-28.endive\n- line 37 | webhook endpoint | registerWebhook | 2024-06-20 | left\nThin event handling: none seen. Manifest: none pasted.\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- P1 | changelog | major | line 8 | the file | From 2024-06-20 to 2026-10-28.endive the integration crosses 5 major releases (acacia, basil, clover, dahlia, endive), each starting with breaking changes: field renames or removals, behaviour changes, removed endpoints or parameters. | source: apiVersion: \"2024-06-20\", | left\n- P2 | pins | major | line 8 | the file | A client pin on 2024-06-20, older than the target 2026-10-28.endive. | source: apiVersion: \"2024-06-20\", | changed\n- P3 | client | major | line 11 | the file | The client is created without an explicit API version - it\n\n[... 1797 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "The client, reporting client and createCheckout pins sit off 2026-10-28.endive and the webhook endpoint stays on 2024-06-20; the page bumped the pins and widened stripe_customer_id, so the first step is reading the changelog from 2024-06-20.",
"target": "The target is 2026-10-28.endive, the skill's bundled fallback and unverified against the live docs, and from the oldest pin 2024-06-20 it crosses five major releases (acacia, basil, clover, dahlia, endive).",
"findings": [
{
"id": "P1",
"widget": "the file",
"issue": "The client moves from 2024-06-20 to 2026-10-28.endive across five major releases, so calls such as stripe.checkout.sessions.create, reporting.charges.list and stripe.webhookEndpoints.create may meet breaking changes.",
"fix": "Read the changelog from 2024-06-20 to 2026-10-28.endive and adjust each call and field this file reads where the changelog says so.",
"code": ""
},
{
"id": "P2",
"widget": "the file",
"issue": "The main client is pinned to 2024-06-20, older than the target.",
"fix": "Keep the page's change, which sets apiVersion to 2026-10-28.endive in the main client configuration.",
"code": "const stripe = new Stripe(config.stripeSecretKey, {\n apiVersion: \"2026-10-28.endive\",\n maxNetworkRetries: 2,\n});"
},
{
"id": "P3",
"widget": "the file",
"issue": "The reporting client is created without an API version, so it follows the account's default and shifts when the account is upgraded.",
"fix": "Keep the page's change, which pins the reporting client explicitly to 2026-10-28.endive.",
"code": "const reporting = new Stripe(config.reportingKey, { apiVersion: \"2026-10-28.endive\" });"
},
{
"id": "P4",
"widget": "migrate",
"issue": "The stripe_customer_id column is capped at 32 characters while Stripe object ids can reach 255.",
"fix": "Keep the page's change, which widens stripe_customer_id to 255 characters.",
"code": "table.string(\"stripe_customer_id\", 255);"
},
{
"id": "P5",
"widget": "createCheckout",
"issue": "The per-request override moves from 2025-03-31.basil to the target across clover, dahlia and endive, so stripe.checkout.sessions.create and its parameters may meet breaking changes.",
"fix": "Check stripe.checkout.sessions.create and its parameters (mode, customer, line_items, success_url) against the changelog from basil to endive before shipping the bump.",
"code": ""
},
{
"id": "P6",
"widget": "createCheckout",
"issue": "The per-request override on stripe.checkout.sessions.create is pinned to 2025-03-31.basil, older than the target.",
"fix": "Keep the page's change, which sets the per-request apiVersion to 2026-10-28.endive.",
"code": "{ apiVersion: \"2026-10-28.endive\" }"
},
{
"id": "P7",
"widget": "registerWebhook",
"issue": "The webhook endpoint is created on 2024-06-20, so its snapshot payloads keep that version until the endpoint is replaced.",
"fix": "Set api_version to 2026-10-28.endive in registerWebhook and create a replacement endpoint with it, saving its new signing secret, then disable the old endpoint after cutover.",
"code": "api_version: \"2026-10-28.endive\","
},
{
"id": "P8",
"widget": "the file",
"issue": "The handler casts snapshot payloads to Stripe.Invoice and Stripe.Subscription, which follow the endpoint's version rather than the client's pin, so they stay on 2024-06-20 until the endpoint moves.",
"fix": "Test markPaid and syncSubscription with snapshot events from the replacement endpoint on 2026-10-28.endive before cutover.",
"code": ""
},
{
"id": "P9",
"widget": "the file",
"issue": "The switch over event.type has no default branch, so an unfamiliar event type passes without any handling.",
"fix": "Add a default branch that logs the unfamiliar event type and still answers with received true.",
"code": " default:\n console.log(\"unhandled event\", event.type);\n break;"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "webhooks",
"line": 44,
"widget": "the file",
"issue": "stripe.webhooks.constructEvent verifies with the single config.webhookSecret, but the cutover to a replacement endpoint needs the old and new signing secrets accepted together.",
"fix": "Try the new signing secret first and fall back to the old one, both read from env vars, until the old endpoint is disabled."
},
{
"id": "X2",
"area": "changelog",
"line": 63,
"widget": "syncSubscription",
"issue": "syncSubscription reads sub.current_period_end from the subscription payload, a field a major release may rename or move.",
"fix": "Check the customer.subscription events and the Subscription object in the changelog and change the field read only if the changelog says so."
},
{
"id": "X3",
"area": "changelog",
"line": 59,
"widget": "markPaid",
"issue": "markPaid reads invoice.amount_paid from the invoice.paid payload, a field a major release may change.",
"fix": "Check the Invoice object in the changelog from 2024-06-20 to endive and adjust the field read only if the changelog says so."
}
],
"plan": [
"Read the changelog from 2024-06-20 to 2026-10-28.endive for every call this file makes (P1, P5)",
"Pin the main client and the reporting client to the target (P2, P3)",
"Move the createCheckout per-request override to the target (P6)",
"Widen stripe_customer_id to 255 characters with a migration (P4)",
"Run the tests with the Stripe-Version header on the target before changing the default",
"Create a replacement webhook endpoint on the target in registerWebhook and save its signing secret (P7)",
"Accept both signing secrets during cutover and test the handlers with target snapshot events (P8)",
"Add a default branch for unfamiliar event types, then disable the old endpoint (P9)"
],
"tests": [
"curl https://api.stripe.com/v1/customers?limit=1 -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"",
"curl https://api.stripe.com/v1/checkout/sessions?limit=1 -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"",
"curl \"https://api.stripe.com/v1/charges?limit=100\" -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"",
"curl https://api.stripe.com/v1/webhook_endpoints -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"",
"curl https://api.stripe.com/v1/webhook_endpoints -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\" -d url=\"$WEBHOOK_URL\" -d api_version=2026-10-28.endive -d \"enabled_events[]=invoice.paid\"",
"curl https://api.stripe.com/v1/subscriptions?limit=1 -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"",
"stripe trigger invoice.paid against the replacement endpoint and check markPaid logs the invoice id and amount",
"stripe trigger customer.subscription.updated and check syncSubscription logs the id, status and period end",
"Call createCheckout with a test price and customer and check the session is created on 2026-10-28.endive",
"Run monthlyRevenue against test-mode charges and check the sum matches"
],
"caveats": [
"The account upgrade itself happens in Workbench, and a rollback stays open for 72 hours after it.",
"Endpoints registered outside this file need a replacement on the target in the Dashboard.",
"The second signing secret has to be added to the environment alongside STRIPE_WEBHOOK_SECRET during cutover."
]
}
What step 5 prints for it: P1: Read the changelog from 2024-06-20 to 2026-10-28.endive and adjust each call and field this file reads where the changelog says so. | P2: Keep the page's change, which sets apiVersion to 2026-10-28.endive in the main client configuration. | P3: Keep the page's change, which pins the reporting client explicitly to 2026-10-28.endive. | P4: Keep the page's change, which widens stripe_customer_id to 255 characters. | P5: Check stripe.checkout.sessions.create and its parameters (mode, customer, line_items, success_url) against the changelog from basil to endive before shipping the bump. | P6: Keep the page's change, which sets the per-request apiVersion to 2026-10-28.endive. | P7: Set api_version to 2026-10-28.endive in registerWebhook and create a replacement endpoint with it, saving its new signing secret, then disable the old endpoint after cutover. | P8: Test markPaid and syncSubscription with snapshot events from the replacement endpoint on 2026-10-28.endive before cutover. | P9: Add a default branch that logs the unfamiliar event type and still answers with received true.
The page's check: kept - X1 repeats what the page already found on line 44.
rewrite - A Node.js billing service on 2024-06-20
The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page re-scanned the upgraded code: 9 findings before, 1 after.
Input (code is 2151 characters; the first 1380 are shown; facts is 3192 characters; the first 1395 are shown; carry is 1811 characters; the first 1399 are shown):
{
"task": "rewrite",
"unit": "File: src/billing.ts\nLanguage: Node.js / TypeScript (stripe-node)\nTarget: 2026-10-28.endive (bundled fallback, unverified)",
"code": "import Stripe from \"stripe\";\nimport express from \"express\";\nimport knex from \"knex\";\nimport { config } from \"./config\";\n\n// One client for the app, one for the nightly reports job.\nconst stripe = new Stripe(config.stripeSecretKey, {\n apiVersion: \"2024-06-20\",\n maxNetworkRetries: 2,\n});\nconst reporting = new Stripe(config.reportingKey);\n\nexport async function migrate(db: knex.Knex) {\n await db.schema.createTable(\"accounts\", (table) => {\n table.increments(\"id\");\n table.string(\"email\").notNullable();\n table.string(\"stripe_customer_id\", 32);\n table.string(\"stripe_subscription_id\", 255);\n });\n}\n\nexport async function createCheckout(priceId: string, customer: string) {\n return stripe.checkout.sessions.create(\n { mode: \"subscription\", customer, line_items: [{ price: priceId, quantity: 1 }], success_url: config.appUrl + \"/thanks\" },\n { apiVersion: \"2025-03-31.basil\" }\n );\n}\n\nexport async function monthlyRevenue() {\n const charges = await reporting.charges.list({ limit: 100, created: { gte: config.monthStart } });\n return charges.data.reduce((sum, c) => sum + c.amount, 0);\n}\n\nexport async function registerWebhook(url: string) {\n return stripe.webhookEndpoints.create({\n url,\n api_version: \"2024-06-20\",\n enabled_events: [\"invoice.paid\", \"customer.subscription.updated\", \"customer.subscription.deleted\"],\n });\n}\n\nconst app = express();\n\n[... 771 more characters cut from this display ...]",
"manifest": "",
"carry": "P1 | confirmed | the file | Read the changelog from 2024-06-20 to 2026-10-28.endive and adjust each call and field this file reads where the changelog says so.\nP2 | confirmed | the file | Keep the page's change, which sets apiVersion to 2026-10-28.endive in the main client configuration.\nP3 | confirmed | the file | Keep the page's change, which pins the reporting client explicitly to 2026-10-28.endive.\nP4 | confirmed | migrate | Keep the page's change, which widens stripe_customer_id to 255 characters.\nP5 | confirmed | createCheckout | Check stripe.checkout.sessions.create and its parameters (mode, customer, line_items, success_url) against the changelog from basil to endive before shipping the bump.\nP6 | confirmed | createCheckout | Keep the page's change, which sets the per-request apiVersion to 2026-10-28.endive.\nP7 | confirmed | registerWebhook | Set api_version to 2026-10-28.endive in registerWebhook and create a replacement endpoint with it, saving its new signing secret, then disable the old endpoint after cutover.\nP8 | confirmed | the file | Test markPaid and syncSubscription with snapshot events from the replacement endpoint on 2026-10-28.endive before cutover.\nP9 | confirmed | the file | Add a default branch that logs the unfamiliar event type and still answers with received true.\nX1 | extra | the file (line 44) | Try the new signing secret first and fall back to the\n\n[... 412 more characters cut from this display ...]",
"facts": "The page's scan of 66 lines (Node.js / TypeScript (stripe-node), detected) - 3 version pins (1 client, 1 per-request, 1 webhook endpoint); 4 of 9 findings changed by the page; verdict: Needs decisions.\nTarget: 2026-10-28.endive - the skill's bundled fallback, a snapshot from 2026-11-03; not verified against the live docs, so never call it the latest; the endive release, stable.\nPins (line | kind | where | pinned | the page's action):\n- line 8 | client | the file | 2024-06-20 | bumped to 2026-10-28.endive\n- line 25 | per-request | createCheckout | 2025-03-31.basil | bumped to 2026-10-28.endive\n- line 37 | webhook endpoint | registerWebhook | 2024-06-20 | left\nThin event handling: none seen. Manifest: none pasted.\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- P1 | changelog | major | line 8 | the file | From 2024-06-20 to 2026-10-28.endive the integration crosses 5 major releases (acacia, basil, clover, dahlia, endive), each starting with breaking changes: field renames or removals, behaviour changes, removed endpoints or parameters. | source: apiVersion: \"2024-06-20\", | left\n- P2 | pins | major | line 8 | the file | A client pin on 2024-06-20, older than the target 2026-10-28.endive. | source: apiVersion: \"2024-06-20\", | changed\n- P3 | client | major | line 11 | the file | The client is created without an explicit API version - it\n\n[... 1797 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rewrite",
"summary": "Every pin in src/billing.ts now sits on 2026-10-28.endive, stripe_customer_id is 255 wide and the webhook accepts two secrets with a default branch; the changelog review and the replacement endpoint cutover stay with the person.",
"code": "import Stripe from \"stripe\";\nimport express from \"express\";\nimport knex from \"knex\";\nimport { config } from \"./config\";\n\n// One client for the app, one for the nightly reports job.\nconst stripe = new Stripe(config.stripeSecretKey, {\n apiVersion: \"2026-10-28.endive\",\n maxNetworkRetries: 2,\n});\nconst reporting = new [... 2487 more characters cut from this display]",
"changes": [
{
"id": "P2",
"change": "The main client configuration keeps the page's apiVersion of 2026-10-28.endive."
},
{
"id": "P3",
"change": "The reporting client is now created with an explicit apiVersion of 2026-10-28.endive."
},
{
"id": "P4",
"change": "migrate keeps the page's widening of stripe_customer_id to 255 characters."
},
{
"id": "P6",
"change": "The createCheckout per-request override keeps the page's bump to 2026-10-28.endive."
},
{
"id": "P7",
"change": "registerWebhook now creates endpoints with api_version 2026-10-28.endive."
},
{
"id": "P9",
"change": "The switch in the webhook route gains a default branch that logs the unfamiliar event type."
},
{
"id": "X1",
"change": "The webhook route verifies through verifyEvent, which falls back to STRIPE_WEBHOOK_SECRET_PREVIOUS during cutover."
}
],
"kept": [
{
"id": "P1",
"why": "The breaking changes from 2024-06-20 to endive need the changelog, which this rewrite cannot read."
},
{
"id": "P5",
"why": "Whether stripe.checkout.sessions.create needs other edits is for the changelog from basil to endive to decide."
},
{
"id": "P8",
"why": "Testing markPaid and syncSubscription against snapshot events needs the replacement endpoint, which is created outside this file."
}
],
"caveats": [
"The fields read in markPaid and syncSubscription (X3, X2) stay as written until the changelog says otherwise.",
"Register the replacement endpoint, set STRIPE_WEBHOOK_SECRET_PREVIOUS to the old secret during cutover, then disable the old endpoint.",
"The account upgrade happens in Workbench, and a rollback stays open for 72 hours."
]
}
Cut from this display only (the saved reply and the check use the full text): code (2804 characters, 317 shown).
What step 5 prints for it: P2: The main client configuration keeps the page's apiVersion of 2026-10-28.endive. | P3: The reporting client is now created with an explicit apiVersion of 2026-10-28.endive. | P4: migrate keeps the page's widening of stripe_customer_id to 255 characters. | P6: The createCheckout per-request override keeps the page's bump to 2026-10-28.endive. | P7: registerWebhook now creates endpoints with api_version 2026-10-28.endive. | P9: The switch in the webhook route gains a default branch that logs the unfamiliar event type. | X1: The webhook route verifies through verifyEvent, which falls back to STRIPE_WEBHOOK_SECRET_PREVIOUS during cutover.
The page's check: kept - nothing to flag
review - A checkout page on Stripe.js basil
The page read it as: Needs decisions - 0 blocker, 1 major, 0 minor; findings P1 stripejs-old, P2 stripejs-override.
Input (facts is 1529 characters; the first 1395 are shown):
{
"task": "review",
"unit": "File: public/checkout.html\nLanguage: HTML page (Stripe.js)\nTarget: 2026-10-28.endive (bundled fallback, unverified)",
"code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <title>Checkout</title>\n <script src=\"https://js.stripe.com/basil/stripe.js\"></script>\n <link rel=\"stylesheet\" href=\"/checkout.css\" />\n</head>\n<body>\n <form id=\"payment-form\">\n <div id=\"payment-element\"></div>\n <button id=\"submit\">Pay now</button>\n <p id=\"error\" role=\"alert\"></p>\n </form>\n <script>\n const stripe = Stripe(window.STRIPE_PUBLISHABLE_KEY, { apiVersion: \"2025-03-31.basil\" });\n let elements;\n\n async function initialize() {\n const res = await fetch(\"/create-payment-intent\", { method: \"POST\" });\n const { clientSecret } = await res.json();\n elements = stripe.elements({ clientSecret, appearance: { theme: \"stripe\" } });\n elements.create(\"payment\").mount(\"#payment-element\");\n }\n\n async function handleSubmit(e) {\n e.preventDefault();\n const { error } = await stripe.confirmPayment({\n elements,\n confirmParams: { return_url: window.location.origin + \"/complete\" },\n });\n document.getElementById(\"error\").textContent = error ? error.message : \"\";\n }\n\n document.getElementById(\"payment-form\").addEventListener(\"submit\", handleSubmit);\n initialize();\n </script>\n</body>\n</html>\n",
"manifest": "",
"carry": "",
"facts": "The page's scan of 40 lines (HTML page (Stripe.js), detected) - 2 version pins (1 Stripe.js option, 1 Stripe.js script); 1 of 2 findings changed by the page; verdict: Needs decisions.\nTarget: 2026-10-28.endive - the skill's bundled fallback, a snapshot from 2026-11-03; not verified against the live docs, so never call it the latest; the endive release, stable.\nPins (line | kind | where | pinned | the page's action):\n- line 16 | Stripe.js option | the file | 2025-03-31.basil | left\n- line 6 | Stripe.js script | the file | basil | bumped to endive\nThin event handling: none seen. Manifest: none pasted.\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- P1 | stripejs | major | line 6 | the file | The script loads the basil release; Stripe.js pairs each release with its own API version, so the page keeps the basil API until the script moves. | source: <script src=\"https://js.stripe.com/basil/stripe.js\"></script> | changed\n- P2 | stripejs | major | line 16 | the file | An apiVersion option (2025-03-31.basil) on a versioned Stripe.js release (basil): each release pairs with its own API version and the association cannot be overridden. | source: const stripe = Stripe(window.STRIPE_PUBLISHABLE_KEY, { apiVersion: \"2025-03-31.basil\" }); | left\nmust: account for every finding id above exactly once - in findings (confirmed) or in cleared (with the\n\n[... 134 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "The Stripe.js script loads basil and the Stripe() call passes apiVersion 2025-03-31.basil; the page moved the script to endive, and the first step is moving the server's API version to the target.",
"target": "The target is 2026-10-28.endive, the skill's bundled fallback and unverified against the live docs, and from the oldest pin 2025-03-31.basil it crosses three major releases (clover, dahlia, endive).",
"findings": [
{
"id": "P1",
"widget": "the file",
"issue": "The script loads the basil release of Stripe.js, which pairs with the basil API version.",
"fix": "Keep the page's change, which loads the endive release of Stripe.js in place of basil.",
"code": ""
},
{
"id": "P2",
"widget": "the file",
"issue": "The Stripe() call passes apiVersion 2025-03-31.basil on a versioned release, where the pairing cannot be overridden.",
"fix": "Remove the apiVersion option from the Stripe() call and let the endive release set the version.",
"code": "const stripe = Stripe(window.STRIPE_PUBLISHABLE_KEY);"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "pins",
"line": 20,
"widget": "initialize",
"issue": "The PaymentIntent behind clientSecret is created by the server route /create-payment-intent, whose pin this file cannot show.",
"fix": "Pin the server code behind /create-payment-intent to 2026-10-28.endive before the Stripe.js switch ships."
}
],
"plan": [
"Read the changelog from 2025-03-31.basil to 2026-10-28.endive for PaymentIntents, Elements and stripe.confirmPayment (P1, P2)",
"Move the server behind /create-payment-intent to the target first",
"Run the tests with the Stripe-Version header on the target",
"Switch the script to the endive release of Stripe.js (P1)",
"Drop the apiVersion option from the Stripe() call (P2)"
],
"tests": [
"curl https://api.stripe.com/v1/payment_intents -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\" -d amount=1000 -d currency=usd -d \"automatic_payment_methods[enabled]=true\"",
"curl https://api.stripe.com/v1/payment_intents?limit=1 -u \"$STRIPE_SECRET_KEY:\" -H \"Stripe-Version: 2026-10-28.endive\"",
"Load the checkout page and check the browser console shows no error from the Stripe() call",
"Check the Payment Element mounts in payment-element with a clientSecret from /create-payment-intent",
"Submit a test card through handleSubmit and check stripe.confirmPayment returns to the complete page",
"stripe trigger payment_intent.succeeded and check the server's handler receives it"
],
"caveats": [
"The server route /create-payment-intent lives outside this file and has to move to the target before the script changes.",
"The account upgrade happens in Workbench, and a rollback stays open for 72 hours."
]
}
What step 5 prints for it: P1: Keep the page's change, which loads the endive release of Stripe.js in place of basil. | P2: Remove the apiVersion option from the Stripe() call and let the endive release set the version.
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./pinkit.js"); require("./pinreply.js")), then:
const K = PinKit, R = PinReply;
const f = K.form({ name: "src/pay.ts", lang: "auto", target: "2026-10-28.endive", manifest: "", carry: "",
code: "import Stripe from \"stripe\";\nconst stripe = new Stripe(key, { apiVersion: \"2025-09-30.clover\" });\nexport const list = () => stripe.customers.list();" });
// f.scan.pins (each with its kind and the page's action), f.findings (each with .auto when the page changed it), f.scan.migrated (the upgraded copy), f.steps
// K.findingsCsv(f), K.itemsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review"); // or "rewrite" (R.laneForm(form, "rewrite", review) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b) // {status: kept or flags, 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://stripe-pin-desk.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://stripe-pin-desk.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"]
// stripe-pin-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://stripe-pin-desk.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://stripe-pin-desk.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
}
// PinDesk.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 PinDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://stripe-pin-desk.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://stripe-pin-desk.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://stripe-pin-desk.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://stripe-pin-desk.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":"stripe-pin-desk"}' | 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": "stripe-pin-desk"})["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: "stripe-pin-desk" })).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": "stripe-pin-desk"}, 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", "stripe-pin-desk"), 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: "stripe-pin-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "stripe-pin-desk"])["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 = "stripe-pin-desk" }))!["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": "review", "unit": "File: src/billing.ts\nLanguage: Node.js / TypeScript (stripe-node)\nTarget: 2026-10-28.endive (bundled fallback, unverified)", "code": "import Stripe from \"stripe\";\n...", "manifest": "", "carry": "", "facts": "The page's scan of 66 lines ...\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- P1 | changelog | major | line 8 | ..."}
# - 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": "review", "unit": "File: src/billing.ts\nLanguage: Node.js / TypeScript (stripe-node)\nTarget: 2026-10-28.endive (bundled fallback, unverified)", "code": "import Stripe from \"stripe\";\n...", "manifest": "", "carry": "", "facts": "The page's scan of 66 lines ...\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- P1 | changelog | major | line 8 | ..."}
# - 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: "review" or "rewrite", unit, code, manifest, 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 stripe-pin-desk:<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 review reply carries summary, target, findings, cleared, extra, plan, tests and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.
KEY="stripe-pin-desk:$(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 == "rewrite" then (.changes[] | "\(.id): \(.change)")
else (.findings[] | "\(.id): \(.fix)") end' reply.json
import hashlib, time
key = "stripe-pin-desk:" + 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"] == "rewrite":
for x in reply["changes"]:
print(x["id"], x["change"])
else:
for x in reply["findings"]:
print(x["id"], x["fix"])
import { createHash } from "node:crypto";
const key = `stripe-pin-desk:${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 === "rewrite") { for (const x of reply.changes) console.log(x.id, x.change); }
else { for (const x of reply.findings) console.log(x.id, x.fix); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "stripe-pin-desk:" + 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"])
key, b := "findings", "fix"
if reply["task"] == "rewrite" {
key, b = "changes", "change"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx["id"], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "stripe-pin-desk:" + 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("rewrite")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("id").asText() + " " + x.path("change").asText());
} else {
for (JsonNode x : reply.path("findings")) System.out.println(x.path("id").asText() + " " + x.path("fix").asText());
}
require "digest"
key = "stripe-pin-desk:#{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"] == "rewrite"
reply["changes"].each { |x| puts "#{x["id"]} #{x["change"]}" }
else
reply["findings"].each { |x| puts "#{x["id"]} #{x["fix"]}" }
end
$key = "stripe-pin-desk:" . $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"] === "rewrite") { foreach ($reply["changes"] as $x) echo $x["id"], " ", $x["change"], "\n"; }
else { foreach ($reply["findings"] as $x) echo $x["id"], " ", $x["fix"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"stripe-pin-desk:{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 == "rewrite") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["change"]}"); }
else { foreach (var x in reply["findings"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["fix"]}"); }
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: stripe-pin-desk:$(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": "review", "unit": "File: src/billing.ts\nLanguage: Node.js / TypeScript (stripe-node)\nTarget: 2026-10-28.endive (bundled fallback, unverified)", "code": "import Stripe from \"stripe\";\n...", "manifest": "", "carry": "", "facts": "The page's scan of 66 lines ...\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- P1 | changelog | major | line 8 | ..."}
# - 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": "stripe-pin-desk:" + 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": `stripe-pin-desk:${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", "stripe-pin-desk:"+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", "stripe-pin-desk:" + 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" => "stripe-pin-desk:#{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: stripe-pin-desk:" . 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", $"stripe-pin-desk:{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);