Run Surface Order from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page ranks the lead posture from the buyer type, names the spec and telemetry groundwork, weighs the demand signals, sequences every surface in play into capacity-wide waves behind the contract it derives from - each with its reversal-cost tier and owning skill - and checks your draft order, then hands it all to the run as facts. The run writes the plan or the handoff cards; the page then holds it to its own facts (soreply.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":"surface-order"}), 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, platform, surfaces, facts) is missing. The page always sends all five of task, platform, surfaces, carry, facts; carry is an empty string for plan. |
| 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 - decide the order, then hand it to the teams:
plan- the sequencing plan: two or three candidate sequences with their trade-offs, the recommended posture, the audience map, the groundwork and the build order wave by wave.handoffs- one handoff card per surface in the plan (decision, audience, signal, reversal-cost tier, owner, owning skill, next step) and the review cadence.
A missing or unknown task is answered as plan. Both lanes send the same five fields. The facts are the page's checks and its plan, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the plan into carry for the handoffs lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or handoffs. If it is missing or neither, do plan and set "task": "plan". |
platform | yes | the platform, what it does, the buyer type, time to value, the spec, telemetry, agent demand, capacity, owners, the deadline, one-off or compounding, the effort ceiling, GraphQL pain and the plan date; "not given" means not given. |
surfaces | yes | each surface and its depth today, the demand signals as surface | kind | evidence, and the user's draft order. |
carry | send it (empty for plan) | handoffs task only: the plan decisions, when the plan task ran first; may be empty. |
facts | yes | the page's checks, one line each as area | id | status | evidence, the postures and candidates, the groundwork, the page's plan as surface | decision | wave | tier | sibling | serves | signal | why, and the audiences. 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/api-integration-surface-strategy - the only standard the run applies:
- Build order is governed by reversal cost plus audience, never novelty. Hyrum's Law is the mechanism: once enough outside users depend on a surface, every observable behaviour is depended on, so a public contract is near-impossible to change. Commit early to the low-tier surfaces; delay high-tier commitments until the contract they project has stabilised.
- The buyer type drives the posture more than anything else. A developer choosing the tool bottom-up favours API-first; a non-technical buyer who needs value in hours favours embedded-first. They are layers, not rivals: the API is the durable foundation and an embedded layer goes on top, so a deal with both an economic buyer and a technical user gets both.
- Default rung: REST plus webhooks first - REST extends additively, and webhooks ride the same infrastructure. Embedded-first still plans the API foundation underneath. GraphQL-led is the starved option: high standing effort and a binding schema, promoted only by a named pain integrators confirm; a team already running GraphQL internally gets it cheaper.
- The spec is the central dependency. SDKs, MCP tools and generated docs derive from one machine-readable source of truth, so a drifting spec is fixed before any derived surface is scheduled. MCP and SDKs are never the first rung: SDKs are generated once the contract stabilises, MCP comes last as a thin layer generated from an existing API, timed by agent signals - RFPs or observed agent traffic schedule it; customer questions or none leave it parked. Per-call agent monetization is unproven: label any such line speculative.
- Demand signals are weighed, not collected: a named blocked deal outranks any volume metric; usage telemetry by client type is the strongest volume signal; community asks rank third and are confirmed before a surface is scheduled on them alone. With no telemetry instrumented, instrumenting it is an early plan item.
- One surface at a time, capacity-matched. Never open more surfaces concurrently than the team can build, document, support and keep releasing for years. Two surfaces done properly beat six done badly. A shallow API-first surface - part of a flow still manual - is a named failure mode, fixed before a partner finds it.
- This plan decides that and when; the sibling skill decides how. Every surface in the plan - scheduled, deferred or declined - carries its audience, the demand signal behind it, its reversal-cost tier, an owner and the sibling that owns its design. Embedded components have no sibling: their decisions are recorded in the plan itself.
- The plan decays: a quarterly review, reopened early when a deal is blocked by a missing surface, agent signals appear, telemetry contradicts the audience map, or a scheduled surface's owner loses capacity.
House rules
- Use only the input. Never invent a company, a customer, a deal, a figure, a percentage or a date - except a value a field below asks you to propose (an owner, a trigger), always labelled as proposed.
- The page's postures, plan, waves, tiers, siblings and checks are settled: never reorder a surface, move it to another wave, change a decision, tier or sibling, or call a failed gate fine. Where the user's draft breaks a gate, say what replaces it; the gap belongs in a caveat.
- Ids are exactly as the facts give them. Surface ids: rest, webhooks, sdks, graphql, grpc, sql, bulk, cli, mcp, embedded, terraform. Posture ids - a different list, used only for candidates and the recommendation: rest, embedded, graphql, deal.
- No web links (http or https).
- Write in the language of the input; keep the JSON keys and the ids as given.
- Never use backticks and never write the input's field names as labels in your text. Terms of art from the rules above are free to use; what you never do is paste a whole sentence from this prompt, or state a rule with no fact of this platform in it.
- Never write a gendered pronoun for anyone; write they, the team or the owner.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
plan
Write the sequencing plan: the candidate sequences, the recommendation, the audience map and the build order.
summary- one sentence, at most 40 words: the recommended posture and the first thing that must happen.candidates- one object per candidate the facts list, in that order:posture(its id),sequence(one sentence: the order this posture would build in, for this platform),tradeoff(one sentence: what it costs and what it buys, for this platform).recommend-posture(the id the facts recommend),why(one sentence tied to this platform's buyer and signals).audiences- one object per audience the facts list, in that order:audience(its id),writer(who writes the calling code),surfaces(one sentence: which surfaces serve it now and in the plan).sequence- one object per scheduled surface, in the page's order, no others:surface(its id),wave(the page's wave, a number),why(one sentence: the signal or rule that puts it there, in this platform's terms).groundwork- short strings: what happens before any new surface, from the facts;[]when nothing.caveats- at most 4 short strings: where the user's draft breaks a gate and what replaces it, deferred surfaces worth watching;[]when there is nothing to say.
{
"task": "plan",
"summary": "one sentence",
"candidates": [ { "posture": "rest", "sequence": "one sentence", "tradeoff": "one sentence" } ],
"recommend": { "posture": "rest", "why": "one sentence" },
"audiences": [ { "audience": "integrators", "writer": "who writes the code", "surfaces": "one sentence" } ],
"sequence": [ { "surface": "webhooks", "wave": 1, "why": "one sentence" } ],
"groundwork": [ "a step" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in soreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no web link; no backticks; no gendered pronoun; a summary of at most 40 words; exactly the page's candidates, in order, each with a sequence and a trade-off; the page's recommended posture, with a reason; every audience the page lists, each with who writes the code and its surfaces; the sequence exactly the page's scheduled surfaces, in order and wave; the groundwork named when the spec or telemetry needs it; MCP monetization labelled speculative; a caveat when the draft order breaks a gate.
handoffs
Write the per-surface handoff cards and the review cadence the team will run the plan on.
summary- one sentence, at most 40 words.handoffs- one object per surface in the page's plan, in its order, no others:surface(its id),decision(scheduled, deferred or declined - the page's),audience(one sentence: who it serves on this platform),signal(one sentence: the demand signal that justified the decision, or that none exists),tier(the page's reversal-cost tier, as written),owner(who owns it - from the owners the input names, or proposed and said so),sibling(the page's sibling skill id, copied as the facts write it - for embedded that is "none - decided in this plan"),next(one sentence: the first step for a scheduled surface, or what would reopen a deferred or declined one).review-date(the page's review date as YYYY-MM-DD; when the facts give none, a date a quarter out written as "proposed: YYYY-MM-DD"),triggers(at least three short strings that reopen the plan early, in this platform's terms),recheck(short strings: the evidence the next review re-checks).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "handoffs",
"summary": "one sentence",
"handoffs": [ { "surface": "webhooks", "decision": "scheduled", "audience": "one sentence", "signal": "one sentence", "tier": "low-medium", "owner": "a team", "sibling": "webhook-platform-design", "next": "one sentence" } ],
"review": { "date": "2027-02-01", "triggers": [ "a trigger" ], "recheck": [ "evidence" ] },
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; one handoff per surface in the page's plan, in its order, each with the page's decision, tier and owning skill and a next step; the skill's coverage gate - an audience, a signal, a tier, an owner and a sibling on every card; owners labelled proposed when none were given; the review on the page's date (or proposed when there is no plan date), at least three triggers and something to re-check.
Worked examples
Three runs from the page's free samples (invented companies): the plan for an invoicing API whose draft puts MCP first, the handoff cards for the same platform carrying that plan, and the plan for an analytics platform that lost deals over warehouse access. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but 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.
plan - Ledgerline - MCP first on customer questions
The page read it as: your draft order breaks the skill's gates; postures rest, embedded, graphql; build order webhooks (wave 1), sdks (wave 2); 1 of 7 gates; failing checks: spec, depth, derivation, novelty, concurrency, signals.
Input (facts is 3591 characters; the first 1399 are shown):
{
"task": "plan",
"platform": "Platform: Ledgerline\nWhat it does: An invoicing and payment-reminder API that developers at small SaaS companies wire into their billing.\nBuyer: a developer picks the tool bottom-up; time to value: days acceptable\nSpec: exists but drifts from the API; per-client telemetry: instrumented; agent demand: customer questions only\nCapacity: 1 surface at once; owners available: One API team of four engineers\nDeadline: no hard date; a compounding surface roadmap; effort ceiling: medium\nGraphQL: no confirmed pain; plan date: 2026-11-02",
"surfaces": "Surfaces today (surface | depth):\n- REST API | self-serve end to end\n- Webhooks | partial - some steps still manual\n- Server SDKs | absent\n- GraphQL API | absent\n- gRPC API | absent\n- SQL / JDBC access | absent\n- Bulk data / warehouse shares | absent\n- CLI | absent\n- MCP server | absent\n- Embedded components | absent\n- Terraform / OpenTofu provider | absent\n\nDemand signals (surface | kind | evidence):\n- Server SDKs | usage telemetry | 71% of REST requests carry a hand-rolled Node client user-agent\n- Webhooks | pipeline / blocked deal | a reseller paused its rollout because payment.failed events need a manual export\n- MCP server | community ask | three customers asked on the forum whether we support agents\n\nThe user's draft order (surface | when):\n- MCP server | now\n- Server SDKs | now\n- GraphQL API | next",
"carry": "",
"facts": "The page's checks (6 fail, 1 warn, 1 pass, 6 n/a); verdict: your draft order breaks the skill's gates:\n- Surfaces and the spec | buyer | na | a developer picks the tool bottom-up; time to value: days acceptable\n- Surfaces and the spec | inventory | na | REST API (self-serve), Webhooks (partial)\n- Surfaces and the spec | spec | fail | exists but drifts from the API - fix it before scheduling any derived surface\n- Surfaces and the spec | depth | fail | Webhooks partial - shallow API-first: a partner discovers the manual step mid-integration\n- Demand signals | telemetry | pass | usage by client type can be read\n- Demand signals | signals | na | Server SDKs: usage telemetry; Webhooks: pipeline / blocked deal; MCP server: community ask\n- Demand signals | community | warn | MCP server - cheap to emit; confirm against pipeline data or telemetry\n- Demand signals | agents | na | customer questions only\n- Your draft order | derivation | fail | MCP server on a spec that is not the source of truth; Server SDKs on a spec that is not the source of truth\n- Your draft order | novelty | fail | MCP leads the draft with customer questions only - novelty-driven MCP-first inverts the order\n- Your draft order | concurrency | fail | 2 surfaces now against a capacity of 1\n- Your draft order | signals | fail | GraphQL API - no signal: surface sprawl without telemetry\n- The page's plan | posture | na |\n\n[... 2192 more characters cut from this display ...]"
}
Reply:
{
"task": "plan",
"summary": "Recommend rest: Ledgerline first makes its drifting machine-readable spec the linted source of truth in CI, then finishes webhooks end to end so the reseller no longer needs a manual payment.failed export.",
"candidates": [
{
"posture": "rest",
"sequence": "Fix the drifting spec in CI, finish the partial webhooks so payment.failed events flow without a manual export, then generate server SDKs from the corrected spec.",
"tradeoff": "It spends the four-engineer API team on unglamorous repair before anything new ships, and buys a contract the reseller and the hand-rolled Node clients can safely depend on."
},
{
"posture": "embedded",
"sequence": "Fix the spec and finish webhooks as the API foundation, then add embedded invoicing components on top before any SDK work.",
"tradeoff": "It adds a medium-tier layer for a non-technical buyer Ledgerline does not have, since developers pick it bottom-up and days to value are acceptable, and delays the SDKs the telemetry points at."
}
],
"recommend": {
"posture": "rest",
"why": "Developers at small SaaS companies choose Ledgerline bottom-up, the one blocked deal is a webhooks gap, and telemetry shows most REST traffic coming from hand-rolled Node clients, so the API rung comes first."
},
"audiences": [
{
"audience": "firstparty",
"writer": "A person on a customer's own engineering team.",
"surfaces": "The self-serve REST API serves them today and stays their surface through the plan."
},
{
"audience": "integrators",
"writer": "A person or a code generator at a reseller or partner.",
"surfaces": "REST serves them now, completed webhooks arrive in wave 1 and server SDKs in wave 2."
},
{
"audience": "agents",
"writer": "An agent writing its own calls.",
"surfaces": "Nothing serves them yet; MCP stays parked while agent demand is limited to forum questions."
}
],
"sequence": [
{
"surface": "webhooks",
"wave": 1,
"why": "A reseller paused its rollout because payment.failed events still need a manual export, a blocked deal that outranks every other signal here."
},
{
"surface": "sdks",
"wave": 2,
"why": "Telemetry shows 71% of REST requests come from a hand-rolled Node client, and the SDKs are generated only after the spec stops drifting."
}
],
"groundwork": [
"Make the Ledgerline machine-readable spec the linted source of truth in CI so it stops drifting from the API."
],
"caveats": [
"The draft puts MCP first on three forum questions; it stays deferred until RFPs or observed agent traffic appear.",
"The draft builds MCP and server SDKs together against a capacity of one; webhooks takes wave 1 alone and SDKs follow in wave 2.",
"The draft schedules GraphQL next with no signal and no named pain; it is declined.",
"Server SDKs generated from today's drifting spec would bake the drift into a high-tier contract, so the spec fix comes first."
]
}
What step 5 prints for it: webhooks: 1 | sdks: 2
The page's check: kept - nothing to flag
handoffs - Ledgerline - MCP first on customer questions
The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does. The page's plan: wire, wireShown, wireNote, reply, shown, cuts, status, notes, carried, gist.
Input (facts is 3612 characters; the first 1399 are shown):
{
"task": "handoffs",
"platform": "Platform: Ledgerline\nWhat it does: An invoicing and payment-reminder API that developers at small SaaS companies wire into their billing.\nBuyer: a developer picks the tool bottom-up; time to value: days acceptable\nSpec: exists but drifts from the API; per-client telemetry: instrumented; agent demand: customer questions only\nCapacity: 1 surface at once; owners available: One API team of four engineers\nDeadline: no hard date; a compounding surface roadmap; effort ceiling: medium\nGraphQL: no confirmed pain; plan date: 2026-11-02",
"surfaces": "Surfaces today (surface | depth):\n- REST API | self-serve end to end\n- Webhooks | partial - some steps still manual\n- Server SDKs | absent\n- GraphQL API | absent\n- gRPC API | absent\n- SQL / JDBC access | absent\n- Bulk data / warehouse shares | absent\n- CLI | absent\n- MCP server | absent\n- Embedded components | absent\n- Terraform / OpenTofu provider | absent\n\nDemand signals (surface | kind | evidence):\n- Server SDKs | usage telemetry | 71% of REST requests carry a hand-rolled Node client user-agent\n- Webhooks | pipeline / blocked deal | a reseller paused its rollout because payment.failed events need a manual export\n- MCP server | community ask | three customers asked on the forum whether we support agents\n\nThe user's draft order (surface | when):\n- MCP server | now\n- Server SDKs | now\n- GraphQL API | next",
"carry": "Recommended posture: REST plus webhooks first - Developers at small SaaS companies choose Ledgerline bottom-up, the one blocked deal is a webhooks gap, and telemetry shows most REST traffic coming from hand-rolled Node clients, so the API rung comes first.\nGroundwork: Make the Ledgerline machine-readable spec the linted source of truth in CI so it stops drifting from the API.\nSequence: webhooks (wave 1): A reseller paused its rollout because payment.failed events still need a manual export, a blocked deal that outranks every other signal here.; sdks (wave 2): Telemetry shows 71% of REST requests come from a hand-rolled Node client, and the SDKs are generated only after the spec stops drifting.\nAudiences: firstparty - The self-serve REST API serves them today and stays their surface through the plan.; integrators - REST serves them now, completed webhooks arrive in wave 1 and server SDKs in wave 2.; agents - Nothing serves them yet; MCP stays parked while agent demand is limited to forum questions.",
"facts": "The page's checks (6 fail, 1 warn, 1 pass, 6 n/a); verdict: your draft order breaks the skill's gates:\n- Surfaces and the spec | buyer | na | a developer picks the tool bottom-up; time to value: days acceptable\n- Surfaces and the spec | inventory | na | REST API (self-serve), Webhooks (partial)\n- Surfaces and the spec | spec | fail | exists but drifts from the API - fix it before scheduling any derived surface\n- Surfaces and the spec | depth | fail | Webhooks partial - shallow API-first: a partner discovers the manual step mid-integration\n- Demand signals | telemetry | pass | usage by client type can be read\n- Demand signals | signals | na | Server SDKs: usage telemetry; Webhooks: pipeline / blocked deal; MCP server: community ask\n- Demand signals | community | warn | MCP server - cheap to emit; confirm against pipeline data or telemetry\n- Demand signals | agents | na | customer questions only\n- Your draft order | derivation | fail | MCP server on a spec that is not the source of truth; Server SDKs on a spec that is not the source of truth\n- Your draft order | novelty | fail | MCP leads the draft with customer questions only - novelty-driven MCP-first inverts the order\n- Your draft order | concurrency | fail | 2 surfaces now against a capacity of 1\n- Your draft order | signals | fail | GraphQL API - no signal: surface sprawl without telemetry\n- The page's plan | posture | na |\n\n[... 2213 more characters cut from this display ...]"
}
Reply:
{
"task": "handoffs",
"summary": "Ledgerline lints its drifting spec in CI, finishes webhooks for the paused reseller, then generates server SDKs; MCP stays parked and GraphQL is declined, all owned by the four-engineer API team.",
"handoffs": [
{
"surface": "webhooks",
"decision": "scheduled",
"audience": "Third-party integrators, such as the reseller, whose billing reacts to Ledgerline invoice and payment events.",
"signal": "A reseller paused its rollout because payment.failed events still need a manual export, the one blocked deal on record.",
"tier": "low-medium",
"owner": "The API team of four engineers",
"sibling": "webhook-platform-design",
"next": "Replace the manual export so payment.failed events are delivered end to end, then tell the reseller the rollout can resume."
},
{
"surface": "sdks",
"decision": "scheduled",
"audience": "Developers at small SaaS companies calling Ledgerline from their own language, starting with the Node users who hand-roll clients today.",
"signal": "Usage telemetry shows 71% of REST requests carry a hand-rolled Node client user-agent.",
"tier": "high",
"owner": "The API team of four engineers, once webhooks ship",
"sibling": "sdk-portfolio-strategy",
"next": "Once the spec passes its CI lint against the live API, generate a Node SDK from it before any other language."
},
{
"surface": "mcp",
"decision": "deferred",
"audience": "AI agent clients acting on Ledgerline invoices for customers, an audience nothing serves yet.",
"signal": "Only customer questions: three customers asked on the forum whether Ledgerline supports agents.",
"tier": "medium (today)",
"owner": "The API team of four engineers (proposed, when reopened)",
"sibling": "mcp-server-offering",
"next": "Reopen when an RFP requires MCP compatibility or telemetry shows agent traffic hitting the REST API."
},
{
"surface": "graphql",
"decision": "declined",
"audience": "Client apps that would want client-driven queries over Ledgerline invoices and payments.",
"signal": "None exists: no request, no telemetry and no named pain from integrators.",
"tier": "high",
"owner": "The API team of four engineers (proposed, as the team that would review any reopening)",
"sibling": "public-graphql-api-design",
"next": "Reopen only if integrators name a concrete pain that the REST API cannot solve and confirm it."
}
],
"review": {
"date": "2027-02-01",
"triggers": [
"Another deal or reseller rollout stalls on a missing Ledgerline surface",
"An RFP asks for MCP compatibility or agent traffic shows up in the REST telemetry",
"Telemetry by client type stops showing hand-rolled Node clients as the main REST callers",
"The four-engineer API team loses capacity while webhooks or SDKs are in flight"
],
"recheck": [
"Whether the spec still passes its CI lint against the live API",
"Whether the reseller resumed its rollout after the payment.failed fix",
"The share of REST requests from hand-rolled Node clients versus the generated SDK",
"Any new forum, RFP or traffic evidence of agent demand"
]
},
"caveats": [
"The draft put MCP and server SDKs first at once; the spec lint replaces them, then webhooks alone in wave 1 against a capacity of one.",
"GraphQL in the draft has no signal behind it, so it is declined rather than scheduled next.",
"The three forum asks about agents are unconfirmed; check them against telemetry before MCP moves."
]
}
What step 5 prints for it: webhooks: scheduled | sdks: scheduled | mcp: deferred | graphql: declined
The page's check: kept - nothing to flag
plan - Tallyfold - deals lost over warehouse access
The page read it as: a sequenced plan ready to review; postures rest, embedded, graphql; build order bulk (wave 1), embedded (wave 1), sql (wave 2), mcp (wave 2); 7 of 7 gates; failing checks: none.
Input (facts is 4161 characters; the first 1399 are shown):
{
"task": "plan",
"platform": "Platform: Tallyfold\nWhat it does: A product-analytics platform: developers instrument events, and growth and finance teams read the dashboards.\nBuyer: both in the same deal; time to value: days acceptable\nSpec: linted source of truth in CI; per-client telemetry: instrumented; agent demand: RFPs or security reviews require MCP compatibility\nCapacity: 2 surfaces at once; owners available: Platform API team, Data team, a new Agents pod\nDeadline: this quarter for Bulk data / warehouse shares; a compounding surface roadmap; effort ceiling: high\nGraphQL: no confirmed pain; plan date: 2026-11-02",
"surfaces": "Surfaces today (surface | depth):\n- REST API | self-serve end to end\n- Webhooks | self-serve end to end\n- Server SDKs | self-serve end to end\n- GraphQL API | absent\n- gRPC API | absent\n- SQL / JDBC access | absent\n- Bulk data / warehouse shares | absent\n- CLI | absent\n- MCP server | absent\n- Embedded components | absent\n- Terraform / OpenTofu provider | absent\n\nDemand signals (surface | kind | evidence):\n- Bulk data / warehouse shares | pipeline / blocked deal | lost two enterprise deals this year over missing Snowflake warehouse access\n- MCP server | pipeline / blocked deal | two RFPs this quarter list MCP compatibility as required\n- GraphQL API | community ask | 40 upvotes on a forum thread asking for GraphQL\n- SQL / JDBC access | usage telemetry | 18% of API calls are paginated full-table pulls feeding BI tools\n\nThe user's draft order (surface | when):\n- Bulk data / warehouse shares | now\n- SQL / JDBC access | next\n- MCP server | later",
"carry": "",
"facts": "The page's checks (0 fail, 1 warn, 7 pass, 6 n/a); verdict: a sequenced plan ready to review:\n- Surfaces and the spec | buyer | na | both in the same deal - an economic buyer and a technical user; time to value: days acceptable\n- Surfaces and the spec | inventory | na | REST API (self-serve), Webhooks (self-serve), Server SDKs (self-serve)\n- Surfaces and the spec | spec | pass | linted in CI - SDKs, MCP and generated docs can derive from it\n- Surfaces and the spec | depth | pass | 3 surfaces self-serve\n- Demand signals | telemetry | pass | usage by client type can be read\n- Demand signals | signals | na | Bulk data / warehouse shares: pipeline / blocked deal; MCP server: pipeline / blocked deal; GraphQL API: community ask; SQL / JDBC access: usage telemetry\n- Demand signals | community | warn | GraphQL API - cheap to emit; confirm against pipeline data or telemetry\n- Demand signals | agents | na | RFPs or security reviews require MCP compatibility\n- Your draft order | derivation | pass | every derived surface follows its contract\n- Your draft order | novelty | pass | MCP does not lead\n- Your draft order | concurrency | pass | 1 surface now against a capacity of 2\n- Your draft order | signals | pass | each drafted surface has a signal\n- The page's plan | posture | na | REST plus webhooks first > Embedded-first, on an API foundation > GraphQL-led\n- The page's plan | order | na |\n\n[... 2762 more characters cut from this display ...]"
}
Reply:
{
"task": "plan",
"summary": "Recommend rest, already in place: Tallyfold starts warehouse shares now to meet this quarter's deadline after two lost Snowflake deals, with the embedded layer in the same wave.",
"candidates": [
{
"posture": "rest",
"sequence": "Build on the self-serve REST, webhooks and SDKs by shipping warehouse shares and embedded dashboards in wave 1, then SQL access and an MCP layer generated from REST in wave 2.",
"tradeoff": "It holds the team to two surfaces at a time on the linted spec, and buys the Snowflake deals and the MCP RFPs without reopening the existing contract."
},
{
"posture": "embedded",
"sequence": "Lead with embedded dashboards for growth and finance readers on the existing API, then add warehouse shares, SQL access and MCP.",
"tradeoff": "It gives the economic buyer value first but risks missing this quarter's warehouse-share deadline that two lost enterprise deals set."
},
{
"posture": "deal",
"sequence": "Pair the API foundation for the developers who instrument events with an embedded layer for the growth and finance buyer, both in wave 1 next to warehouse shares.",
"tradeoff": "It serves both sides of the same deal at once, at the cost of filling both capacity slots in wave 1 and pushing SQL and MCP to wave 2."
}
],
"recommend": {
"posture": "rest",
"why": "Developers instrument Tallyfold while growth and finance teams buy, REST, webhooks and SDKs are already self-serve on a linted spec, and the blocked deals point at data access built on that foundation."
},
"audiences": [
{
"audience": "firstparty",
"writer": "A person on a customer's engineering team instrumenting events.",
"surfaces": "The self-serve REST API serves them today and throughout the plan."
},
{
"audience": "integrators",
"writer": "A person or a code generator at a partner.",
"surfaces": "REST, webhooks and server SDKs already serve them self-serve, with no change in the plan."
},
{
"audience": "operators",
"writer": "Nobody; growth and finance staff configure dashboards rather than code.",
"surfaces": "Nothing serves them outside the product today; embedded components arrive in wave 1."
},
{
"audience": "bi",
"writer": "A person writing SQL, or a BI tool.",
"surfaces": "Today they page through REST in full-table pulls; warehouse shares arrive in wave 1 and SQL access in wave 2."
},
{
"audience": "agents",
"writer": "An agent writing its own calls.",
"surfaces": "Nothing serves them yet; an MCP server generated from the REST API arrives in wave 2."
}
],
"sequence": [
{
"surface": "bulk",
"wave": 1,
"why": "Two enterprise deals were lost this year over missing Snowflake warehouse access, and the deadline for warehouse shares is this quarter."
},
{
"surface": "embedded",
"wave": 1,
"why": "The deal pairs a technical user with a growth and finance buyer, so the embedded layer goes on the API foundation in the same wave."
},
{
"surface": "sql",
"wave": 2,
"why": "Telemetry shows 18% of API calls are paginated full-table pulls feeding BI tools."
},
{
"surface": "mcp",
"wave": 2,
"why": "Two RFPs this quarter require MCP compatibility, so a thin layer generated from the linted REST spec comes last."
}
],
"groundwork": [],
"caveats": [
"The draft order leaves embedded components out; they join warehouse shares in wave 1 for the economic buyer.",
"GraphQL stays deferred: 40 forum upvotes are not an integrator-confirmed pain.",
"Any per-call charge for agent use of the MCP server is speculative; the RFPs ask for compatibility, not a paid tier."
]
}
What step 5 prints for it: bulk: 1 | embedded: 1 | sql: 2 | mcp: 2
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./sokit.js"); require("./soreply.js")), then:
const K = SurfaceKit, R = SurfaceReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", spec: "drifts", agents: "asks", capacity: "1",
d_rest: "selfserve", signals: "SDKs | telemetry | 60% of calls from one hand-rolled client",
draft: "MCP server | now\nSDKs | now" }));
// f.posture (the ranked postures), f.planned (groundwork, the build order in waves), f.audiences, f.gates
// K.gateMd(f), K.orderCsv(f) - the gate sheet and the build-order sheet
const b = K.buildInput(f, "plan"); // or "handoffs" (R.laneForm(form, "handoffs", plan) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// 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://surface-order.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://surface-order.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"]
// surface-order.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://surface-order.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://surface-order.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
}
// Surface Order.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 Surface Order {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://surface-order.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://surface-order.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://surface-order.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://surface-order.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":"surface-order"}' | 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": "surface-order"})["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: "surface-order" })).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": "surface-order"}, 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", "surface-order"), 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: "surface-order" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "surface-order"])["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 = "surface-order" }))!["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": "plan", "platform": "Platform: Ledgerline
What it does: ...", "surfaces": "Surfaces today (surface | depth):
- REST API | self-serve end to end ...", "carry": "", "facts": "The page's checks (...); verdict: your draft order breaks the skill's gates:
- Your draft order | novelty | 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": "plan", "platform": "Platform: Ledgerline
What it does: ...", "surfaces": "Surfaces today (surface | depth):
- REST API | self-serve end to end ...", "carry": "", "facts": "The page's checks (...); verdict: your draft order breaks the skill's gates:
- Your draft order | novelty | 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: "plan" or "handoffs", platform, surfaces, 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 surface-order:<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 plan reply carries summary, candidates, recommend, audiences, sequence, groundwork and caveats; a handoffs reply carries summary, handoffs, review and caveats. The code below prints the gist of either: each scheduled surface and its wave, or each surface and its decision.
KEY="surface-order:$(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 == "handoffs" then (.handoffs[] | "\(.surface): \(.decision)")
else (.sequence[] | "\(.surface): wave \(.wave)") end' reply.json
import hashlib, time
key = "surface-order:" + 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"] == "handoffs":
for x in reply["handoffs"]:
print(x["surface"], x["decision"])
else:
for x in reply["sequence"]:
print(x["surface"], x["wave"])
import { createHash } from "node:crypto";
const key = `surface-order:${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 === "handoffs") { for (const x of reply.handoffs) console.log(x.surface, x.decision); }
else { for (const x of reply.sequence) console.log(x.surface, x.wave); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "surface-order:" + 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, a, b := "sequence", "surface", "wave"
if reply["task"] == "handoffs" {
key, a, b = "handoffs", "surface", "decision"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx[a], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "surface-order:" + 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("handoffs")) {
for (JsonNode x : reply.path("handoffs")) System.out.println(x.path("surface").asText() + " " + x.path("decision").asText());
} else {
for (JsonNode x : reply.path("sequence")) System.out.println(x.path("surface").asText() + " " + x.path("wave").asText());
}
require "digest"
key = "surface-order:#{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"] == "handoffs"
reply["handoffs"].each { |x| puts "#{x["surface"]} #{x["decision"]}" }
else
reply["sequence"].each { |x| puts "#{x["surface"]} #{x["wave"]}" }
end
$key = "surface-order:" . $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"] === "handoffs") { foreach ($reply["handoffs"] as $x) echo $x["surface"], " ", $x["decision"], "\n"; }
else { foreach ($reply["sequence"] as $x) echo $x["surface"], " ", $x["wave"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"surface-order:{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 == "handoffs") { foreach (var x in reply["handoffs"]!.AsArray()) Console.WriteLine($"{x!["surface"]} {x["decision"]}"); }
else { foreach (var x in reply["sequence"]!.AsArray()) Console.WriteLine($"{x!["surface"]} {x["wave"]}"); }
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: surface-order:$(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": "plan", "platform": "Platform: Ledgerline
What it does: ...", "surfaces": "Surfaces today (surface | depth):
- REST API | self-serve end to end ...", "carry": "", "facts": "The page's checks (...); verdict: your draft order breaks the skill's gates:
- Your draft order | novelty | 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": "surface-order:" + 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": `surface-order:${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", "surface-order:"+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", "surface-order:" + 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" => "surface-order:#{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: surface-order:" . 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", $"surface-order:{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);