Run Fee Stack 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 merchant-of-record posture, the fee instruments, the waivers and the payout cadence, computes the sale-to-bank days and the per-developer economics, and audits the tax split and the enforcement rules, then hands it all to the run as facts. The run writes the monetization design or the developer fee page; the page then holds it to its own facts (fsreply.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":"fee-stack"}), 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, marketplace, numbers, facts) is missing. The page always sends all five of task, marketplace, numbers, carry, facts; carry is an empty string for design. |
| 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 - the design the platform team implements, then the page developers read:
design- the monetization design: the merchant-of-record posture with tax, chargeback and payout ownership, the rails bundle, the fee instruments with collection and enforcement, the pricing menu the rails can bill, the waivers, the payout cadence and timeline, the tax split and the enforcement and notice.page- the developer-facing fees and payouts page: the fees with only amounts the input gives, the honest sale-to-bank days, who handles which tax, the waiver and the notice. Needs a decided take rate.
A missing or unknown task is answered as design. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the design's decisions into carry for the page lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | design or page. If it is missing or neither, do design and set "task": "design". |
marketplace | yes | the marketplace, its take rate, the interview's answers and what it does today; "not given" means not given. |
numbers | yes | the pricing models against the rails, the payout days, the per-developer economics, the tax rows as jurisdiction | tax | owner | registered, and the page's issues in fix order. |
carry | send it (empty for design) | page task only: the design's decisions, when the design task ran first; may be empty. |
facts | yes | the page's checks, one line each as area | id | status | evidence, and the ranked menus. 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/app-marketplace-monetization-model - the only standard the run applies:
- Merchant of record decides everything else - tax liability, chargeback ownership, the pricing models developers can offer, how payouts work. Developer-billed is right only while the take rate is zero; the moment a rate is charged, marketplace-billed collects at source, the only mechanism that does not depend on developers reporting their own revenue. Configurable per app exists only for a legacy developer-billed population that cannot migrate in one cycle, as a closing path.
- Fee instruments: a revenue share netted at source before payout; flat program fees only where they price a real cost or filter low-intent submissions; processing cost as a separately labeled line, never buried in the share; an audited self-reported share only when a developer-billed posture forces it.
- Waivers: a revenue-exemption tier anchored at the $1M the sourced programs cluster around, annual reset or lifetime decided explicitly, with eligible developers enrolled automatically - an opt-in waiver changes paper, not behavior. Behavior-based discounts pay for a named choice; migration incentives exist only for a measurable back-book.
- Payouts: a monthly cycle plus an explicit hold window by default; ride an existing invoice cycle when one exists; rolling payouts with reserves when competing for supply; instant payouts only as an option on top. Publish the honest end-to-end sale-to-bank timeline - the hold window is the lever developers feel, not the minimum threshold.
- Tax: marketplace-facilitator statutes (US sales tax) and the EU deemed-supplier rule (B2C VAT) attach to the platform by law, whatever the billing posture. Publish a jurisdiction-by-jurisdiction split, never a single "we handle tax" or "you handle tax". Automatic tax calculation with no registration collects nothing and cannot be corrected later. None of this is tax advice - the split goes through qualified counsel before it ships.
- Enforcement: every percentage instrument needs an anti-circumvention rule backed by a delisting path, and a written notice period for fee changes - six months, unfavorable changes deferred, favorable ones accelerated.
House rules
- Use only the input. Never invent a figure, a fee amount, a rate, a threshold, a number of days, a jurisdiction, a vendor or a platform name. An amount the input does not give is "to be set".
- The page's rankings, its timeline days and its tax owners are settled: never move them.
- Never claim tax is handled by one side everywhere, and never present the tax split as legal advice - say it goes through counsel.
- No URL at all.
- Write in the language of the input; keep the JSON keys and the ids (posture, instrument, waiver, cadence and model ids) as given.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
design
Write the monetization design the platform team implements, in the skill's order.
summary- one sentence, at most 40 words: the posture, how the cut is collected, and the first thing to change.posture-rung(marketplace,developerorconfigurable, exactly as the facts rank it),tax,chargebacks,payouts(one sentence each: who owns it under this posture, naming the platform or developers).rails- one sentence: the account bundle (marketplace-shaped or SaaS-shaped) and what it means for the charge, the losses and the developer's dashboard.instruments- one object per instrument the facts list, in their order:instrument(share,program,processingoraudited),collection(one sentence: how it is collected),enforcement(one sentence: what stops it being avoided). An empty list at zero take.pricing- one object per model in the input:model(flat,seat,usage,onetimeorfreemium),offered(yesonly when developers want it and the rails support it, elseno),note(one sentence).waivers- one object per waiver shape the facts list, in their order:shape(exemption,behaviorormigration),rule(one sentence: the threshold or the choice it pays for, and automatic enrollment).payout-cadence(monthly,consolidated,rollingornoneas the facts rank it),hold(one sentence),timeline(one sentence stating the longest sale-to-bank days exactly as the facts give them; when the cadence isnone, say developers are paid by their own customers).tax- one object per tax row in the "must:" line, in its order:jurisdiction,tax,owner(platformordeveloper, exactly as the must line gives it).enforcement-circumvention(one sentence naming the delisting path),notice(one sentence: at least six months, unfavorable changes deferred). At zero take, say what changes when a rate is first charged.caveats- at most 4 short strings, always including that the tax split goes through counsel when there are tax rows;[]otherwise.
{
"task": "design",
"summary": "one sentence",
"posture": { "rung": "marketplace", "tax": "one sentence", "chargebacks": "one sentence", "payouts": "one sentence" },
"rails": "one sentence",
"instruments": [ { "instrument": "share", "collection": "one sentence", "enforcement": "one sentence" } ],
"pricing": [ { "model": "flat", "offered": "yes", "note": "one sentence" } ],
"waivers": [ { "shape": "exemption", "rule": "one sentence" } ],
"payout": { "cadence": "monthly", "hold": "one sentence", "timeline": "one sentence" },
"tax": [ { "jurisdiction": "United States", "tax": "sales tax", "owner": "platform" } ],
"enforcement": { "circumvention": "one sentence", "notice": "one sentence" },
"caveats": [ ]
}
The page holds it to its own facts (check() in fsreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; a summary of at most 40 words; no percentage the input never gives; no single tax toggle; the ranked posture with tax, chargebacks and payouts each owned; the instruments, waivers and payout cadence exactly as ranked and in order; every pricing model listed, none offered that the rails cannot bill; the longest sale-to-bank days in the timeline; every tax row with the owner law and posture require; a delisting path and at least six months' notice when a rate is charged; a caveat sending the tax split to counsel.
page
Write the developer-facing fees and payouts page the marketplace publishes - plain, specific, in the second person ("you").
summary- one sentence, at most 40 words: what the page commits to.title- a short page title.intro- two sentences: who bills your customers and how you get paid.fees- objects:fee(a short name),amount(only an amount the input gives - the take rate as a percentage, the processing percentage - else "to be set"),when(one sentence: when it is charged or deducted). Include the revenue share when the take rate is above zero, one row per tier the input states; never at zero take.payouts- two or three sentences: the cadence, the hold, the minimum payout when the input gives one, and the honest longest sale-to-bank days exactly as the facts give them; or, when developers bill their own customers, that you are paid by them directly.tax- one object per tax row in the "must:" line, in its order:jurisdiction,tax,who(platformordeveloper, exactly as the must line gives it).waiver- one or two sentences: the threshold and that eligible developers are enrolled automatically; "None" when there is no waiver.notice- one sentence: fee changes come with the notice the facts give in months, in writing.caveats- at most 4 short strings for the platform team (not shown to developers), including that the tax split goes through counsel;[]when there is nothing to say.
{
"task": "page",
"summary": "one sentence",
"title": "Fees and payouts",
"intro": "two sentences",
"fees": [ { "fee": "Revenue share", "amount": "15%", "when": "one sentence" } ],
"payouts": "two or three sentences",
"tax": [ { "jurisdiction": "United States", "tax": "sales tax", "who": "platform" } ],
"waiver": "one or two sentences",
"notice": "one sentence",
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; a revenue-share fee at the take rate (none at zero take); every amount one the input gives, else to be set; the longest sale-to-bank days, or that developers are paid by their own customers; every tax row with its required owner; the notice in months; the waiver's threshold with automatic enrollment; a caveat sending the tax split to counsel.
Worked examples
Three runs from the page's free samples (invented marketplaces): the design for a marketplace charging 20% while developers still bill, the developer page for the same marketplace carrying that design, and the design for on-invoice billing with a tax hole. 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.
design - A 20% cut developers still collect themselves
The page read it as: money leaks or obligations nobody holds; 20% take; posture marketplace; longest sale to bank 80 days; 1 of 5 gates; failing checks: posture, timeline, taxsplit, facilitator, circumvention, notice, ownership, instrumentsGate, publishedGate, taxsafeGate.
Input (numbers is 1463 characters; the first 1391 are shown; facts is 3813 characters; the first 1396 are shown):
{
"task": "design",
"marketplace": "Marketplace: Kestrova\nWhat it sells and to whom: A field-service management platform; partners sell scheduling, parts and invoicing apps to contractor businesses.\nTake rate: 20%; tiers: 20% on all paid-app revenue; decided: yes\nBills these customers on a recurring cycle already: no; billing engineering: A managed payment-rails provider; buyers: US and the EU; consumer buyers: no; losses carried by: the platform; developer-billed apps that cannot migrate in one cycle: no\nToday: Developers bill their own customers; payouts Monthly cycle plus a hold window; processing shown separately: no; anti-circumvention rule: no; delisting path: no; notice for fee changes: 3 months\nWaiver today: annual threshold $500k, enrollment manual; a choice to steer developers toward: no; off-marketplace revenue to migrate: no; small developers bound by cash flow: no; competing for supply with faster payers: no\nDeadline: no; a compounding asset; effort ceiling: medium",
"numbers": "Pricing models (model | developers want it | rails support it):\n- Flat subscription | yes | yes\n- Per-seat | yes | yes\n- Usage-based | no | NO\n- One-time | yes | yes\nPayout days: cycle 30, hold 45, bank 5 - sale to bank 50 to 80 days; published 30 days; minimum payout $100\nPer active developer a month: sales $4,200, cut $840, rails fees $2.25 (account $2.00 + 1 payouts x $0.25), net $838 across 180 developers: $151k; processing 2.9%\nTax split (jurisdiction | tax | owner | registered):\n- United States | sales tax | DEVELOPER | NO\n- EU | VAT | UNASSIGNED | not given\n- United Kingdom | VAT | DEVELOPER | yes\nIssues in fix order: posture - developers bill their own customers while the marketplace charges 20% - promote to marketplace-billed and collect at source; circumvention - no rule against routing billing around the marketplace - circumvention pressure rises with the rate; notice - 3 months of notice - the sourced pattern is six, with unfavorable changes deferred; processing - processing cost sits inside the share - the share reads as a higher take than it is; optin - eligible developers must enroll - a qualifying developer who misses the paperwork silently pays the full rate; default them in; timeline - you publish 30 days; a sale can take 80 (cycle 30 + hold 45 + bank 5); taxsplit - EU VAT - nobody named; facilitator - United States sales tax - marketplace-facilitator\n\n[... 72 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (10 fail, 2 warn, 3 pass, 3 n/a); verdict: money leaks or obligations nobody holds:\n- The four menus | strategy | pass | 20% - 20% on all paid-app revenue\n- The four menus | posture | fail | Marketplace-billed (platform as merchant of record) - charging 20% - collect at source; only the marketplace-billed posture does not depend on developers reporting their own revenue; no legacy population - the configurable posture leaves the discussion; today: developers bill their own customers while the marketplace charges 20% - promote to marketplace-billed and collect at source\n- The four menus | instruments | na | Revenue share netted at source > Flat program fees (registration, review, listing) > Processing pass-through, a separate labeled line - marketplace-billed - the audited self-reported share is deleted\n- The four menus | waivers | warn | Revenue-exemption tier > Behavior-based discount - no back-book to migrate - the migration incentive is deleted; a $500k threshold - the sourced programs cluster at $1M; eligible developers must enroll - a qualifying developer who misses the paperwork silently pays the full rate; default them in\n- The four menus | cadence | na | Monthly cycle plus a hold window > Rolling payouts with reserves - no existing invoice cycle - consolidation is deleted\n- The four menus | menu | pass | Flat subscription, Per-seat, One-time - each\n\n[... 2417 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "design",
"summary": "Move Kestrova to marketplace-billed so the platform is merchant of record and nets the 20% share at source; first, take billing off developers, who bill their own customers today while the marketplace charges 20%.",
"posture": {
"rung": "marketplace",
"tax": "The platform owns collection and remittance where the law puts it on the marketplace, including US sales tax and EU VAT, while developers keep United Kingdom VAT.",
"chargebacks": "The platform owns chargebacks and disputes as merchant of record, which matches the losses it already carries today.",
"payouts": "The platform pays developers their net earnings after the share and processing are deducted, instead of developers collecting from their own customers."
},
"rails": "Use a marketplace-shaped account bundle on the managed payment-rails provider, so the platform charges the contractor business, carries refunds and disputes, and each developer sees sales, deductions and pending payouts in a [... 81 more characters cut from this display]",
"instruments": [
{
"instrument": "share",
"collection": "The 20% revenue share on all paid-app revenue is deducted from each sale before the payout is calculated, so nothing depends on developers reporting their own revenue.",
"enforcement": "A written anti-circumvention rule forbids routing billing for a listed app around the marketplace, with warning, suspension and delisting as the path for repeat breaches."
},
{
"instrument": "program",
"collection": "Any registration, review or listing fee, amount to be set, is charged only where it prices a real review cost or filters low-intent submissions, and is invoiced at submission.",
"enforcement": "Listing review is gated on the fee being paid, so an unpaid submission never reaches the catalog."
},
{
"instrument": "processing",
"collection": "Processing at 2.9% is deducted as its own labeled line on every payout statement, taken out of the share so the share reads as the true 20% take.",
"enforcement": "Because it is netted at source alongside the share, it falls under the same anti-circumvention rule and delisting path."
}
],
"pricing": [
{
"model": "flat",
"offered": "yes",
"note": "Developers want flat subscriptions and the rails support them, so offer them at launch."
},
{
"model": "seat",
"offered": "yes",
"note": "Per-seat pricing fits contractor teams and the rails support it, so offer it at launch."
},
{
"model": "usage",
"offered": "no",
"note": "The rails do not support usage-based billing and developers have not asked for it, so leave it off the menu."
},
{
"model": "onetime",
"offered": "yes",
"note": "One-time purchases are wanted and supported, so offer them for apps sold without a subscription."
}
],
"waivers": [
{
"shape": "exemption",
"rule": "Raise the revenue-exemption threshold from $500k toward the $1M the sourced programs cluster at, keep the annual reset, and enroll every eligible developer automatically instead of requiring them to apply."
},
{
"shape": "behavior",
"rule": "Hold a behavior-based discount, rate to be set, for a named choice the platform wants to steer developers toward; none is named today, so it stays unused until one is chosen, and qualifying developers are enrolled automatically."
}
],
"payout": {
"cadence": "monthly",
"hold": "Keep the monthly cycle with an explicit 45-day hold window, and treat the hold as the lever to shorten if developers push back, not the $100 minimum payout.",
"timeline": "Publish the honest timeline: a sale can take up to 80 days to reach the developer's bank (cycle 30 + hold 45 + bank 5), not the 30 days published today."
},
"tax": [
{
"jurisdiction": "United States",
"tax": "sales tax",
"owner": "platform"
},
{
"jurisdiction": "EU",
"tax": "VAT",
"owner": "platform"
},
{
"jurisdiction": "United Kingdom",
"tax": "VAT",
"owner": "developer"
}
],
"enforcement": {
"circumvention": "Add a rule that a listed app may not move billing for marketplace customers off the marketplace, backed by a written delisting path of warning, suspension and removal from the catalog.",
"notice": "Raise fee-change notice from 3 months to at least six months in writing, with unfavorable changes deferred to the end of the notice and favorable ones applied early."
},
"caveats": [
"The jurisdiction-by-jurisdiction tax split goes through qualified counsel before it ships; it is not tax advice.",
"US sales tax registration is not in place today; register before collecting, since calculation without registration collects nothing.",
"Developers who bill their own customers today need a migration plan to marketplace billing.",
"Program fee and behavior discount amounts are to be set."
]
}
Cut from this display only (the saved reply and the check use the full text): rails (305 characters, 224 shown).
What step 5 prints for it: share | program | processing | marketplace
The page's check: kept - nothing to flag
page - A 20% cut developers still collect themselves
The input carries the saved design (R.laneForm fills carry from the design reply), as the page's handoff does. The issues in fix order: posture, circumvention, notice, processing, optin, timeline, taxsplit, facilitator.
Input (numbers is 1463 characters; the first 1391 are shown; facts is 3748 characters; the first 1396 are shown; carry is 2214 characters; the first 1399 are shown):
{
"task": "page",
"marketplace": "Marketplace: Kestrova\nWhat it sells and to whom: A field-service management platform; partners sell scheduling, parts and invoicing apps to contractor businesses.\nTake rate: 20%; tiers: 20% on all paid-app revenue; decided: yes\nBills these customers on a recurring cycle already: no; billing engineering: A managed payment-rails provider; buyers: US and the EU; consumer buyers: no; losses carried by: the platform; developer-billed apps that cannot migrate in one cycle: no\nToday: Developers bill their own customers; payouts Monthly cycle plus a hold window; processing shown separately: no; anti-circumvention rule: no; delisting path: no; notice for fee changes: 3 months\nWaiver today: annual threshold $500k, enrollment manual; a choice to steer developers toward: no; off-marketplace revenue to migrate: no; small developers bound by cash flow: no; competing for supply with faster payers: no\nDeadline: no; a compounding asset; effort ceiling: medium",
"numbers": "Pricing models (model | developers want it | rails support it):\n- Flat subscription | yes | yes\n- Per-seat | yes | yes\n- Usage-based | no | NO\n- One-time | yes | yes\nPayout days: cycle 30, hold 45, bank 5 - sale to bank 50 to 80 days; published 30 days; minimum payout $100\nPer active developer a month: sales $4,200, cut $840, rails fees $2.25 (account $2.00 + 1 payouts x $0.25), net $838 across 180 developers: $151k; processing 2.9%\nTax split (jurisdiction | tax | owner | registered):\n- United States | sales tax | DEVELOPER | NO\n- EU | VAT | UNASSIGNED | not given\n- United Kingdom | VAT | DEVELOPER | yes\nIssues in fix order: posture - developers bill their own customers while the marketplace charges 20% - promote to marketplace-billed and collect at source; circumvention - no rule against routing billing around the marketplace - circumvention pressure rises with the rate; notice - 3 months of notice - the sourced pattern is six, with unfavorable changes deferred; processing - processing cost sits inside the share - the share reads as a higher take than it is; optin - eligible developers must enroll - a qualifying developer who misses the paperwork silently pays the full rate; default them in; timeline - you publish 30 days; a sale can take 80 (cycle 30 + hold 45 + bank 5); taxsplit - EU VAT - nobody named; facilitator - United States sales tax - marketplace-facilitator\n\n[... 72 more characters cut from this display ...]",
"carry": "Posture: marketplace - tax: The platform owns collection and remittance where the law puts it on the marketplace, including US sales tax and EU VAT, while developers keep United Kingdom VAT. Chargebacks: The platform owns chargebacks and disputes as merchant of record, which matches the losses it already carries today. Payouts: The platform pays developers their net earnings after the share and processing are deducted, instead of developers collecting from their own customers.\nInstruments: share (The 20% revenue share on all paid-app revenue is deducted from each sale before the payout is calculated, so nothing depends on developers reporting their own revenue.); program (Any registration, review or listing fee, amount to be set, is charged only where it prices a real review cost or filters low-intent submissions, and is invoiced at submission.); processing (Processing at 2.9% is deducted as its own labeled line on every payout statement, taken out of the share so the share reads as the true 20% take.)\nPricing offered: flat, seat, onetime\nWaivers: exemption - Raise the revenue-exemption threshold from $500k toward the $1M the sourced programs cluster at, keep the annual reset, and enroll every eligible developer automatically instead of requiring them to apply.; behavior - Hold a behavior-based discount, rate to be set, for a named choice the platform wants to steer developers\n\n[... 815 more characters cut from this display ...]",
"facts": "The page's checks (10 fail, 2 warn, 3 pass, 3 n/a); verdict: money leaks or obligations nobody holds:\n- The four menus | strategy | pass | 20% - 20% on all paid-app revenue\n- The four menus | posture | fail | Marketplace-billed (platform as merchant of record) - charging 20% - collect at source; only the marketplace-billed posture does not depend on developers reporting their own revenue; no legacy population - the configurable posture leaves the discussion; today: developers bill their own customers while the marketplace charges 20% - promote to marketplace-billed and collect at source\n- The four menus | instruments | na | Revenue share netted at source > Flat program fees (registration, review, listing) > Processing pass-through, a separate labeled line - marketplace-billed - the audited self-reported share is deleted\n- The four menus | waivers | warn | Revenue-exemption tier > Behavior-based discount - no back-book to migrate - the migration incentive is deleted; a $500k threshold - the sourced programs cluster at $1M; eligible developers must enroll - a qualifying developer who misses the paperwork silently pays the full rate; default them in\n- The four menus | cadence | na | Monthly cycle plus a hold window > Rolling payouts with reserves - no existing invoice cycle - consolidation is deleted\n- The four menus | menu | pass | Flat subscription, Per-seat, One-time - each\n\n[... 2352 more characters cut from this display ...]"
}
Reply:
{
"task": "page",
"summary": "Kestrova bills your customers, deducts a 20% revenue share and 2.9% processing as separate lines, pays you monthly after a 45-day hold within 80 days of a sale, and gives six months' written notice of fee changes.",
"title": "Fees and payouts",
"intro": "Kestrova bills the contractor businesses that buy your app, as merchant of record, and handles refunds and disputes. We deduct our fees from each sale and pay the rest into your bank account on a monthly cycle.",
"fees": [
{
"fee": "Revenue share",
"amount": "20%",
"when": "Deducted from each paid-app sale before your payout is calculated."
},
{
"fee": "Payment processing",
"amount": "2.9%",
"when": "Deducted from each sale and shown as its own line on your payout statement, separate from the revenue share."
},
{
"fee": "Registration, review or listing fee",
"amount": "to be set",
"when": "Charged when you submit an app for review, if one applies."
}
],
"payouts": "We pay out once a month, after a 45-day hold window on each sale. Counting the 30-day cycle, the 45-day hold and up to 5 days for your bank, a sale can take up to 80 days to reach your account. The minimum payout is $100.",
"tax": [
{
"jurisdiction": "United States",
"tax": "sales tax",
"who": "platform"
},
{
"jurisdiction": "EU",
"tax": "VAT",
"who": "platform"
},
{
"jurisdiction": "United Kingdom",
"tax": "VAT",
"who": "developer"
}
],
"waiver": "If your paid-app revenue stays under the $500k annual threshold, the revenue share is waived, and the threshold resets each year. You are enrolled automatically when you qualify; there is nothing to apply for.",
"notice": "We give you at least 6 months' written notice before any fee change, and changes that cost you more take effect only after that notice.",
"caveats": [
"The tax split goes through qualified counsel before this page ships; it is not tax advice.",
"Register for US sales tax before collecting, since it is not registered today.",
"Decide whether to raise the $500k threshold toward $1M before publishing, and set or drop the program fee.",
"Publish the anti-circumvention rule and its delisting path alongside this page."
]
}
What step 5 prints for it: Revenue share 20% | Payment processing 2.9% | Registration, review or listing fee to be set
The page's check: kept - nothing to flag
design - On-invoice billing with a tax hole
The page read it as: money leaks or obligations nobody holds; 15% take; posture marketplace; longest sale to bank 39 days; 4 of 5 gates; failing checks: menu, economics, silent, taxsafeGate.
Input (facts is 3758 characters; the first 1394 are shown):
{
"task": "design",
"marketplace": "Marketplace: Pixmoor\nWhat it sells and to whom: A design and creative-tools platform; developers sell plugins to studios and to individual creators on the platform's own monthly invoice.\nTake rate: 15%; tiers: 0% on the first $1M lifetime, 15% above; decided: yes\nBills these customers on a recurring cycle already: yes; billing engineering: A managed payment-rails provider; buyers: US, the EU and beyond; consumer buyers: yes; losses carried by: the platform; developer-billed apps that cannot migrate in one cycle: no\nToday: The marketplace bills; payouts Rolling payouts with reserves; processing shown separately: yes; anti-circumvention rule: yes; delisting path: yes; notice for fee changes: 6 months\nWaiver today: lifetime threshold $1M, enrollment auto; a choice to steer developers toward: yes; off-marketplace revenue to migrate: no; small developers bound by cash flow: yes; competing for supply with faster payers: yes\nDeadline: no; a compounding asset; effort ceiling: high",
"numbers": "Pricing models (model | developers want it | rails support it):\n- Flat subscription | yes | yes\n- Usage-based | yes | NO\n- One-time | yes | yes\n- Free with paid tiers | yes | yes\nPayout days: cycle 7, hold 30, bank 2 - sale to bank 32 to 39 days; published 39 days; minimum payout $50.00\nPer active developer a month: sales $15.00, cut $2.25, rails fees $3.08 (account $2.00 + 4.33 payouts x $0.25), net -$0.83 across 2400 developers: -$1,998; processing 2.9%\nTax split (jurisdiction | tax | owner | registered):\n- United States | sales tax | PLATFORM | yes\n- EU | VAT (B2C) | PLATFORM | NO\n- United Kingdom | VAT | PLATFORM | yes\n- Australia | GST | DEVELOPER | not given\nIssues in fix order: economics - per active developer each month the cut is $2.25 and the rails' flat fees are $3.08 - below $20.55 of monthly sales a developer costs more than it pays; menu - Usage-based - wanted, but the rails cannot bill it; verify before publishing the menu; silent - EU VAT (B2C) - automatic calculation with no registration collects $0, raises no error and cannot be corrected later",
"carry": "",
"facts": "The page's checks (4 fail, 0 warn, 10 pass, 4 n/a); verdict: money leaks or obligations nobody holds:\n- The four menus | strategy | pass | 15% - 0% on the first $1M lifetime, 15% above\n- The four menus | posture | na | Marketplace-billed (platform as merchant of record) - charging 15% - collect at source; only the marketplace-billed posture does not depend on developers reporting their own revenue; no legacy population - the configurable posture leaves the discussion; you already bill these customers on a cycle - consolidate app charges onto that invoice\n- The four menus | instruments | na | Revenue share netted at source > Flat program fees (registration, review, listing) > Processing pass-through, a separate labeled line - marketplace-billed - the audited self-reported share is deleted\n- The four menus | waivers | na | Behavior-based discount > Revenue-exemption tier - a specific pricing or architecture choice to steer - a behavior-based discount moves up; no back-book to migrate - the migration incentive is deleted; a $1M threshold - on the sourced anchor\n- The four menus | cadence | na | Rolling payouts with reserves > Invoice-cycle consolidation > Monthly cycle plus a hold window (+ instant as an option) - an existing invoice cycle - payouts ride the settlement you already run; you compete for supply against faster-paying alternatives - rolling payouts with reserves;\n\n[... 2364 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "design",
"summary": "Pixmoor stays marketplace-billed and nets its 15% share at source on its own monthly invoice; first, register for EU VAT (B2C) before collecting it, then fix per-developer economics where flat rails fees exceed the cut.",
"posture": {
"rung": "marketplace",
"tax": "The platform, as merchant of record, owns the sales tax and VAT it is liable for, while developers keep Australia GST under the published split.",
"chargebacks": "The platform owns chargebacks and carries the losses, because it is the seller on every plugin charge.",
"payouts": "The platform pays developers their net proceeds after the share, the processing line and any reserve are deducted."
},
"rails": "The managed payment-rails provider runs a marketplace-shaped bundle: the platform makes the charge on its invoice, carries the losses, and each developer gets a connected account and payout dashboard whose $2.00 monthly account fee and [... 97 more characters cut from this display]",
"instruments": [
{
"instrument": "share",
"collection": "The 15% revenue share is netted from each plugin sale on the platform invoice before the developer's payout is calculated, at 0% on a developer's first $1M lifetime.",
"enforcement": "The terms forbid steering buyers to pay off-platform for a listed plugin, and a breach leads to delisting through the written path already in the terms."
},
{
"instrument": "program",
"collection": "Flat registration, review or listing fees, amount to be set, are charged only where they price the real cost of review and account upkeep or filter low-intent submissions.",
"enforcement": "A plugin is not reviewed or listed until its program fee is paid, so the fee cannot be skipped."
},
{
"instrument": "processing",
"collection": "Processing at 2.9% is passed through as its own labeled line on each payout statement, never folded into the 15% share.",
"enforcement": "It is deducted at source from every marketplace charge, so it cannot be avoided, and it shares the anti-circumvention rule and delisting path that protect the share."
}
],
"pricing": [
{
"model": "flat",
"offered": "yes",
"note": "Developers want flat subscriptions and the rails bill them on the platform's monthly invoice."
},
{
"model": "usage",
"offered": "no",
"note": "Developers want usage-based pricing but the rails cannot bill it, so keep it off the published menu until that is verified."
},
{
"model": "onetime",
"offered": "yes",
"note": "One-time purchases are wanted and supported by the rails."
},
{
"model": "freemium",
"offered": "yes",
"note": "Free plugins with paid tiers are wanted and supported, with only the paid tier carrying the share."
}
],
"waivers": [
{
"shape": "behavior",
"rule": "A discount on the share, amount to be set, pays developers for the specific pricing or architecture choice the platform wants to steer them toward, applied automatically to every plugin that makes it."
},
{
"shape": "exemption",
"rule": "The share is 0% on a developer's first $1M of lifetime sales and 15% above, with every eligible developer enrolled automatically and the lifetime basis stated explicitly."
}
],
"payout": {
"cadence": "rolling",
"hold": "Payouts roll on a 7-day cycle after a 30-day hold, with a reserve held back against chargebacks and refunds, and instant payout offered only as a paid option on top, fee to be set.",
"timeline": "Publish the honest sale-to-bank timeline of 32 to 39 days, stated as 39 days at the longest, with the 30-day hold named as the main part of the wait."
},
"tax": [
{
"jurisdiction": "United States",
"tax": "sales tax",
"owner": "platform"
},
{
"jurisdiction": "EU",
"tax": "VAT (B2C)",
"owner": "platform"
},
{
"jurisdiction": "United Kingdom",
"tax": "VAT",
"owner": "platform"
},
{
"jurisdiction": "Australia",
"tax": "GST",
"owner": "developer"
}
],
"enforcement": {
"circumvention": "Keep the rule against moving listed plugin sales off the platform invoice, backed by the delisting path already written into the developer terms.",
"notice": "Give at least six months of written notice for any fee change, defer unfavorable changes to the end of that window, and apply favorable ones early."
},
"caveats": [
"The tax split is not tax advice and goes through qualified counsel before it ships.",
"EU VAT (B2C) is owned by the platform but not registered, so automatic calculation collects nothing until registration is done.",
"Below $20.55 of monthly sales a developer costs more in rails fees than the cut brings in.",
"Usage-based pricing stays off the menu until the rails are verified to bill it."
]
}
Cut from this display only (the saved reply and the check use the full text): rails (332 characters, 235 shown).
What step 5 prints for it: share | program | processing | marketplace
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./fskit.js"); require("./fsreply.js")), then:
const K = FeeKit, R = FeeReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", take: "15", posture: "developer",
tax: "United States | sales tax | developer | no" }));
// f.postureR, f.instrumentR, f.waiverR, f.cadenceR (the ranked menus), f.timeline, f.economics, f.taxRows, f.issues (in fix order), f.gate
// K.scheduleMd(f), K.taxCsv(f) - the fee schedule and the tax split
const b = K.buildInput(f, "design"); // or "page" (R.laneForm(form, "page", design) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// 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://fee-stack.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://fee-stack.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"]
// fee-stack.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://fee-stack.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://fee-stack.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
}
// Fee Stack.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 Fee Stack {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://fee-stack.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://fee-stack.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://fee-stack.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://fee-stack.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":"fee-stack"}' | 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": "fee-stack"})["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: "fee-stack" })).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": "fee-stack"}, 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", "fee-stack"), 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: "fee-stack" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "fee-stack"])["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 = "fee-stack" }))!["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": "design", "marketplace": "Marketplace: Kestrova
What it sells and to whom: ...", "numbers": "Pricing models (model | developers want it | rails support it):
- Flat subscription | yes | yes ...", "carry": "", "facts": "The page's checks (...); verdict: money leaks or obligations nobody holds:
- The four menus | strategy | pass | ..."}
# - 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": "design", "marketplace": "Marketplace: Kestrova
What it sells and to whom: ...", "numbers": "Pricing models (model | developers want it | rails support it):
- Flat subscription | yes | yes ...", "carry": "", "facts": "The page's checks (...); verdict: money leaks or obligations nobody holds:
- The four menus | strategy | pass | ..."}
# - 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: "design" or "page", marketplace, numbers, 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 fee-stack:<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 design reply carries summary, posture, rails, instruments, pricing, waivers, payout, tax, enforcement and caveats; a page reply carries summary, title, intro, fees, payouts, tax, waiver, notice and caveats. The code below prints the gist of either: each fee instrument and how it is collected, or each fee and its amount.
KEY="fee-stack:$(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 == "page" then (.fees[] | "\(.fee): \(.amount)")
else (.instruments[] | "\(.instrument): \(.collection)") end' reply.json
import hashlib, time
key = "fee-stack:" + 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"] == "page":
for x in reply["fees"]:
print(x["fee"], x["amount"])
else:
for x in reply["instruments"]:
print(x["instrument"], x["collection"])
import { createHash } from "node:crypto";
const key = `fee-stack:${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 === "page") { for (const x of reply.fees) console.log(x.fee, x.amount); }
else { for (const x of reply.instruments) console.log(x.instrument, x.collection); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "fee-stack:" + 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, name, field := "instruments", "instrument", "collection"
if reply["task"] == "page" {
key, name, field = "fees", "fee", "amount"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx[name], xx[field])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "fee-stack:" + 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("page")) {
for (JsonNode x : reply.path("fees")) System.out.println(x.path("fee").asText() + " " + x.path("amount").asText());
} else {
for (JsonNode x : reply.path("instruments")) System.out.println(x.path("instrument").asText() + " " + x.path("collection").asText());
}
require "digest"
key = "fee-stack:#{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"] == "page"
reply["fees"].each { |x| puts "#{x["fee"]} #{x["amount"]}" }
else
reply["instruments"].each { |x| puts "#{x["instrument"]} #{x["collection"]}" }
end
$key = "fee-stack:" . $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"] === "page") { foreach ($reply["fees"] as $x) echo $x["fee"], " ", $x["amount"], "\n"; }
else { foreach ($reply["instruments"] as $x) echo $x["instrument"], " ", $x["collection"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"fee-stack:{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 == "page") { foreach (var x in reply["fees"]!.AsArray()) Console.WriteLine($"{x!["fee"]} {x["amount"]}"); }
else { foreach (var x in reply["instruments"]!.AsArray()) Console.WriteLine($"{x!["instrument"]} {x["collection"]}"); }
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: fee-stack:$(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": "design", "marketplace": "Marketplace: Kestrova
What it sells and to whom: ...", "numbers": "Pricing models (model | developers want it | rails support it):
- Flat subscription | yes | yes ...", "carry": "", "facts": "The page's checks (...); verdict: money leaks or obligations nobody holds:
- The four menus | strategy | pass | ..."}
# - 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": "fee-stack:" + 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": `fee-stack:${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", "fee-stack:"+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", "fee-stack:" + 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" => "fee-stack:#{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: fee-stack:" . 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", $"fee-stack:{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);