Run Ticket Ladder from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page computes the target, paid seats after comps, the posture, the persona ladder and its projection, and tracks the nine refund decisions - all free, in tickkit.js. The run writes the plan or the policy; the page then holds the reply to its own figures and to the decisions made (tickreply.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":"ticket-ladder"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. 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, event, ladder, facts) is missing. |
| 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 - plan (validate the ticket structure before sales open) and policy (needs a refund deadline and a transfer decision). A missing or unknown task is answered as plan. Both lanes send the same four fields.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or policy. If it is missing or neither, do plan and set "task": "plan". |
event | yes | the event, who runs it, the budget lines, the room and the comps, who pays for a ticket, the dates, what the organizers can staff, and any comparable events they hold. |
ladder | yes | the page's arithmetic: the target, paid capacity, the posture, every persona with its share, seats, price, gate price and where the price came from, the ratio, the gate, the group and invoice rules, and the projection at a full house. |
facts | yes | the page's picks (posture, gate, access rungs, bulk mechanics), the refund and transfer checklist with each answer or NOT DECIDED, the measurement picks, the calibration points and every finding tagged [fix], [check] or [info]. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
- The target comes from the budget, never from the ladder: total expenses minus sponsorship and other income is what tickets must raise. Comps come off capacity before pricing, counted from the agreements - there is no percentage rule for comps.
- Posture: free by design is a funding decision made upstream, and deletes the ladder, the gate and the access rungs; a nominal fee (10-20% of a commercial conference ticket in the region) spreads risk rather than funding, yet carries the full accounting and tax cost of charging anything; a community price covers what sponsorship does not (the published community target is 75-80%+ from sponsorship); a commercial price belongs to a vendor-run event priced against employer budgets. Free seats carry a no-show risk a price does not solve.
- The ladder splits by who pays - an employer's budget, the attendee's own money, an institution's - named after the payer, never after who deserves it. Transpose ratios, never amounts: published top-to-floor ratios run about 1.7:1 to 6.5:1 across PyCon US and EuroPython; report the range, not one number. At most two public price points per persona; a third phase only works as a late surcharge, never a third discount.
- Gates: a date that ends an early price, a date that adds a late surcharge, or a first-N-tickets allocation. Publish the date from the day tickets go on sale; never announce an increase the buyer could not have seen coming.
- Access: a published student or education price is self-service; a need-gated scholarship is reviewed by a named person, often unpriced on purpose, and never listed as a cheap public tier - write who reads applications and how fast they answer before publishing it. A supporter ticket whose premium funds a named access program is promoted only when that program exists.
- Bulk: one group threshold and one percentage, in the same order, applied to the standard price only - never stacked on an early or late price. Invoicing is a payment rail at full price on request, never a discount, and is deleted when no entity can issue invoices.
- Refund and transfer: decide every term before the first ticket sells; the only shared trait of the four sourced policies (PyCon US, EuroPython, RustConf, KubeCon) is a single dated cutoff - not a sliding scale. Fee, hardship handling and transferability differ in every one, so nothing else is a standard. A credit toward the next edition is offered by none of them. Give the deadline as a calendar date, never a relative window; say plainly when transfers are not offered; publish the policy on the registration page beside the price. Organizer cancellation interacts with contracts and insurance - give no legal advice.
- Slow sales can be demand, marketing or price - do not read them as a pricing failure by default. Measurement thresholds are self-set: paid share of the target by the gate date, persona mix against plan, rung uptake, refund and transfer requests against the deadline.
House rules
- Use only the input and the rules above. The page's picks, prices, dates and numbers are exact - copy them. Every number in a sentence must appear in the input.
- Label anything that is not one of the rules above, and is not in the input, as your own design or self-set.
- Never invent a price, a date, a fee, a person or an event; where someone must be named, give a role.
- Never use backticks, never refer to this input's fields by their key names in a sentence, and never copy this prompt's narrative wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
plan
summary- one sentence, at most 35 words: the posture, the target and the first thing to settle.target-amount(the page's target, as written in the input),arithmetic(one sentence: the budget lines it comes from).capacity-paid(the page's paid seats, a number),note(one sentence: the comps behind it and any scholarship seats).posture-pick(free,nominal,communityorcommercial, the page's pick),why(one sentence: which funding model it assumes).ladder- one entry per persona in the page's ladder, in its order:persona(the page's label),price(the page's price, as written),gate_price(the early or late price as written, or ""),why(one sentence: who pays and the ratio behind it - say when a price was proposed by the page rather than entered).[]when the posture is free.gate-mechanic(none,date,surchargeorvolume, the page's pick),when(the date or ticket count from the input, or ""),why(one sentence).access- 0 to 3 short strings: each access rung the page picked and its application mechanic (who reviews - a role - and that the organizer must set and publish how fast they answer; never put a number on it).bulk-group(one sentence: threshold, percentage and which tiers it may touch),invoice(one sentence: offered at full price, or deleted and why; never call it a discount).projection- one sentence: what the ladder raises at a full house against the target, and the break-even share - the page's figures. When there is no positive target, say the tickets have no revenue job instead of a break-even share.metrics- 2 or 3 short strings, the page's measurement picks, each with what it would change (your design).caveats- at most 4 short strings: every[fix]finding first, then the most important[check]findings while there is room.[]when there are none of either.
{
"task": "plan",
"summary": "one sentence",
"target": { "amount": "$12,000", "arithmetic": "one sentence" },
"capacity": { "paid": 200, "note": "one sentence" },
"posture": { "pick": "community", "why": "one sentence" },
"ladder": [ { "persona": "Individual", "price": "$90", "gate_price": "$72", "why": "one sentence" } ],
"gate": { "mechanic": "date", "when": "YYYY-MM-DD", "why": "one sentence" },
"access": [ "a rung and its mechanic" ],
"bulk": { "group": "one sentence", "invoice": "one sentence" },
"projection": "one sentence",
"metrics": [ "what to record and what it changes" ],
"caveats": [ "a caveat" ]
}
The page holds it to its own figures: the posture, the target, the paid seats, every persona's price and gate price and the gate must be the page's; the invoice rail may never be called a discount; no number may appear that the input does not hold.
policy
policy- the publishable wording: one short paragraph per topic (refunds, hardship, transfers, scholarship and comp tickets, if the event is cancelled), plain sentences, the deadline as the calendar date from the checklist and never as a relative window (no "N days before the event", not even beside the date), the fee exactly as decided. Use only decided answers. Where an item is NOT DECIDED, write [to be decided: the item] in its place - never fill it in.decisions- exactly 9 entries, one per checklist item in its order:item(the checklist's name),answer(the decided answer in a few words, or "not decided").open- the checklist items still NOT DECIDED, by name.[]when every item is decided.note- one sentence, at most 30 words, for the organizer: what to decide or check before publishing (organizer cancellation interacts with contracts and insurance - no legal advice).
{
"task": "policy",
"policy": "Refunds. ...\n\nTransfers. ...",
"decisions": [ { "item": "Refund deadline", "answer": "YYYY-MM-DD" } ],
"open": [ "Hardship waiver" ],
"note": "one sentence"
}
The page requires all nine checklist items in order, refuses an answer to an item that was not decided (it must come back as a placeholder), requires the deadline as a calendar date and the fee as decided, and refuses a relative window or a credit nobody decided.
Worked examples - every lane, every sample
These are the page's three free samples (fictional events, people and companies). Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped as the validating model wrote it; the last line is the page's own check. The meetup has no policy lane - no refund deadline was decided.
Riverbend PyDay (community, 300 seats) - plan
Posture community; target $16,500 from 240 paid seats; gate date; $20,301 at a full house; 0 refund decisions open.
Input:
{
"task": "plan",
"event": "Event: Riverbend PyDay; run by an independent community group; recurring yearly yes; currency $\nBudget: expenses $60,000; sponsorship $42,000; other income $1,500; free to attend by design no\nRoom: capacity 300; comps committed - sponsor 20, speakers 16, organizers 10, volunteers 14\nWho pays: Corporate (an employer's budget) 35%; Individual (the attendee's own money) 50%; Student (an institution or a student budget) 15%\nDates: today 2027-01-20; tickets on sale 2027-02-01; event 2027-06-12; ticket revenue needed by a hard date no\nCapacity to run it: an entity that can issue invoices yes; someone named to review scholarship applications yes; accessibility is a stated positioning claim yes; a named access program exists no\nComparable events the organizer holds: Last year's edition: $180 individual, $320 corporate, $60 student, sold out 3 weeks before the event (read 2026-12-02).",
"ladder": "Target: expenses $60,000 - sponsorship $42,000 - other income $1,500 = $16,500\nPaid capacity: 300 - 60 comps = 240 seats, of which 12 scholarship seats at $0, leaving 228 priced seats; break-even average $69 per seat\nPosture: community - Community price\nLadder (persona | payer | share | seats | price | gate price | source):\nCorporate | an employer's budget | 35% | 80 | $140 | early $112 | proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)\nIndividual | the attendee's own money | 50% | 114 | $85 | early $68 | proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)\nStudent | an institution or a student budget | 15% | 34 | $40 | early $32 | proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)\nTop-to-floor ratio 3.5:1 (published range 1.7:1 to 6.5:1)\nGate: date - Date-gated early price (a discount that expires) on 2027-04-15, 20% off, 40% of tickets assumed to sell at the early price (self-set)\nGroup: 15% off for 5 or more in the same order, on the standard price only - never stacked on the early or late price; 15% of corporate seats assumed in group orders (self-set); invoice rail offered\nProjection at a full house: $20,301 against the $16,500 target (123%); break-even at 81.3% of priced seats sold",
"facts": "Posture: community - Ticket revenue has a real job in the budget: price to cover what sponsorship does not.\nGate: date - An early price that ends on a published date - announced from the day tickets go on sale, and a forecasting checkpoint for the sale.\nAccess rungs: published - A published student or education price, self-identified (sometimes an institutional email check). scholarship - A need-gated scholarship route by application, reviewed by a named organizer, never listed as a public tier (12 seats at $0).\nBulk: group 15% off for 5 or more in the same order, on the standard price only - never stacked on the early or late price; invoice rail offered (full price, on request - a payment rail, never a discount)\nRefund and transfer checklist: 1. Refund deadline: 2027-05-22; 2. Administrative fee: $25; 3. Hardship waiver: the fee is waived for health and visa reasons; 4. Post-deadline requests: refused; 5. Transfer to another person: allowed until the refund deadline, with a fee if the two ticket prices differ; 6. Transfer within a company: allowed at any time; 7. Scholarship and comp tickets: not transferable; 8. Organizer cancellation: a full refund of the ticket price; 9. Where the policy is published: on the registration page beside the price\nMeasurement picks: Paid share of the revenue target by the gate date (self-set); Persona mix against plan (self-set); Scholarship applications received against granted (self-set)\nCalibration, ratios not amounts: PyCon US 2026 - Corporate $899 / Individual $469 / Student $139 (6.5:1); refunds to 1 May minus $50 ($25 students), waived for health and visa; EuroPython 2026 - Business EUR 500 / Personal 300 / Education 135 (3.7-3.8:1); late-bird step 1.5x after 26 June; full refund to 7 June; GopherCon - 5% off 5-9 tickets, 10% off 10+, standard tier only; devopsdays Halifax 2026 - 15% off for 5 or more in the same order; KubeCon NA 2026 - refunds to two weeks out minus 6%; free self-service transfer; RustConf 2026 - refunds to 15 days out, a later visa and travel deadline, generally non-transferable\nFindings (2):\n[info] sponsor_share (target): Sponsorship covers 70% of the budget; the published community-conference target is 75-80%+ from sponsorship - the sponsor rate card is set upstream, not here.\n[info] commitment (posture): A recurring event: consider a published community-price commitment (PyCon US held individual and corporate prices flat across venue step-ups and cut its student price by more than 40% over five years)."
}
Reply:
{
"task": "plan",
"summary": "The posture is community, the target is $16,500 to cover what sponsorship does not, and the first thing to settle is confirming the proposed persona prices before publishing them.",
"target": {
"amount": "$16,500",
"arithmetic": "The target is the event's $60,000 in expenses minus $42,000 in sponsorship and $1,500 in other income."
},
"capacity": {
"paid": 240,
"note": "The 240 paid seats follow the room's 300 capacity minus 60 comps (20 sponsor, 16 speaker, 10 organizer, 14 volunteer), with 12 of them scholarship seats at $0 leaving 228 priced seats."
},
"posture": {
"pick": "community",
"why": "Community pricing assumes ticket revenue covers what the $42,000 in sponsorship and $1,500 in other income do not."
},
"ladder": [
{
"persona": "Corporate",
"price": "$140",
"gate_price": "$112",
"why": "Corporate tickets are paid from an employer's budget and sit at the top of the ladder's proposed 3.5:1 top-to-floor ratio, sized by the page to break even at 85% sold."
},
{
"persona": "Individual",
"price": "$85",
"gate_price": "$68",
"why": "Individual tickets are paid from the attendee's own money and were proposed by the page within the 3.5:1 top-to-floor ratio, sized to break even at 85% sold."
},
{
"persona": "Student",
"price": "$40",
"gate_price": "$32",
"why": "Student tickets are paid from an institution or student budget and sit at the floor of the proposed 3.5:1 top-to-floor ratio, sized to break even at 85% sold."
}
],
"gate": {
"mechanic": "date",
"when": "2027-04-15",
"why": "This date-gated early price, worth 20% off, is announced from the day tickets go on sale and used as a forecasting checkpoint, with 40% of tickets assumed to sell at the early price."
},
"access": [
"A published student price, self-identified at purchase with no application review.",
"A need-gated scholarship rung covering 12 seats at $0, reviewed by a named organizer who must set and publish how fast they answer applications."
],
"bulk": {
"group": "A group discount of 15% off applies to 5 or more tickets in the same order, on the standard price only, never stacked on the early price.",
"invoice": "Invoicing is offered as a payment rail at full price, on request, since an entity that can issue invoices exists."
},
"projection": "At a full house the ladder raises $20,301 against the $16,500 target (123%), with break-even at 81.3% of priced seats sold.",
"metrics": [
"Paid share of the revenue target by the gate date - a shortfall would prompt a marketing push rather than a reflex price change, since slow sales are not read as a pricing failure by default (design).",
"Persona mix against the 35% Corporate, 50% Individual, 15% Student plan - a persona running heavy or light would flag whether seats shift between personas next cycle (design).",
"Scholarship applications received against granted - a high volume would flag whether the organizer needs more reviewers or a faster published response time (design)."
],
"caveats": []
}
The page's check: kept - nothing to flag
Riverbend PyDay (community, 300 seats) - policy
Posture community; target $16,500 from 240 paid seats; gate date; $20,301 at a full house; 0 refund decisions open.
Input:
{
"task": "policy",
"event": "Event: Riverbend PyDay; run by an independent community group; recurring yearly yes; currency $\nBudget: expenses $60,000; sponsorship $42,000; other income $1,500; free to attend by design no\nRoom: capacity 300; comps committed - sponsor 20, speakers 16, organizers 10, volunteers 14\nWho pays: Corporate (an employer's budget) 35%; Individual (the attendee's own money) 50%; Student (an institution or a student budget) 15%\nDates: today 2027-01-20; tickets on sale 2027-02-01; event 2027-06-12; ticket revenue needed by a hard date no\nCapacity to run it: an entity that can issue invoices yes; someone named to review scholarship applications yes; accessibility is a stated positioning claim yes; a named access program exists no\nComparable events the organizer holds: Last year's edition: $180 individual, $320 corporate, $60 student, sold out 3 weeks before the event (read 2026-12-02).",
"ladder": "Target: expenses $60,000 - sponsorship $42,000 - other income $1,500 = $16,500\nPaid capacity: 300 - 60 comps = 240 seats, of which 12 scholarship seats at $0, leaving 228 priced seats; break-even average $69 per seat\nPosture: community - Community price\nLadder (persona | payer | share | seats | price | gate price | source):\nCorporate | an employer's budget | 35% | 80 | $140 | early $112 | proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)\nIndividual | the attendee's own money | 50% | 114 | $85 | early $68 | proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)\nStudent | an institution or a student budget | 15% | 34 | $40 | early $32 | proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)\nTop-to-floor ratio 3.5:1 (published range 1.7:1 to 6.5:1)\nGate: date - Date-gated early price (a discount that expires) on 2027-04-15, 20% off, 40% of tickets assumed to sell at the early price (self-set)\nGroup: 15% off for 5 or more in the same order, on the standard price only - never stacked on the early or late price; 15% of corporate seats assumed in group orders (self-set); invoice rail offered\nProjection at a full house: $20,301 against the $16,500 target (123%); break-even at 81.3% of priced seats sold",
"facts": "Posture: community - Ticket revenue has a real job in the budget: price to cover what sponsorship does not.\nGate: date - An early price that ends on a published date - announced from the day tickets go on sale, and a forecasting checkpoint for the sale.\nAccess rungs: published - A published student or education price, self-identified (sometimes an institutional email check). scholarship - A need-gated scholarship route by application, reviewed by a named organizer, never listed as a public tier (12 seats at $0).\nBulk: group 15% off for 5 or more in the same order, on the standard price only - never stacked on the early or late price; invoice rail offered (full price, on request - a payment rail, never a discount)\nRefund and transfer checklist: 1. Refund deadline: 2027-05-22; 2. Administrative fee: $25; 3. Hardship waiver: the fee is waived for health and visa reasons; 4. Post-deadline requests: refused; 5. Transfer to another person: allowed until the refund deadline, with a fee if the two ticket prices differ; 6. Transfer within a company: allowed at any time; 7. Scholarship and comp tickets: not transferable; 8. Organizer cancellation: a full refund of the ticket price; 9. Where the policy is published: on the registration page beside the price\nMeasurement picks: Paid share of the revenue target by the gate date (self-set); Persona mix against plan (self-set); Scholarship applications received against granted (self-set)\nCalibration, ratios not amounts: PyCon US 2026 - Corporate $899 / Individual $469 / Student $139 (6.5:1); refunds to 1 May minus $50 ($25 students), waived for health and visa; EuroPython 2026 - Business EUR 500 / Personal 300 / Education 135 (3.7-3.8:1); late-bird step 1.5x after 26 June; full refund to 7 June; GopherCon - 5% off 5-9 tickets, 10% off 10+, standard tier only; devopsdays Halifax 2026 - 15% off for 5 or more in the same order; KubeCon NA 2026 - refunds to two weeks out minus 6%; free self-service transfer; RustConf 2026 - refunds to 15 days out, a later visa and travel deadline, generally non-transferable\nFindings (2):\n[info] sponsor_share (target): Sponsorship covers 70% of the budget; the published community-conference target is 75-80%+ from sponsorship - the sponsor rate card is set upstream, not here.\n[info] commitment (posture): A recurring event: consider a published community-price commitment (PyCon US held individual and corporate prices flat across venue step-ups and cut its student price by more than 40% over five years)."
}
Reply:
{
"task": "policy",
"policy": "Refunds. Refund requests are accepted through May 22, 2027. After that date, no refund requests will be accepted. An administrative fee of $25 applies to every refund.\n\nHardship. The administrative fee is waived for health and visa reasons.\n\nTransfers. A ticket may be transferred to another person through May 22, 2027; a fee applies if the two ticket prices differ. A ticket may be transferred within the same company at any time.\n\nScholarship and comp tickets. Scholarship and comp tickets are not transferable.\n\nIf the event is cancelled. Ticket holders receive a full refund of the ticket price.",
"decisions": [
{
"item": "Refund deadline",
"answer": "2027-05-22"
},
{
"item": "Administrative fee",
"answer": "$25"
},
{
"item": "Hardship waiver",
"answer": "fee waived for health and visa reasons"
},
{
"item": "Post-deadline requests",
"answer": "refused"
},
{
"item": "Transfer to another person",
"answer": "allowed until the refund deadline, fee if prices differ"
},
{
"item": "Transfer within a company",
"answer": "allowed at any time"
},
{
"item": "Scholarship and comp tickets",
"answer": "not transferable"
},
{
"item": "Organizer cancellation",
"answer": "full refund of the ticket price"
},
{
"item": "Where the policy is published",
"answer": "registration page beside the price"
}
],
"open": [],
"note": "Before publishing, confirm the organizer cancellation refund commitment is consistent with event contracts and any insurance coverage; this is not legal advice."
}
The page's check: kept - nothing to flag
Northgate Cloud Summit (vendor, 900 seats) - plan
Posture commercial; target $580,000 from 770 paid seats; gate surcharge; $911,459 at a full house; 3 refund decisions open.
Input:
{
"task": "plan",
"event": "Event: Northgate Cloud Summit; run by a vendor or company; recurring yearly yes; currency $\nBudget: expenses $900,000; sponsorship $320,000; other income $0; free to attend by design no\nRoom: capacity 900; comps committed - sponsor 60, speakers 45, organizers 25, volunteers 0\nWho pays: Corporate (an employer's budget) 75%; Individual (the attendee's own money) 20%; Student (an institution or a student budget) 5%\nDates: today 2027-02-15; tickets on sale 2027-03-01; event 2027-09-21; ticket revenue needed by a hard date yes\nCapacity to run it: an entity that can issue invoices no; someone named to review scholarship applications no; accessibility is a stated positioning claim yes; a named access program exists no\nComparable events the organizer holds: none given",
"ladder": "Target: expenses $900,000 - sponsorship $320,000 - other income $0 = $580,000\nPaid capacity: 900 - 130 comps = 770 seats; break-even average $753 per seat\nPosture: commercial - Commercial price\nLadder (persona | payer | share | seats | price | gate price | source):\nCorporate | an employer's budget | 75% | 578 | $1,290 | late $1,677 | entered\nIndividual | the attendee's own money | 20% | 154 | $690 | late $897 | entered\nStudent | an institution or a student budget | 5% | 39 | $190 | late $247 | entered\nTop-to-floor ratio 6.8:1 (published range 1.7:1 to 6.5:1)\nGate: surcharge - Date-gated late surcharge (a price that rises on a published date) on 2027-08-01, 30% on top, 30% of tickets assumed to sell at the late price (self-set)\nGroup: 10% off for 10 or more in the same order, on every tier; 30% of corporate seats assumed in group orders (self-set); invoice rail deleted\nProjection at a full house: $911,459 against the $580,000 target (157.1%); break-even at 63.6% of priced seats sold",
"facts": "Posture: commercial - A vendor or company-run event priced against employer budgets - the starved rung, promoted by who runs it.\nGate: surcharge - A late price that lands on a published date on top of the regular price (EuroPython's late-bird step was 1.5x).\nAccess rungs: published - A published student or education price, self-identified (sometimes an institutional email check). scholarship - A need-gated scholarship route by application, reviewed by a named organizer, never listed as a public tier.\nBulk: group 10% off for 10 or more in the same order, on every tier; invoice rail deleted\nRefund and transfer checklist: 1. Refund deadline: 2027-09-07; 2. Administrative fee: 6%; 3. Hardship waiver: NOT DECIDED; 4. Post-deadline requests: converted into a free transfer; 5. Transfer to another person: allowed, free, until the event; 6. Transfer within a company: allowed; 7. Scholarship and comp tickets: NOT DECIDED; 8. Organizer cancellation: NOT DECIDED; 9. Where the policy is published: on the registration page beside the price\nMeasurement picks: Paid share of the revenue target by the gate date (self-set); Persona mix against plan (self-set); Scholarship applications received against granted (self-set)\nCalibration, ratios not amounts: PyCon US 2026 - Corporate $899 / Individual $469 / Student $139 (6.5:1); refunds to 1 May minus $50 ($25 students), waived for health and visa; EuroPython 2026 - Business EUR 500 / Personal 300 / Education 135 (3.7-3.8:1); late-bird step 1.5x after 26 June; full refund to 7 June; GopherCon - 5% off 5-9 tickets, 10% off 10+, standard tier only; devopsdays Halifax 2026 - 15% off for 5 or more in the same order; KubeCon NA 2026 - refunds to two weeks out minus 6%; free self-service transfer; RustConf 2026 - refunds to 15 days out, a later visa and travel deadline, generally non-transferable\nFindings (5):\n[check] ratio (ladder): Top-to-floor ratio 6.8:1 is outside the published 1.7:1 to 6.5:1 range across PyCon US and EuroPython - transpose a ratio, never an amount.\n[check] stacking (bulk): The group discount also applies to the late price - published practice (GopherCon) applies it to the standard price only; say which tiers it touches before anyone asks.\n[info] no_invoice (bulk): No entity can issue invoices, so the invoice rail is deleted - a company that cannot pay by card will not buy; that is a payment-handling problem upstream, not a price.\n[fix] unstaffed (access): Accessibility is a stated claim but no one is named to review scholarship applications - an unanswered application is worse than no route at all.\n[check] undecided (policy): Not decided yet: Hardship waiver, Scholarship and comp tickets, Organizer cancellation - a stated silence is a decision, an unstated one is a surprise."
}
Reply:
{
"task": "plan",
"summary": "This is a commercial-priced event needing $580,000 from tickets; the first thing to settle is naming who will review scholarship applications before publishing the accessibility claim.",
"target": {
"amount": "$580,000",
"arithmetic": "The target is the event's $900,000 in expenses minus $320,000 in sponsorship and $0 in other income."
},
"capacity": {
"paid": 770,
"note": "Paid capacity of 770 comes from the 900-seat room minus 130 comps for sponsors, speakers and organizers, with no separate scholarship seats broken out of that capacity."
},
"posture": {
"pick": "commercial",
"why": "A commercial price fits a vendor-run event priced against employer budgets, since Corporate seats paid from an employer's budget make up 75% of the ladder."
},
"ladder": [
{
"persona": "Corporate",
"price": "$1,290",
"gate_price": "$1,677",
"why": "Corporate seats are paid from an employer's budget and sit at the top of the ladder's 6.8:1 top-to-floor ratio."
},
{
"persona": "Individual",
"price": "$690",
"gate_price": "$897",
"why": "Individual seats are paid from the attendee's own money and sit in the middle of the ladder, between Corporate and Student."
},
{
"persona": "Student",
"price": "$190",
"gate_price": "$247",
"why": "Student seats are paid from an institution or a student budget and set the floor of the ladder's 6.8:1 top-to-floor ratio."
}
],
"gate": {
"mechanic": "surcharge",
"when": "2027-08-01",
"why": "A late surcharge of 30% applies to seats sold after this published date, which buyers can already see when tickets go on sale on 2027-03-01."
},
"access": [
"Published student price, self-service and not reviewed by anyone.",
"Need-gated scholarship, reviewed by a named organizer who must set and publish how fast applications are answered, and never listed as a public tier."
],
"bulk": {
"group": "A 10% discount applies to orders of 10 or more tickets placed in the same order, applied to every tier.",
"invoice": "Invoicing is deleted, not offered, because no entity can issue invoices for buyers who cannot pay by card."
},
"projection": "At a full house the ladder raises $911,459 against the $580,000 target (157.1%), breaking even once 63.6% of priced seats are sold.",
"metrics": [
"Paid share of the $580,000 target measured by 2027-08-01 - if behind plan, it flags whether to revisit the surcharge timing or the group terms.",
"Persona mix against the 75%, 20% and 5% plan - if Corporate lags, it flags whether to revisit the Corporate price or outreach to employers.",
"Scholarship applications received against granted - if unanswered requests pile up, it flags the need for a named reviewer and a published response time."
],
"caveats": [
"No one is named yet to review scholarship applications, even though accessibility is a stated claim for this event.",
"The ladder's top-to-floor ratio of 6.8:1 falls outside the published 1.7:1 to 6.5:1 range.",
"The 10% group discount is applied to the late price as well as the standard price, where published practice reserves it for the standard price only.",
"The hardship waiver, scholarship and comp ticket handling, and organizer cancellation terms are not yet decided."
]
}
The page's check: kept - nothing to flag
Northgate Cloud Summit (vendor, 900 seats) - policy
Posture commercial; target $580,000 from 770 paid seats; gate surcharge; $911,459 at a full house; 3 refund decisions open.
Input:
{
"task": "policy",
"event": "Event: Northgate Cloud Summit; run by a vendor or company; recurring yearly yes; currency $\nBudget: expenses $900,000; sponsorship $320,000; other income $0; free to attend by design no\nRoom: capacity 900; comps committed - sponsor 60, speakers 45, organizers 25, volunteers 0\nWho pays: Corporate (an employer's budget) 75%; Individual (the attendee's own money) 20%; Student (an institution or a student budget) 5%\nDates: today 2027-02-15; tickets on sale 2027-03-01; event 2027-09-21; ticket revenue needed by a hard date yes\nCapacity to run it: an entity that can issue invoices no; someone named to review scholarship applications no; accessibility is a stated positioning claim yes; a named access program exists no\nComparable events the organizer holds: none given",
"ladder": "Target: expenses $900,000 - sponsorship $320,000 - other income $0 = $580,000\nPaid capacity: 900 - 130 comps = 770 seats; break-even average $753 per seat\nPosture: commercial - Commercial price\nLadder (persona | payer | share | seats | price | gate price | source):\nCorporate | an employer's budget | 75% | 578 | $1,290 | late $1,677 | entered\nIndividual | the attendee's own money | 20% | 154 | $690 | late $897 | entered\nStudent | an institution or a student budget | 5% | 39 | $190 | late $247 | entered\nTop-to-floor ratio 6.8:1 (published range 1.7:1 to 6.5:1)\nGate: surcharge - Date-gated late surcharge (a price that rises on a published date) on 2027-08-01, 30% on top, 30% of tickets assumed to sell at the late price (self-set)\nGroup: 10% off for 10 or more in the same order, on every tier; 30% of corporate seats assumed in group orders (self-set); invoice rail deleted\nProjection at a full house: $911,459 against the $580,000 target (157.1%); break-even at 63.6% of priced seats sold",
"facts": "Posture: commercial - A vendor or company-run event priced against employer budgets - the starved rung, promoted by who runs it.\nGate: surcharge - A late price that lands on a published date on top of the regular price (EuroPython's late-bird step was 1.5x).\nAccess rungs: published - A published student or education price, self-identified (sometimes an institutional email check). scholarship - A need-gated scholarship route by application, reviewed by a named organizer, never listed as a public tier.\nBulk: group 10% off for 10 or more in the same order, on every tier; invoice rail deleted\nRefund and transfer checklist: 1. Refund deadline: 2027-09-07; 2. Administrative fee: 6%; 3. Hardship waiver: NOT DECIDED; 4. Post-deadline requests: converted into a free transfer; 5. Transfer to another person: allowed, free, until the event; 6. Transfer within a company: allowed; 7. Scholarship and comp tickets: NOT DECIDED; 8. Organizer cancellation: NOT DECIDED; 9. Where the policy is published: on the registration page beside the price\nMeasurement picks: Paid share of the revenue target by the gate date (self-set); Persona mix against plan (self-set); Scholarship applications received against granted (self-set)\nCalibration, ratios not amounts: PyCon US 2026 - Corporate $899 / Individual $469 / Student $139 (6.5:1); refunds to 1 May minus $50 ($25 students), waived for health and visa; EuroPython 2026 - Business EUR 500 / Personal 300 / Education 135 (3.7-3.8:1); late-bird step 1.5x after 26 June; full refund to 7 June; GopherCon - 5% off 5-9 tickets, 10% off 10+, standard tier only; devopsdays Halifax 2026 - 15% off for 5 or more in the same order; KubeCon NA 2026 - refunds to two weeks out minus 6%; free self-service transfer; RustConf 2026 - refunds to 15 days out, a later visa and travel deadline, generally non-transferable\nFindings (5):\n[check] ratio (ladder): Top-to-floor ratio 6.8:1 is outside the published 1.7:1 to 6.5:1 range across PyCon US and EuroPython - transpose a ratio, never an amount.\n[check] stacking (bulk): The group discount also applies to the late price - published practice (GopherCon) applies it to the standard price only; say which tiers it touches before anyone asks.\n[info] no_invoice (bulk): No entity can issue invoices, so the invoice rail is deleted - a company that cannot pay by card will not buy; that is a payment-handling problem upstream, not a price.\n[fix] unstaffed (access): Accessibility is a stated claim but no one is named to review scholarship applications - an unanswered application is worse than no route at all.\n[check] undecided (policy): Not decided yet: Hardship waiver, Scholarship and comp tickets, Organizer cancellation - a stated silence is a decision, an unstated one is a surprise."
}
Reply:
{
"task": "policy",
"policy": "Refunds. Requests received by 2027-09-07 are honored minus a 6% administrative fee. Requests made after that date are converted into a free transfer instead of a refund.\n\nHardship. [to be decided: hardship waiver]\n\nTransfers. A ticket may be transferred to another person at no charge any time until the event. A ticket may also be transferred to another attendee within the same company.\n\nScholarship and comp tickets. [to be decided: scholarship and comp tickets]\n\nIf the event is cancelled. [to be decided: organizer cancellation]",
"decisions": [
{
"item": "Refund deadline",
"answer": "2027-09-07"
},
{
"item": "Administrative fee",
"answer": "6%"
},
{
"item": "Hardship waiver",
"answer": "not decided"
},
{
"item": "Post-deadline requests",
"answer": "converted into a free transfer"
},
{
"item": "Transfer to another person",
"answer": "allowed, free, until the event"
},
{
"item": "Transfer within a company",
"answer": "allowed"
},
{
"item": "Scholarship and comp tickets",
"answer": "not decided"
},
{
"item": "Organizer cancellation",
"answer": "not decided"
},
{
"item": "Where the policy is published",
"answer": "on the registration page beside the price"
}
],
"open": [
"Hardship waiver",
"Scholarship and comp tickets",
"Organizer cancellation"
],
"note": "Decide the hardship waiver, scholarship and comp ticket handling, and organizer cancellation terms before publishing, and check cancellation terms against contracts and insurance with counsel."
}
The page's check: kept - nothing to flag
Harbor Rust Night (sponsor-funded meetup) - plan
Posture nominal; target -$200 from 110 paid seats; gate none; $9,900 at a full house; 8 refund decisions open.
Input:
{
"task": "plan",
"event": "Event: Harbor Rust Night; run by an independent community group; recurring yearly no; currency $\nBudget: expenses $3,000; sponsorship $3,200; other income $0; free to attend by design no\nRoom: capacity 120; comps committed - sponsor 4, speakers 3, organizers 3, volunteers 0\nWho pays: Individual (the attendee's own money) 100%\nDates: today 2027-02-20; tickets on sale 2027-03-01; event 2027-04-08; ticket revenue needed by a hard date no\nCapacity to run it: an entity that can issue invoices no; someone named to review scholarship applications no; accessibility is a stated positioning claim no; a named access program exists no\nComparable events the organizer holds: none given; a commercial conference ticket in the region costs $600",
"ladder": "Target: expenses $3,000 - sponsorship $3,200 - other income $0 = -$200\nPaid capacity: 120 - 10 comps = 110 seats\nPosture: nominal - Nominal fee\nLadder (persona | payer | share | seats | price | gate price | source):\nIndividual | the attendee's own money | 100% | 110 | $90 | - | proposed: 15% of the commercial reference (self-set midpoint of the 10-20% rule)\nGate: none - No gate - one price per persona\nGroup: none; invoice rail n/a\nProjection at a full house: $9,900",
"facts": "Posture: nominal - Sponsorship and other income already cover the budget, so the ticket has no revenue job: a token price turns an RSVP into a headcount you can commit catering against.\nGate: none - One price per persona, no gate.\nAccess rungs: none\nBulk: group none; invoice rail n/a\nRefund and transfer checklist: 1. Refund deadline: NOT DECIDED; 2. Administrative fee: NOT DECIDED; 3. Hardship waiver: NOT DECIDED; 4. Post-deadline requests: NOT DECIDED; 5. Transfer to another person: NOT DECIDED; 6. Transfer within a company: NOT DECIDED; 7. Scholarship and comp tickets: NOT DECIDED; 8. Organizer cancellation: NOT DECIDED; 9. Where the policy is published: on the registration page beside the price\nMeasurement picks: Refund and transfer request volume against the deadline (self-set); Tickets sold against paid capacity, week by week (self-set)\nCalibration, ratios not amounts: PyCon US 2026 - Corporate $899 / Individual $469 / Student $139 (6.5:1); refunds to 1 May minus $50 ($25 students), waived for health and visa; EuroPython 2026 - Business EUR 500 / Personal 300 / Education 135 (3.7-3.8:1); late-bird step 1.5x after 26 June; full refund to 7 June; GopherCon - 5% off 5-9 tickets, 10% off 10+, standard tier only; devopsdays Halifax 2026 - 15% off for 5 or more in the same order; KubeCon NA 2026 - refunds to two weeks out minus 6%; free self-service transfer; RustConf 2026 - refunds to 15 days out, a later visa and travel deadline, generally non-transferable\nFindings (3):\n[info] nominal (posture): Sponsorship and other income cover the budget, so the ticket's job is risk-spreading, not funding - a nominal fee still carries the full accounting and tax cost of charging anything.\n[fix] no_refund (policy): No refund deadline - write the refund and transfer policy before the first ticket sells.\n[check] undecided (policy): Not decided yet: Refund deadline, Administrative fee, Hardship waiver, Post-deadline requests, Transfer to another person, Transfer within a company, Scholarship and comp tickets, Organizer cancellation - a stated silence is a decision, an unstated one is a surprise."
}
Reply:
{
"task": "plan",
"summary": "Nominal fee event with a -$200 target - sponsorship and other income already cover the budget - so the first thing to settle is the refund deadline, still not decided.",
"target": {
"amount": "-$200",
"arithmetic": "The target equals $3,000 in expenses minus $3,200 in sponsorship minus $0 in other income, which comes to -$200."
},
"capacity": {
"paid": 110,
"note": "Paid capacity is 120 seats minus 10 comps (4 sponsor, 3 speakers, 3 organizers, 0 volunteers), with no scholarship seats set aside."
},
"posture": {
"pick": "nominal",
"why": "The nominal pick assumes sponsorship and other income already cover the budget, so the fee spreads no-show risk instead of funding the event."
},
"ladder": [
{
"persona": "Individual",
"price": "$90",
"gate_price": "",
"why": "Individual pays 100% from their own money; the page proposed the $90 price as 15% of the $600 commercial reference rather than entering it directly, and the ladder has no other persona to set a ratio against."
}
],
"gate": {
"mechanic": "none",
"when": "",
"why": "The page sets no gate, so the $90 price applies to all 110 seats with nothing to publish as a cutoff."
},
"access": [],
"bulk": {
"group": "No group discount threshold or percentage is set for this event.",
"invoice": "The invoice rail is deleted because no entity can issue invoices for this event."
},
"projection": "At a full house, the ladder raises $9,900, but since the target is -$200 and sponsorship and other income already exceed expenses, the tickets have no revenue job to do.",
"metrics": [
"Refund and transfer request volume against the deadline (self-set) - a spike close to the deadline would prompt tightening the fee or hardship terms next time.",
"Tickets sold against the 110-seat paid capacity, week by week (self-set) - slow early sales would prompt more marketing rather than an assumed price cut."
],
"caveats": [
"No refund deadline is set - write the refund and transfer policy before the first ticket sells.",
"Several refund and transfer checklist items remain not decided (refund deadline, administrative fee, hardship waiver, post-deadline requests, transfer to another person, transfer within a company, scholarship and comp tickets, organizer cancellation) - a stated silence is a decision, an unstated one is a surprise."
]
}
The page's check: kept - nothing to flag
The ladder CSV for the first sample
persona,payer,share,seats,price,early_price,late_price,projected_revenue,source
Corporate,an employer's budget,35%,80,140,112,,10127,"proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)"
Individual,the attendee's own money,50%,114,85,68,,8915,"proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)"
Student,an institution or a student budget,15%,34,40,32,,1259,"proposed: EuroPython 2026 ratio, sized to break even at 85% sold (self-set)"
Scholarship,need-gated,,12,0,,,0,entered
Build the input yourself
Load the engine in node (global.window = global; require("./tickkit.js"); require("./tickreply.js")), then:
const K = TickKit, R = TickReply;
const f = K.form({ name: "My Conf", expenses: "60000", sponsorship: "42000", capacity: "300", compSpeakers: "16", compSponsor: "20",
mixEmployer: "35", mixSelf: "50", mixStudent: "15", gate: "date", gateDate: "2027-04-15", gatePct: "20", refundDate: "2027-05-22", transfer: "fee" });
// f.target, f.seats, f.posture (free | nominal | community | commercial), f.ladder: [{label, price, early, late, seats, revenue, source}]
// f.full, f.coverage, f.breakEven, f.topFloor, f.checklist: [{item, answer, decided}], f.undecided, f.findings
K.ladderCSV(f) // persona, payer, share, seats, price, early_price, late_price, ...
const b = K.buildInput(f, "plan"); // or "policy" (needs a refund deadline and a transfer decision)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://ticket-ladder.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 = "paste-your-token-here" # from https://ticket-ladder.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"]
// ticket-ladder.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://ticket-ladder.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 = "paste-your-token-here" // from https://ticket-ladder.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
}
// TicketLadder.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 TicketLadder {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://ticket-ladder.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 = "paste-your-token-here" # from https://ticket-ladder.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 = "paste-your-token-here"; // from https://ticket-ladder.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 = "paste-your-token-here"; // from https://ticket-ladder.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":"ticket-ladder"}' | 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": "ticket-ladder"})["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: "ticket-ladder" })).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": "ticket-ladder"}, 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", "ticket-ladder"), 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: "ticket-ladder" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "ticket-ladder"])["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 = "ticket-ladder" }))!["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", "event": "Event: ...; run by an independent community group ...", "ladder": "Target: expenses $60,000 - sponsorship $42,000 ...", "facts": "Posture: community - ..."}
# - 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", "event": "Event: ...; run by an independent community group ...", "ladder": "Target: expenses $60,000 - sponsorship $42,000 ...", "facts": "Posture: community - ..."}
# - 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 "policy", event, ladder, 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 ticket-ladder:<task>:<inputHash>:a<attempt> - the lane is part of the key - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A plan carries summary, ladder, projection and caveats among its sections; a policy carries policy, decisions, open and note. The code below prints either.
KEY="ticket-ladder:plan:$(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 '.summary // .format, .note // .projection, ((.ladder // .decisions)[] | "\(.persona // .item): \(.price // .answer)")' reply.json
jq -r '.caveats // .note // empty' reply.json # caveats (plan) or the note for the organizer (policy)
import hashlib, time
key = "ticket-ladder:" + 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.get("summary"))
print(reply.get("note") or reply.get("projection", ""))
for s in reply.get("ladder") or reply.get("decisions") or []:
print(s.get("persona", s.get("item")), "-", s.get("price", s.get("answer")))
if reply.get("caveats"):
print("caveats:", reply["caveats"])
import { createHash } from "node:crypto";
const key = `ticket-ladder:${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.summary);
console.log(reply.note ?? reply.projection);
for (const s of reply.ladder || reply.decisions || []) console.log(s.persona ?? s.item, "-", s.price ?? s.answer);
if (reply.caveats) console.log("caveats:", reply.caveats);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "ticket-ladder:" + 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)
items, _ := reply["ladder"].([]any) // a policy lists "decisions" instead
fmt.Println(j["charged_credits"], j["truncated"], reply["summary"])
fmt.Println(reply["note"], reply["projection"])
for _, x := range items {
c := x.(map[string]any)
fmt.Println(c["persona"], "-", c["status"])
}
if fix, ok := reply["caveats"].(string); ok && fix != "" {
fmt.Println("caveats:", fix)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "ticket-ladder:" + 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.get("summary"));
System.out.println(reply.path("note").asText(reply.path("projection").asText()));
for (JsonNode s : reply.has("ladder") ? reply.path("ladder") : reply.path("decisions")) System.out.println(s.path("persona").asText(s.path("item").asText()) + " - " + s.path("status").asText(s.path("answer").asText()));
if (!reply.path("caveats").asText().isEmpty()) System.out.println("caveats: " + reply.get("caveats").asText());
require "digest"
key = "ticket-ladder:#{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["summary"]].inspect
puts reply["note"] || reply["projection"]
(reply["ladder"] || reply["decisions"] || []).each { |s| puts "#{s["persona"] || s["item"]} - #{s["price"] || s["answer"]}" }
puts "caveats: #{reply["caveats"]}" unless reply["caveats"].to_s.empty?
$key = "ticket-ladder:" . $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["summary"] ?? ""), "\n";
echo $reply["note"] ?? $reply["projection"] ?? "", "\n";
foreach ($reply["ladder"] ?? $reply["decisions"] ?? [] as $s) echo $s["persona"] ?? $s["item"], " - ", $s["price"] ?? $s["answer"], "\n";
if (!empty($reply["caveats"])) echo "caveats: ", $reply["caveats"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"ticket-ladder:{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>());
Console.WriteLine($"{j["charged_credits"]} {reply!["summary"]}");
Console.WriteLine($"{reply["note"] ?? reply["projection"]}");
foreach (var s in (reply["ladder"] ?? reply["decisions"]) as JsonArray ?? new JsonArray()) Console.WriteLine($"{s!["persona"] ?? s["item"]} - {s["price"] ?? s["answer"]}");
if (reply["caveats"] is JsonNode fix && fix.GetValue<string>() != "") Console.WriteLine("caveats: " + fix);
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: ticket-ladder:plan:$(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", "event": "Event: ...; run by an independent community group ...", "ladder": "Target: expenses $60,000 - sponsorship $42,000 ...", "facts": "Posture: community - ..."}
# - 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": "ticket-ladder:plan:" + 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": `ticket-ladder:plan:${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", "ticket-ladder:plan:"+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", "ticket-ladder:plan:" + 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" => "ticket-ladder:plan:#{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: ticket-ladder:plan:" . 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", $"ticket-ladder:plan:{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);