Run AWS Offer Desk from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the listing setup and the private offer draft, decides the fit verdict, recommends the pricing model, builds the irreversible-choice register, runs every listing check and every offer check, computes the fee on the pre-tax total contract value, settles the buyer pre-check, places this quarter's co-sell rung and lays out the timeline to the first deal, then hands it all to the run as facts. The run plans the listing or reviews the offer; the page then holds it to its own facts (awreply.js). AWS Offer Desk is about AWS Marketplace SaaS listings and their private offers - not job offers or sales quotes - and is not affiliated with or endorsed by Amazon Web Services.
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":"aws-offer-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, listing, dimensions, facts) is missing. The page always sends all six of task, listing, dimensions, offer, carry, facts; carry is an empty string for plan, and offer is an empty string when no offer is drafted. |
| 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 listing, then each private offer:
plan- the listing plan: the page's fit verdict and recommended pricing model, one row per page register row (choice plus item, decision, owner and date - "unassigned" until named), one fix per failing listing check (check id plus subject), the timeline to the first deal with every wait marked, and this quarter's co-sell rung with the condition to move up - every AWS figure with its dagger, practitioner claims reported.offer- the private offer reviewed: the page's fee tier, rate, amount and net (never recomputed), one row per offer check that fails or warns (check id plus subject), keep or fix on the installments, the net terms, the renewal flag and auto-renewal, the expiry, the buyer pre-check with the page's statuses, and a short message to the buyer's procurement contact - one item per line, no commit-burn language unless the badge is held, no signature date earlier than review allows. Needs the offer draft inoffer.
A missing or unknown task is answered as plan. Both lanes send the same six 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 plan's model, fit, register and fixes into carry for the offer lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or offer. If it is missing or neither, do plan and set "task": "plan". |
listing | yes | the listing setup as the page read it: the product, the pricing model, the stage, another public paid product, the public terms and the trial, the seller account, the hosting, a third-party service off AWS, the badge, Partner Central, the buyers, the offer owner, the engineering weeks, the target date with its days from today, the countries, how the vendor sells today and the public price changes planned. |
dimensions | yes | one line per dimension: API name, display name, kind, unit and public price. |
offer | send it (empty with no offer drafted; offer needs it) | the private offer draft as the page read it (buyer, country, renewal, renewal flag, replacement, total contract value, term, net terms, auto-renewal, expiry, CPPO and the wholesale price, professional services, the installments with their index and date, the buyer-side notes), or empty when no offer is drafted. |
carry | send it (empty for plan) | offer task only: the listing plan's model, fit, register and fixes when the plan ran first; may be empty. |
facts | yes | the page's listing checks, one line each with the rule, the id, the subject, the status and the evidence separated by vertical bars; then the fit verdict, the pricing model, the register, the timeline, the co-sell ladder, the offer checks, the fee lines, the buyer pre-check, the figure list, the reported claims and the footnote. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it and reply with only the JSON object. |
The rules the model applies
From the source skill, @samber/aws-marketplace-playbook - the only standard the run applies:
- Scope: the ISV seller's own SaaS listing on AWS Marketplace and its private offers. Not buyers, not resellers, not listing copy or search rank, not metering code, not the vendor's own marketplace billing.
- The listing is a procurement rail, not a demand channel: revenue comes from private offers to buyers who already spend on AWS. Public listings draw little inbound.
- Commitments you cannot undo: the seller account becomes seller of record and cannot change once a product is listed - use a fresh standalone account, or an organization whose accounts all share your legal entity (with tax inheritance a member account takes the management account's legal entity); never the main production account. The SaaS pricing model is fixed once the product is published to Limited - the wrong model means a new product. Dimension API names never change; custom dimensions added for offers cannot be removed. Public price increases freeze every further pricing update for at least 90 days† and reach existing subscribers only after AWS notifies them - lower prices first, then raise. Selling authorizations to resellers cannot be edited or extended once published, only deactivated. A seller opt-out of an auto-renewal cannot be reversed; the buyer then needs a new offer. Accepted offer terms - signed payment schedules and net terms - are frozen; changing them takes a replacement offer the buyer must accept.
- Fit verdict: transact with commit when the product runs fully on AWS and the buyers are enterprises holding AWS commitments (earn the badge before the first offer; sell burning the commit only with the buyer's AWS team confirming); transact, no commit for off-AWS hosting or buyers wanting AWS billing for vendor consolidation or procurement speed (no commit language in decks or offers; pitch one invoice and pre-approved terms; weigh a fully-on-AWS edition); skip for now for self-serve or card buyers, no AWS-committed accounts in the pipeline, or no one to own offers (revisit when a named deal asks for it).
- Pricing model: the default rung is SaaS contract (public terms of 1, 12, 24 or 36 months†; custom lengths in private offers); contract with consumption when overage is part of the price book; subscription only when the product is billed purely by metered usage; free for lead capture with every dimension at zero. Installments and auto-renewal need a contract model. Every SaaS model supports a Marketplace free trial of 7-90 days†; trials hosted off Marketplace may not be advertised. Bill everything through listed dimensions: no card capture, no links to other clouds.
- Path to the first deal: register (tax interview, bank account, a USD disbursement method, bank verification, and KYC when selling into EMEA or with a UK bank - practitioners report up to two weeks for KYC); build and test the integration in parallel; publish to Limited at a reduced test price and cancel every test subscription; request Public with the real price - Seller Operations reviews in 7-10 business days†; plan 3-6 weeks† from registration to Public. A seller account needs at least one public listing priced above zero before any private offer; a first product cannot carry one while Limited. Submit the architecture diagram for the Deployed on AWS badge when the product qualifies, and link Partner Central with the seller account so offers attach to co-sell opportunities. When a renewal date falls before Public can realistically pass, extend the customer briefly on your own paper and set the renewal flag at the next renewal. Never promise a Marketplace signature date earlier than review allows.
- Private offers: keep the public price at list and discount in private offers. Flag existing customers moving their renewal onto Marketplace as Existing Customer Moving to AWS Marketplace - renewals pay 1.5%† instead of 3%†; set the same flag on a reseller's selling authorization. Moving a public Marketplace subscriber onto a private offer is not a renewal. Re-include unbilled installments when replacing an agreement: the replacement cancels the old pending payments. Through a buyer's chosen reseller (CPPO), the extra 0.5%† comes out of your payout, computed on the wholesale price, and the reseller becomes seller of record for that deal's refunds and cancellations.
- Offer settings (the reference): expiry closes at 23:59:59 UTC and can be extended - practitioners report setting 30 days aligned to the buyer's quarter-end and extending rather than issuing long-dated offers; installments may be monthly, quarterly, annual or custom, and only one invoice date may fall before acceptance; net terms run Net 15 to Net 120†, apply to every charge and are fixed after acceptance; a Private Marketplace means extending the offer to the buyer account and the administrator account, and asking the administrator to approve the product; card buyers are charged at once, so net terms and a spread schedule do nothing for them.
- Commitment drawdown: only products holding the Deployed on AWS badge may still count, inside a share set in each buyer's private AWS contract; AWS reviews the architecture first. The badge needs the application and control plane and every third-party service touching customer data on AWS (CDN, DNS and corporate identity providers are exempt). Practitioners commonly cite 25%, reported - AWS publishes no figure; never promise a percentage. Professional services do not count.
- Co-sell: ACE from the first deal; answer AWS-referred opportunities within 5 business days†; FTR is an architecture and security review of roughly 2-4 weeks†; ISV Accelerate arrives by invitation once a GA listing, Validated status, a Payee Central account, 5 launched† and 15 qualified† opportunities in 12 months, one trained seller and $2,000† of AWS revenue are in place - never plan around applying early; promotion credits need a request to the AWS partner team before the buyer accepts†. Expect field engagement 6-12 months† after listing.
- Money: fees† run on the pre-tax total contract value whatever the payment schedule - 3%† SaaS public; new private offers 3%† under $1M†, 2%† to $10M†, 1.5%† above; every renewal 1.5%† at any size; CPPO +0.5%†; South Korea buyers +1%†; professional services 0.5%†, 0%† inside a qualifying multi-product offer set. AWS pays only after it collects, so net terms move cash by the same days. Pay reps the same commission and quota credit as direct, on acceptance; never inflate the public price to recover the fee.
- Every AWS figure carries † right after it. Practitioner claims say "reported". Close with the footnote: "† verify against the current AWS Marketplace Seller Guide before quoting it."
House rules
- Use only the input. Never invent a price, a fee, a date, a dimension, a buyer, a threshold, an account id or a URL; the only figures you may quote are the skill's, each with † as the facts mark it, the page's computed amounts and the input's own values. When the input does not say, write "not stated in the input".
- The page's verdicts are settled: every check status, the fit verdict, the recommended pricing model, every register row, the fee tier, the fee amount and the net, every buyer pre-check status and the co-sell rung. The fee amount and the tier are the page's - never recompute or move them; explain them.
- A row is its identity: a register row is its choice plus its item exactly as the facts write them (a dimension item is its API name plus its kind, such as "seats (entitled)"); a check row is its check id plus its subject exactly as the facts write them (an installment subject is its index plus its date, such as "#2 2027-01-15"). Never merge two rows that share a display name or an id.
- Every AWS figure carries † right after it; every practitioner claim says "reported"; the
footnotefield is exactly "† verify against the current AWS Marketplace Seller Guide before quoting it." - Never imply that AWS or Amazon endorses, recommends, certifies or vets the product.
- No URL: never write a scheme such as https and never copy a link. Write "the Seller Guide" or "the listing page" where a link belongs.
- Write in the language of the input; keep the JSON keys, check ids, choice ids, API names and enum values exactly 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
plan
Plan the listing: the fit verdict, the pricing model, the irreversible-choice register, one fix per failing listing check, the timeline to the first deal and this quarter's co-sell rung.
summary- one sentence, at most 40 words: what stands between this listing and its first Marketplace deal today, and what the plan changes.verdict- an object:fit(exactly the facts' fit verdict:transact_with_commit,transact_no_commitorskip_for_now),why(one sentence naming the answers behind it).model- an object:pricing(exactly the facts' recommended model:contract,contract_consumption,subscriptionorfree),why(one sentence: how the vendor sells, and that the model is fixed once published to Limited).register- one object per register row in the facts, in their order:choice(exactly the facts' choice:seller_account,pricing_model,dimension_api_name,custom_dimension,reseller_authorization,auto_renewal_optoutoroffer_terms),item(exactly the facts' item),decision(the facts' decision from the input, or "undecided"),owner("unassigned" unless the input names a role),by("unassigned", or a YYYY-MM-DD date the input gives).fixes- one object per listing check the facts mark fail, and any warn worth fixing:check(the page's check id),subject(the row's subject exactly as the facts write it, or "" when the row has none),change(one concrete sentence: what to change in the account, the model, the dimensions, the prices, the offer timing or the commitment language). Every fail row appears.timeline- the steps to the first deal, one object per step, in the facts' order:step(one sentence),wait(the wait, with † on every AWS figure, "reported" on every practitioner claim, or the page's own assumption named as such).cosell- an object:rung(exactly the facts' rung:ace,ftr,isv_accelerateormpopp),next(one sentence: the condition that moves up a rung).footnote- exactly the footnote.caveats- at most 4 short strings: what the team must confirm;[]when there is nothing to say.
{
"task": "plan",
"summary": "one sentence",
"verdict": { "fit": "transact_with_commit", "why": "one sentence" },
"model": { "pricing": "contract", "why": "one sentence" },
"register": [ { "choice": "dimension_api_name", "item": "seats (entitled)", "decision": "keep seats", "owner": "unassigned", "by": "unassigned" } ],
"fixes": [ { "check": "dim_dup", "subject": "seats", "change": "one sentence" } ],
"timeline": [ { "step": "one sentence", "wait": "Seller Operations reviews in 7-10 business days†" } ],
"cosell": { "rung": "ace", "next": "one sentence" },
"footnote": "† verify against the current AWS Marketplace Seller Guide before quoting it.",
"caveats": [ ]
}
The page holds it to its own facts (check() in awreply.js; any warning makes the reply flagged), matching rows by the same composite identity its facts use: the reply answers the task it was sent; no URL with a scheme; no backticks; no implied AWS endorsement; a summary of at most 40 words; every AWS figure with its dagger and every practitioner claim labelled reported; the exact footnote; the page's fit verdict and pricing model (put back if moved); one register row per page row by choice plus item - two dimensions sharing a display name stay two rows - with the input's decision (put back), an owner of unassigned unless the input names a role and a date of unassigned or YYYY-MM-DD; a fix for every listing fail by check id plus subject, and only the page's rows; a timeline whose steps all carry a wait; the page's co-sell rung (put back) and a condition to move up.
offer
Review the private offer: the fee as the page computed it, every offer check that fails or warns, the terms, the buyer pre-check and a short message to the buyer's procurement contact.
summary- one sentence, at most 40 words: what the offer contains, what must change before it goes out, and the fee.fee- an object:tier(exactly the facts' tier:public,new_under_1m,new_1m_10m,new_over_10morrenewal),rate(the facts' rate, each percentage with †),amount(exactly the facts' total fee, such as "$36,000.00"),net(exactly the facts' net).checks- one object per offer check the facts mark fail or warn, in their order:check(the page's check id),subject(the row's subject exactly as the facts write it, or ""),finding(one sentence),change(one concrete sentence: what to change in the draft before it goes out).[]when nothing fails or warns.terms- an object:installments(starting with exactlykeeporfix, as the facts' must line says, then the reason),net_terms(what the net terms do to cash),renewal(the renewal flag and the auto-renewal advice),expiry(the expiry advice).buyer_precheck- one object per item of the facts' buyer pre-check, in their order:item(exactly the facts' item:aws_account,private_marketplace,invoice,active_agreement,commitment,currencyorcountry),status(exactly the facts' status:ready,missingorunknown).message- a short note to the buyer's procurement contact: what the offer contains, then what they need to approve or confirm, each item on its own line. Separate the lines with a newline, written \n inside the JSON string - for example "The offer covers 36 months of Analyst seats for $2,400,000.00.\nThree annual installments of $800,000.00, net 30.\nPlease ask your Private Marketplace administrator to approve the product." Plain text; never promise a signature date earlier than the facts' realistic first deal; never claim AWS endorsement; no commit-burn language unless the facts say the badge is held for a fully-on-AWS product.footnote- exactly the footnote.caveats- at most 4 short strings: what to confirm before the offer goes out;[]when there is nothing to say.
{
"task": "offer",
"summary": "one sentence",
"fee": { "tier": "renewal", "rate": "1.5%†", "amount": "$36,000.00", "net": "$2,364,000.00" },
"checks": [ { "check": "inst_sum", "subject": "", "finding": "one sentence", "change": "one sentence" } ],
"terms": { "installments": "keep - one sentence", "net_terms": "one sentence", "renewal": "one sentence", "expiry": "one sentence" },
"buyer_precheck": [ { "item": "aws_account", "status": "unknown" } ],
"message": "first line\nsecond line",
"footnote": "† verify against the current AWS Marketplace Seller Guide before quoting it.",
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; the page's fee tier (put back), a rate carrying the tier's percentage with its dagger, and exactly the page's fee amount and net (put back - never recomputed); one row per offer check that fails or warns, by check id plus subject - an installment is its index plus its date - and no row the page does not flag; installments advice starting keep or fix as the page says; the renewal flag named when the page finds it missing; the buyer pre-check items with the page's statuses (put back); a message one item per line (an inline numbered list is caught), with no commit-burn language unless the badge is held and no signature date earlier than the page's realistic first deal.
Worked examples
Three runs from the page's free samples (invented products and buyers): the listing plan for a first listing that fails a lot, its offer review carrying that plan, and the offer review for a clean renewal moving to Marketplace. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the long fields, strings and lists 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 - A first listing that fails a lot
The page read it as: not yet - fails to fix first; stage Limited; fit transact_with_commit; model contract; 10 register rows; listing fails: seller_account, dim_dup (seats), price_window (seats 2027-01-11), first_offer_public, commit_burn; co-sell rung ace; the target date is late.
Input (facts is 12608 characters; the first 1396 are shown):
{
"task": "plan",
"listing": "Product: Ledgerline; pricing model: SaaS subscription; stage: Limited; another public paid product already listed: no\nPublic contract terms offered: 12, 36 months; free trial: 14 days\nSeller account: our main production account; hosting: fully on AWS; a third-party service touching customer data off AWS: no; Deployed on AWS badge: not applied; Partner Central and seller accounts linked: no\nBuyers: enterprise accounts with an AWS commitment; offer owner named: yes; engineering weeks available: 3; target date for the first Marketplace deal: 2026-11-12 (10 days from today, 2026-11-02)\nCompany country: United States; buyer countries: United States, Germany\nHow you sell today: Three-year enterprise contracts on annual seat tiers, invoiced yearly. Typical deal $250k to $900k. A few smaller customers pay by credit card.\nPublic price changes planned: seats $1,200.00 -> $1,320.00 on 2026-11-22; seats $1,320.00 -> $1,450.00 on 2027-01-11",
"dimensions": "Dimensions (api name | display name | kind | unit | public price):\n- seats | Seats | entitled | user | $1,200.00\n- seats | Seat overage | metered | user-hour | $0.40\n- api_calls_v2 | API calls | metered | 1,000 calls | $2.50\n- enterprise_tier | Enterprise tier | custom-for-offers | tier | $0.00",
"offer": "Buyer: Corvane Freight; buyer country: United States; existing customer moving the renewal to Marketplace: no; renewal flag set on the offer: no; replaces an active agreement: no\nTotal contract value: $850,000.00; term: 36 months; net terms: net 60 days; auto-renewal: on; offer expiry: 2026-11-23\nVia reseller CPPO: yes; wholesale price: $760,000.00; professional services included: no\nInstallments (index | date | amount): 1 | 2026-11-07 | $300,000.00; 2 | 2027-11-07 | $300,000.00; 3 | 2028-11-06 | $200,000.00\nBuyer-side notes: The buyer wants to burn their EDP commit with this purchase. Procurement needs vendor onboarding before the purchase order. Payment by invoice.",
"carry": "",
"facts": "The page's listing checks (5 fail, 7 warn, 3 pass, 3 info, 2 n/a); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:\n- account | seller_account | - | fail | our main production account - it becomes seller of record forever and mixes Marketplace finance with infrastructure billing; register a fresh standalone account\n- model | model_fit | - | warn | a SaaS subscription while you sell contracts - installments and auto-renewal need a contract model; pick the model your sales team actually closes (SaaS contract) - already fixed at Limited: the wrong model means a new product\n- dimensions | dim_listed | - | pass | 4 dimensions listed\n- dimensions | dim_dup | seats | fail | seats is the API name of 2 dimensions (line 1, entitled; line 2, metered) - one API name per dimension, and it never changes\n- dimensions | dim_name | api_calls_v2 (metered) | warn | api_calls_v2 has a version suffix - dimension API names never change; pick the final name before you create it\n- dimensions | dim_custom | enterprise_tier (custom) | info | enterprise_tier - a custom dimension added for offers cannot be removed\n- dimensions | dim_metered | - | na | SaaS subscription meters usage\n- dimensions | dim_free | - | na | not the free model\n- model | public_terms | - | info | public terms of 12, 36 months are for the contract models\n- prices | price_change | seats 2026-11-22 |\n\n[... 11212 more characters cut from this display ...]"
}
Reply (shortened for display - see the cuts below):
{
"task": "plan",
"summary": "Ledgerline is registered on the main production account, sells contracts on a subscription model and drafts an offer while Limited; the plan moves the account, fixes the dimensions and holds the offer until Public.",
"verdict": {
"fit": "transact_with_commit",
"why": "The product runs fully on AWS, the buyers are enterprises holding AWS commitments and an offer owner is named, so earn the badge before the first offer."
},
"model": {
"pricing": "contract",
"why": "The sales team closes three-year contracts on annual seat tiers, so a SaaS contract fits; the subscription model is already fixed at Limited, so that means a new product."
},
"register": [
{
"choice": "seller_account",
"item": "our main production account",
"decision": "our main production account",
"owner": "unassigned",
"by": "unassigned"
},
{
"choice": "pricing_model",
"item": "SaaS subscription",
"decision": "SaaS subscription",
"owner": "unassigned",
"by": "unassigned"
},
{
"choice": "dimension_api_name",
"item": "seats (entitled)",
"decision": "keep seats",
"owner": "unassigned",
"by": "unassigned"
},
{
"choice": "dimension_api_name",
"item": "seats (metered)",
"decision": "keep seats",
"owner": "unassigned",
"by": "unassigned"
},
{
"choice": "dimension_api_name",
"item": "api_calls_v2 (metered)",
"decision": "keep api_calls_v2",
"owner": "unassigned",
"by": "unassigned"
}
],
"fixes": [
{
"check": "seller_account",
"subject": "",
"change": "Register a fresh standalone seller account before any product is listed, so the main production account never becomes seller of record."
},
{
"check": "dim_dup",
"subject": "seats",
"change": "Keep seats on the entitled dimension and give the metered overage row its own final lower-case API name before it is created, since API names never change."
},
{
"check": "price_window",
"subject": "seats 2027-01-11",
"change": "Fold the two seats increases into one, or move the second one at least 90 days† after the first, because an increase freezes every further pricing update."
},
{
"check": "first_offer_public",
"subject": "",
"change": "Hold the Corvane Freight offer until the listing passes Public review at a price above zero; a first product carries no private offer while Limited."
},
{
"check": "commit_burn",
"subject": "",
"change": "Take the promise about the buyer's commitment out of the deal notes until the Deployed on AWS badge is granted, then let the buyer's AWS account team confirm any share."
}
],
"timeline": [
{
"step": "Link Partner Central with the AWS Marketplace seller account so offers attach to co-sell opportunities.",
"wait": "no wait"
},
{
"step": "Run the Limited listing at a reduced test price with allowlisted test accounts, then cancel every test subscription.",
"wait": "one week - the page's own assumption"
},
{
"step": "Submit the architecture diagram for the Deployed on AWS badge in Partner Central.",
"wait": "AWS reviews the architecture first"
},
{
"step": "Request Public with the real price.",
"wait": "Seller Operations reviews in 7-10 business days†, longer on errors"
},
{
"step": "Send the Corvane Freight offer after the buyer pre-check, once Public passes.",
"wait": "no private offer before Public; plan 3-6 weeks† from registration to Public"
}
],
"cosell": {
"rung": "ace",
"next": "Move up to FTR once ACE opportunities carry real AWS consumption: an architecture and security review of roughly 2-4 weeks†."
},
"footnote": "† verify against the current AWS Marketplace Seller Guide before quoting it.",
"caveats": [
"Confirm with Seller Operations what happens to the subscription listing already in Limited.",
"Confirm which legal entity the fresh seller account registers under."
]
}
Cut from this display only (the saved reply and the check use the full text): register (10 items, 5 shown); fixes (7 items, 5 shown).
What step 5 prints for it: seller_account | dim_dup (seats) | price_window (seats 2027-01-11) | first_offer_public | commit_burn | model_fit | dim_name (api_calls_v2 (metered))
The page's check: kept - nothing to flag
offer - A first listing that fails a lot
The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the offer draft: fee $26,600.00 on tier new_under_1m, net $733,400.00; flagged offer checks: inst_model, inst_sum, auto_renewal.
Input (carry is 2433 characters; the first 1393 are shown; facts is 12337 characters; the first 1396 are shown):
{
"task": "offer",
"listing": "Product: Ledgerline; pricing model: SaaS subscription; stage: Limited; another public paid product already listed: no\nPublic contract terms offered: 12, 36 months; free trial: 14 days\nSeller account: our main production account; hosting: fully on AWS; a third-party service touching customer data off AWS: no; Deployed on AWS badge: not applied; Partner Central and seller accounts linked: no\nBuyers: enterprise accounts with an AWS commitment; offer owner named: yes; engineering weeks available: 3; target date for the first Marketplace deal: 2026-11-12 (10 days from today, 2026-11-02)\nCompany country: United States; buyer countries: United States, Germany\nHow you sell today: Three-year enterprise contracts on annual seat tiers, invoiced yearly. Typical deal $250k to $900k. A few smaller customers pay by credit card.\nPublic price changes planned: seats $1,200.00 -> $1,320.00 on 2026-11-22; seats $1,320.00 -> $1,450.00 on 2027-01-11",
"dimensions": "Dimensions (api name | display name | kind | unit | public price):\n- seats | Seats | entitled | user | $1,200.00\n- seats | Seat overage | metered | user-hour | $0.40\n- api_calls_v2 | API calls | metered | 1,000 calls | $2.50\n- enterprise_tier | Enterprise tier | custom-for-offers | tier | $0.00",
"offer": "Buyer: Corvane Freight; buyer country: United States; existing customer moving the renewal to Marketplace: no; renewal flag set on the offer: no; replaces an active agreement: no\nTotal contract value: $850,000.00; term: 36 months; net terms: net 60 days; auto-renewal: on; offer expiry: 2026-11-23\nVia reseller CPPO: yes; wholesale price: $760,000.00; professional services included: no\nInstallments (index | date | amount): 1 | 2026-11-07 | $300,000.00; 2 | 2027-11-07 | $300,000.00; 3 | 2028-11-06 | $200,000.00\nBuyer-side notes: The buyer wants to burn their EDP commit with this purchase. Procurement needs vendor onboarding before the purchase order. Payment by invoice.",
"carry": "The listing plan's model: contract - The sales team closes three-year contracts on annual seat tiers, so a SaaS contract fits; the subscription model is already fixed at Limited, so that means a new product.\nThe listing plan's fit: transact_with_commit\nThe register (choice | item | decision | owner | by):\n- seller_account | our main production account | our main production account | unassigned | unassigned\n- pricing_model | SaaS subscription | SaaS subscription | unassigned | unassigned\n- dimension_api_name | seats (entitled) | keep seats | unassigned | unassigned\n- dimension_api_name | seats (metered) | keep seats | unassigned | unassigned\n- dimension_api_name | api_calls_v2 (metered) | keep api_calls_v2 | unassigned | unassigned\n- dimension_api_name | enterprise_tier (custom) | keep enterprise_tier | unassigned | unassigned\n- custom_dimension | enterprise_tier | keep enterprise_tier | unassigned | unassigned\n- reseller_authorization | CPPO for Corvane Freight | issue a single-buyer authorization at $760,000.00 wholesale | unassigned | unassigned\n- auto_renewal_optout | Corvane Freight | undecided | unassigned | unassigned\n- offer_terms | Corvane Freight | undecided | unassigned | unassigned\nThe plan's fixes:\n- seller_account: Register a fresh standalone seller account before any product is listed, so the main production account never becomes seller of record.\n- dim_dup\n\n[... 1040 more characters cut from this display ...]",
"facts": "The page's listing checks (5 fail, 7 warn, 3 pass, 3 info, 2 n/a); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:\n- account | seller_account | - | fail | our main production account - it becomes seller of record forever and mixes Marketplace finance with infrastructure billing; register a fresh standalone account\n- model | model_fit | - | warn | a SaaS subscription while you sell contracts - installments and auto-renewal need a contract model; pick the model your sales team actually closes (SaaS contract) - already fixed at Limited: the wrong model means a new product\n- dimensions | dim_listed | - | pass | 4 dimensions listed\n- dimensions | dim_dup | seats | fail | seats is the API name of 2 dimensions (line 1, entitled; line 2, metered) - one API name per dimension, and it never changes\n- dimensions | dim_name | api_calls_v2 (metered) | warn | api_calls_v2 has a version suffix - dimension API names never change; pick the final name before you create it\n- dimensions | dim_custom | enterprise_tier (custom) | info | enterprise_tier - a custom dimension added for offers cannot be removed\n- dimensions | dim_metered | - | na | SaaS subscription meters usage\n- dimensions | dim_free | - | na | not the free model\n- model | public_terms | - | info | public terms of 12, 36 months are for the contract models\n- prices | price_change | seats 2026-11-22 |\n\n[... 10941 more characters cut from this display ...]"
}
Reply (shortened for display - see the cuts below):
{
"task": "offer",
"summary": "The $850,000.00 Corvane Freight offer through a reseller pays a $26,600.00 fee (3%† plus 0.5%† CPPO), but its installments fall short of the total and need a contract model first.",
"fee": {
"tier": "new_under_1m",
"rate": "3%† +0.5%† CPPO",
"amount": "$26,600.00",
"net": "$733,400.00"
},
"checks": [
{
"check": "inst_model",
"subject": "",
"finding": "The listing runs on SaaS subscription, and installments need a contract model.",
"change": "Move the deal to the SaaS contract product before attaching an installment schedule, or bill it as one charge."
},
{
"check": "inst_sum",
"subject": "",
"finding": "The three installments add up to $800,000.00 against an $850,000.00 total.",
"change": "Raise the installments so they add up to $850,000.00, or lower the total to match the schedule the buyer agreed."
},
{
"check": "auto_renewal",
"subject": "",
"finding": "Auto-renewal is on under SaaS subscription, which needs a contract model.",
"change": "Turn auto-renewal off on this draft and set it again on the contract product, with the renewal price and the decision deadline."
}
],
"terms": {
"installments": "fix - the schedule falls $50,000.00 short of the total and installments need a contract model.",
"net_terms": "Net 60 sits inside Net 15 to Net 120†; AWS pays only after it collects, so the cash arrives 60 days after each invoice.",
"renewal": "A new customer, so no renewal flag; leave auto-renewal off until the listing runs on a contract model.",
"expiry": "The offer expires 2026-11-23 at 23:59:59 UTC; extend it rather than reissue it if Public review runs long."
},
"buyer_precheck": [
{
"item": "aws_account",
"status": "unknown"
},
{
"item": "private_marketplace",
"status": "unknown"
},
{
"item": "invoice",
"status": "ready"
},
{
"item": "active_agreement",
"status": "unknown"
},
{
"item": "commitment",
"status": "missing"
}
],
"message": "The offer covers 36 months of Ledgerline for a total of $850,000.00, through the reseller your team chose.\nThe payment schedule is being corrected so the installments add up to the total.\nPlease confirm the 12-digit AWS account that will own the subscription. [... 114 more characters cut from this display]",
"footnote": "† verify against the current AWS Marketplace Seller Guide before quoting it.",
"caveats": [
"Confirm the reseller is a registered paid seller before the authorization is published.",
"Confirm the corrected schedule with the buyer before the offer goes out."
]
}
Cut from this display only (the saved reply and the check use the full text): buyer_precheck (7 items, 5 shown); message (373 characters, 259 shown).
What step 5 prints for it: new_under_1m $26,600.00 net $733,400.00 | inst_model | inst_sum | auto_renewal
The page's check: kept - nothing to flag
offer - A clean renewal moving to Marketplace
The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the offer draft: fee $36,000.00 on tier renewal, net $2,364,000.00; flagged offer checks: none.
Input (facts is 9500 characters; the first 1398 are shown):
{
"task": "offer",
"listing": "Product: Northwind Analytics; pricing model: SaaS contract; stage: Public; another public paid product already listed: no\nPublic contract terms offered: 12, 24, 36 months; free trial: none\nSeller account: a fresh standalone account; hosting: fully on AWS; a third-party service touching customer data off AWS: no; Deployed on AWS badge: held; Partner Central and seller accounts linked: yes\nBuyers: enterprise accounts with an AWS commitment; offer owner named: yes; engineering weeks available: 2; target date for the first Marketplace deal: 2026-12-12 (40 days from today, 2026-11-02)\nCompany country: United States; buyer countries: United States, Canada\nHow you sell today: Annual and multi-year contracts on analyst seats and workspace tiers, invoiced annually. Typical deal $400k to $2.5M.\nPublic price changes planned: none",
"dimensions": "Dimensions (api name | display name | kind | unit | public price):\n- analyst_seats | Analyst seats | entitled | user | $900.00\n- workspace_tier | Workspace | entitled | workspace | $12,000.00\n- premium_support | Premium support | custom-for-offers | year | $0.00",
"offer": "Buyer: Fernhill Retail Group; buyer country: United States; existing customer moving the renewal to Marketplace: yes; renewal flag set on the offer: yes; replaces an active agreement: no\nTotal contract value: $2,400,000.00; term: 36 months; net terms: net 30 days; auto-renewal: on; offer expiry: 2026-11-22\nVia reseller CPPO: no; professional services included: no\nInstallments (index | date | amount): 1 | 2026-11-16 | $800,000.00; 2 | 2027-11-16 | $800,000.00; 3 | 2028-11-15 | $800,000.00\nBuyer-side notes: An existing customer moving its renewal to Marketplace. Their Private Marketplace administrator must approve the product. They expect the purchase to count toward their AWS commitment. Payment by invoice.",
"carry": "The listing plan's model: contract - The team sells annual and multi-year contracts on seats and workspace tiers, and the SaaS contract it chose is fixed since Limited.\nThe listing plan's fit: transact_with_commit\nThe register (choice | item | decision | owner | by):\n- seller_account | a fresh standalone account | a fresh standalone account | unassigned | unassigned\n- pricing_model | SaaS contract | SaaS contract | unassigned | unassigned\n- dimension_api_name | analyst_seats (entitled) | keep analyst_seats | unassigned | unassigned\n- dimension_api_name | workspace_tier (entitled) | keep workspace_tier | unassigned | unassigned\n- dimension_api_name | premium_support (custom) | keep premium_support | unassigned | unassigned\n- custom_dimension | premium_support | keep premium_support | unassigned | unassigned\n- reseller_authorization | none in this draft | undecided | unassigned | unassigned\n- auto_renewal_optout | Fernhill Retail Group | undecided | unassigned | unassigned\n- offer_terms | Fernhill Retail Group | undecided | unassigned | unassigned",
"facts": "The page's listing checks (0 fail, 0 warn, 14 pass, 1 info, 3 n/a); verdict: ready; rule | id | subject | status | evidence:\n- account | seller_account | - | pass | a fresh standalone account - seller of record, which cannot change once a product is listed\n- model | model_fit | - | pass | SaaS contract - what the page recommends from how you sell\n- dimensions | dim_listed | - | pass | 3 dimensions listed\n- dimensions | dim_dup | - | pass | every API name appears once\n- dimensions | dim_name | - | pass | every API name is lower case, without spaces or a version suffix\n- dimensions | dim_custom | premium_support (custom) | info | premium_support - a custom dimension added for offers cannot be removed\n- dimensions | dim_metered | - | pass | no metered dimension on the SaaS contract\n- dimensions | dim_free | - | na | not the free model\n- model | public_terms | - | pass | public terms of 12, 24, 36 months, from the 1, 12, 24 or 36 months† a contract offers; custom lengths go in private offers\n- prices | price_change | - | na | no change planned\n- trial | trial_days | - | na | no trial\n- billing | billing_channel | - | pass | nothing in how you sell points off Marketplace billing\n- offers | first_offer_public | - | pass | the listing is Public\n- commit | commit_burn | - | pass | \"count toward their AWS commitment\" - the badge is held and the product runs fully on AWS; never promise\n\n[... 8102 more characters cut from this display ...]"
}
Reply (shortened for display - see the cuts below):
{
"task": "offer",
"summary": "The $2,400,000.00 Fernhill Retail Group renewal is flagged as an existing customer moving to Marketplace, so it pays $36,000.00 at the 1.5%† renewal tier; every offer check holds.",
"fee": {
"tier": "renewal",
"rate": "1.5%†",
"amount": "$36,000.00",
"net": "$2,364,000.00"
},
"checks": [],
"terms": {
"installments": "keep - three annual installments of $800,000.00 add up to the total inside the 36-month term.",
"net_terms": "Net 30 sits inside Net 15 to Net 120†; AWS pays only after it collects, so cash lands 30 days after each invoice.",
"renewal": "The renewal flag, Existing Customer Moving to AWS Marketplace, is set; auto-renewal is on, so set the renewal price, the maximum renewals and the decision deadline.",
"expiry": "The offer expires 2026-11-22 at 23:59:59 UTC and can be extended if procurement needs more time."
},
"buyer_precheck": [
{
"item": "aws_account",
"status": "unknown"
},
{
"item": "private_marketplace",
"status": "ready"
},
{
"item": "invoice",
"status": "ready"
},
{
"item": "active_agreement",
"status": "unknown"
},
{
"item": "commitment",
"status": "ready"
}
],
"message": "The renewal offer covers 36 months of Analyst seats and the Workspace tier for $2,400,000.00.\nThree annual installments of $800,000.00 on 2026-11-16, 2027-11-16 and 2028-11-15, on net 30.\nPlease ask your Private Marketplace administrator to approve the [... 178 more characters cut from this display]",
"footnote": "† verify against the current AWS Marketplace Seller Guide before quoting it.",
"caveats": [
"Confirm whether the account already holds an active agreement for the product, which would need a replacement offer."
]
}
Cut from this display only (the saved reply and the check use the full text): buyer_precheck (7 items, 5 shown); message (430 characters, 252 shown).
What step 5 prints for it: renewal $36,000.00 net $2,364,000.00
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./awkit.js"); require("./awreply.js")), then:
const K = AwKit, R = AwReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme Analytics", pricing: "contract", stage: "public", owner: "yes", buy_enterprise: "yes",
dimensions: "seats | Seats | kind: entitled | user | $1,200",
o_buyer: "Example Corp", o_country: "United States", o_tcv: "USD 1.0M", o_term: "12", o_installments: "2027-01-15 | $1,000,000" }));
// f.fit (transact_with_commit, transact_no_commit, skip_for_now), f.model.pick, f.register (choice + item),
// f.rows (the listing checks: id + subject), f.offerRows (the offer pre-check), f.fee (tier, rate, amount, net, lines),
// f.precheck (the buyer pre-check), f.cosell (rung, next), f.timeline (steps, realistic, late)
// K.registerCsv(f), K.checksCsv(f), K.feeCsv(f), K.board(f), K.applyPlan(f, planReply) - the exports
const b = K.buildInput(f, "offer"); // or "plan" (R.laneForm(form, "offer", planReply) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "offer");
// R.check(reply, b) // {status: kept or flagged, notes, fixed}
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://aws-offer-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://aws-offer-desk.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// aws-offer-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://aws-offer-desk.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://aws-offer-desk.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// AwsOfferDesk.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 AwsOfferDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://aws-offer-desk.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://aws-offer-desk.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://aws-offer-desk.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://aws-offer-desk.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"aws-offer-desk"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "aws-offer-desk"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "aws-offer-desk" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "aws-offer-desk"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "aws-offer-desk"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "aws-offer-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "aws-offer-desk"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "aws-offer-desk" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "plan", "listing": "Product: Ledgerline; pricing model: SaaS subscription; stage: Limited
...", "dimensions": "Dimensions (api name | display name | kind | unit | public price):
...", "offer": "Buyer: Corvane Freight; buyer country: United States
...", "carry": "", "facts": "The page's listing checks (...); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:
- account | seller_account | - | fail | our main production account ..."}
# - 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", "listing": "Product: Ledgerline; pricing model: SaaS subscription; stage: Limited
...", "dimensions": "Dimensions (api name | display name | kind | unit | public price):
...", "offer": "Buyer: Corvane Freight; buyer country: United States
...", "carry": "", "facts": "The page's listing checks (...); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:
- account | seller_account | - | fail | our main production account ..."}
# - 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 "offer", listing, dimensions, offer, 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 aws-offer-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A plan reply carries summary, verdict, model, register, fixes, timeline, cosell, footnote and caveats; an offer-lane reply carries summary, fee, checks, terms, buyer_precheck, message, footnote and caveats. The code below prints the gist of either: each fix of the plan, or the fee and each flagged offer check.
KEY="aws-offer-desk:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "offer" then (.fee | "\(.tier) \(.amount) net \(.net)"), (.checks[] | "\(.check) \(.subject) \(.change)")
else (.fixes[] | "\(.check) \(.subject) \(.change)") end' reply.json
import hashlib, time
key = "aws-offer-desk:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "offer":
print(reply["fee"]["tier"], reply["fee"]["amount"], reply["fee"]["net"])
for x in reply["checks"]:
print(x["check"], x["subject"], x["change"])
else:
for x in reply["fixes"]:
print(x["check"], x["change"])
import { createHash } from "node:crypto";
const key = `aws-offer-desk:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "offer") { console.log(reply.fee.tier, reply.fee.amount, reply.fee.net); for (const x of reply.checks) console.log(x.check, x.subject, x.change); }
else { for (const x of reply.fixes) console.log(x.check, x.change); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "aws-offer-desk:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, name, field := "fixes", "check", "change"
if reply["task"] == "offer" {
key, name, field = "checks", "check", "change"
}
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 = "aws-offer-desk:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("offer")) {
System.out.println(reply.path("fee").path("tier").asText() + " " + reply.path("fee").path("amount").asText());
for (JsonNode x : reply.path("checks")) System.out.println(x.path("check").asText() + " " + x.path("change").asText());
} else {
for (JsonNode x : reply.path("fixes")) System.out.println(x.path("check").asText() + " " + x.path("change").asText());
}
require "digest"
key = "aws-offer-desk:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "offer"
puts [reply["fee"]["tier"], reply["fee"]["amount"]].inspect
reply["checks"].each { |x| puts "#{x["check"]} #{x["change"]}" }
else
reply["fixes"].each { |x| puts "#{x["check"]} #{x["change"]}" }
end
$key = "aws-offer-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "offer") { echo $reply["fee"]["tier"], " ", $reply["fee"]["amount"], "\n"; foreach ($reply["checks"] as $x) echo $x["check"], " ", $x["change"], "\n"; }
else { foreach ($reply["fixes"] as $x) echo $x["check"], " ", $x["change"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"aws-offer-desk:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "offer") { Console.WriteLine($"{reply["fee"]!["tier"]} {reply["fee"]!["amount"]}"); foreach (var x in reply["checks"]!.AsArray()) Console.WriteLine($"{x!["check"]} {x["change"]}"); }
else { foreach (var x in reply["fixes"]!.AsArray()) Console.WriteLine($"{x!["check"]} {x["change"]}"); }
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: aws-offer-desk:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "plan", "listing": "Product: Ledgerline; pricing model: SaaS subscription; stage: Limited
...", "dimensions": "Dimensions (api name | display name | kind | unit | public price):
...", "offer": "Buyer: Corvane Freight; buyer country: United States
...", "carry": "", "facts": "The page's listing checks (...); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:
- account | seller_account | - | fail | our main production account ..."}
# - 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": "aws-offer-desk:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `aws-offer-desk:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "aws-offer-desk:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "aws-offer-desk:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "aws-offer-desk:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: aws-offer-desk:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"aws-offer-desk:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);