Run Prize Shape from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page decides the medium before the size, checks the pool against the Prizes line, counts awards and units, drafts the eligibility paragraph and routes every exposure - all free, in prizekit.js. The run writes the structure or the brief section; the page then holds the reply to its own picks (prizereply.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":"prize-shape"}), 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, awards, 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 - structure (the prize structure to validate) and brief (the prize section to publish; unavailable while a prize uses a removed medium). A missing or unknown task is answered as structure. Both lanes send the same four fields.
The input
| Field | Required | What |
|---|---|---|
task | yes | structure or brief. If it is missing or neither, do structure and set "task": "structure". |
event | yes | the event, its cadence and intent, the Prizes line and team cap, who holds the money and who reviews, who might win, the handover, leftover and roster rules, any continuing condition, and the publication and close dates. |
awards | yes | the tracks as published, every award with its medium and value, and the page's draft eligibility-to-receive paragraph. |
facts | yes | the page's medium decision, pool, split, units, gates, payout rung, the exposure categories with their routes, the pass threshold, the signals to record 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
- You own the prize - medium, pool, split, eligibility to receive, payout mechanic. The rank belongs to judging: never re-open a placement or let a prize's size influence who won. The rules document belongs to the brief: you hand it the eligibility-to-receive text. The ceiling belongs to the budget's Prizes line: never size a pool against no line.
- Medium before size. Five rungs: gear (the default - high perceived value and no payment rail; order early, many items get backordered, keep receipts), credits and vouchers (cheap when a sponsor holds them; publish the expiry, the redemption region and the account a winner must create; treated like cash for reporting), cash (cheapest to procure, dearest to hand over; deleted when a winner could be a minor or outside the organizer's country, or there is no legal entity and no fiscal host), opportunity (top value, top effort; deleted without a written commitment from whoever delivers it; its legal treatment is an open question), recognition only (a genuine choice, promoted when the prize rewards winners rather than buys attention or there is no entity to pay through; favour a form that outlives the ceremony). One organizer network does not recommend cash because its perceived value is lower - one organization's stated position, not a measured comparison.
- Pool: no monetary pool (legitimate), modest (sized to the Prizes line), sponsor-funded (the default where any sponsor funds a prize; never announced before the money lands), flagship (only when the line is genuinely large enough and the intent is attention before the event; deleted when no Prizes line exists). "A bigger pool draws more submissions" is an assumption, not a measurement. Keep the organizer's baseline apart from the sponsor-funded part: the Prizes line bounds the organizer-funded part only, and sponsor money sits outside it.
- Split: graduated podium is the default (one published podium ran 50/30/20); winner take most is deleted once any sponsor funds a track; track only fits a brief with no overall category; flat across many tracks is deleted on a low effort ceiling and promoted only when several sponsor tracks exist and the medium is non-cash. A non-cash prize counts in units per person against the team-size cap, with a stated rule for a leftover unit (returned, raffled or held) and whether the award follows the submitted roster or the people present.
- Eligibility to receive is not eligibility to compete: organizers, volunteers, mentors and judges may build and demo, and do not receive a prize. A minor in the winner pool, a winner outside the organizer's country, and a sponsor's own employee able to win that sponsor's prize are gates, not choices: counsel or the fiscal host reviews the payout before the prize is announced. A bracketed blank left in the published paragraph is worse than no paragraph.
- Payout: standard-form (published text, a claim step, identity or status collected as appropriate) is the default; administered when a fiscal host or the funding sponsor runs it; counsel-reviewed when a gate is live or the pool is large enough that a losing team would ask who decided; informal handover is deleted for cash or a cash equivalent, or when a gate is live. A continuing condition (code and video stay public) needs the date the award becomes final.
- Publish the prize structure before submissions close. Never state a numeric threshold, a tax-form name or a jurisdiction's rule: name the exposure category - contest-law classification, payer-side tax reporting and withholding, cross-border payment, minors receiving an award, sponsor employees receiving their own sponsor's prize, people working the event receiving anything - and the person it is routed to. Every gate and signal here is self-set; no other event's pool is a target.
House rules
- Use only the input and the rules above. The page's picks, names, amounts and dates 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.
- Never invent a prize, a sponsor, a value, a date, a threshold, a form name or a person; where someone must be named and is not, give a role. Refer to people by name or as they - never infer anyone's gender from a name.
- 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
structure
summary- one sentence, at most 35 words: the medium, the pool against the line, and the first thing to settle.medium-used(every medium the tracks use, as the page names them, including any removed),deleted(the page's removed media,[]when none),why(one sentence: the reason for each deletion, or why the media fit).pool-rung(none,modest,sponsororflagship, the page's pick - copy it even when a finding says it must change, and say so in the note),organizerandsponsor(the page's amounts as numbers),line(the Prizes line as a number, or null when not set),note(one sentence).split-shape(podium,winner,trackorflat, the page's pick - copy it even when a finding deletes it, and name the fix in the note),awards(the page's award count),note(one sentence).units-count(the page's per-person units, 0 when none),leftover(the page's leftover rule, or "" when not decided),note(one sentence).eligibility-blanks(the page's count of bracketed blanks),note(one sentence: what still has to be decided, or that the paragraph is ready to hand to the brief).payout-rung(standard,administeredorcounsel, the page's pick),handover(who hands over, from the input, or a role),claim(one sentence: what a winner provides, described as a category, never a form name),finality(one sentence, or "" when there is no continuing condition),lapse(one sentence: what happens to an unclaimed award - your design where the input is silent, labelled).exposures- one object per live category in the page's order:category(the page's name),route(the page's route exactly, or "not routed").signals- 2 or 3 short strings, the page's signals to record, each with the revision it would trigger (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": "structure",
"summary": "one sentence",
"medium": { "used": [ "Gear" ], "deleted": [], "why": "one sentence" },
"pool": { "rung": "modest", "organizer": 600, "sponsor": 0, "line": 1000, "note": "one sentence" },
"split": { "shape": "podium", "awards": 3, "note": "one sentence" },
"units": { "count": 12, "leftover": "returned", "note": "one sentence" },
"eligibility": { "blanks": 0, "note": "one sentence" },
"payout": { "rung": "standard", "handover": "a name or a role", "claim": "one sentence", "finality": "", "lapse": "one sentence" },
"exposures": [ { "category": "Contest-law classification", "route": "a name or a role" } ],
"signals": [ "what to record and what it changes" ],
"caveats": [ "a caveat" ]
}
The page holds it to its own picks: the media and removals, the pool rung with its amounts and the Prizes line, the split and award count, the units and leftover rule, the blank count, the payout rung and every live exposure with its route; no number may appear that the input does not hold, and no tax form may be named.
brief
awards- one short string per award, in the page's order: the track and place exactly as the page names them, what the award is, and its value or units as the page states them (for credits, the expiry and region). An award whose sponsorship is not signed is still listed - say in the note that it must not be published until signed.eligibility- the page's eligibility-to-receive paragraph, lightly edited for flow only: keep every sentence's decision, and keep every bracketed blank exactly as bracketed.claim- a few sentences: how a winner claims (identity and status as a category, never a form name), who hands over, and what happens to an unclaimed award.conditions- one or two sentences on the continuing condition and when awards become final, or "" when there is none.note- one sentence, at most 30 words, for the organizers: what to settle before the section is published.
{
"task": "brief",
"awards": [ "Overall first - ..." ],
"eligibility": "the paragraph",
"claim": "a few sentences",
"conditions": "",
"note": "one sentence"
}
The page requires every award in order with credit expiries, the eligibility paragraph's decisions with its exact bracketed blanks, unsigned sponsor awards held back in the note, any continuing condition, and no tax form named.
Worked examples - every lane, every sample
These are the page's three free samples (fictional events, sponsors and people). 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. Every sample has both lanes.
Northgate Student Hack (gear and credits) - structure
Modest pool, sized to the Prizes line; Graduated podium, 6 awards; Administered payout; 6 of 6 written.
Input:
{
"task": "structure",
"event": "Event: Northgate Student Hack; a recurring event; the prize is meant to reward the winners after it; effort ceiling normal\nBudget: Prizes line $1,500; team-size cap 4\nMoney: runs under a fiscal host (the student union's finance office); counsel not named\nWho might win: a minor no; outside the organizer's country no; organizers, volunteers, mentors and judges may build, may not win; sponsor employees may compete, not win their own employer's prize\nHandover by the prize lead; awards follow the submitted roster; leftover units returned; continuing condition code and video stay public, final 2027-04-15\nDates: prize structure published 2027-02-01; submissions close 2027-03-07",
"awards": "Tracks as published:\nOverall | overall | funded by organizer | Gear | per person | 3 places\nBest use of the Atlas Maps API | sponsor | funded by Atlas Maps (signed) | Credits and vouchers | per team | 1 place | expiry 2027-12-31, region worldwide\nBest hardware hack | sponsor | funded by Voltrane Parts (NOT signed) | Gear | per person | 1 place\nAudience choice | audience | funded by organizer | Recognition only | per team | 1 place\nAwards (6):\nOverall first: Gear, $100 per person x 4 = $400\nOverall second: Gear, $60 per person x 4 = $240\nOverall third: Gear, $30 per person x 4 = $120\nBest use of the Atlas Maps API first: Credits and vouchers, $500 per team\nBest hardware hack first: Gear, $80 per person x 4 = $320\nAudience choice first: Recognition only\nEligibility-to-receive paragraph, as the page drafted it (no blanks):\nPrizes are awarded to the team named in the submission. Organizers, volunteers, mentors and judges may build projects and demo them, and are not eligible to receive a prize. Employees of a prize sponsor may build and compete, and are not eligible to receive a prize funded by their own employer. Prizes are awarded per team member up to the team-size limit of 4; unclaimed units are returned. Credit prizes carry the expiry date and redemption region stated with each award, and may need an account you create. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim. Your code and demo video must remain public to keep the prize; awards become final on 2027-04-15.",
"facts": "Medium: Gear, Credits and vouchers, Recognition only; nothing deleted; informal handover deleted\nPool: modest - Modest pool, sized to the Prizes line; organizer-funded $760; sponsor-funded $820 (Best hardware hack not signed); Prizes line $1,500 (it bounds the organizer-funded part only; sponsor money sits outside it)\nSplit: podium - Graduated podium; 6 awards; overall shares 53/32/16 (one published podium ran 50/30/20)\nUnits: 16 per-person units against a team cap of 4; leftover returned\nGates (counsel or the fiscal host reviews before announcing): none live\nPayout: administered - Administered payout; handover by the prize lead\nExposure categories live, with routes (never answered here - no threshold, no form name, no amount): Contest-law classification -> the student union's finance office; Payer-side tax reporting and withholding -> the student union's finance office; Sponsor employees receiving their own sponsor's prize -> the funding sponsor's legal team; Organizers, volunteers, mentors and judges receiving anything -> published practice\nPass threshold (6 of 6): The medium written; The total pool with the budget line it fits inside written; The split with an award count written; The eligibility-to-receive text, no blanks written; The payout mechanic naming who hands over what written; Every live exposure category routed to someone written\nSignals to record: Awards actually handed over against awards announced (self-set); Winners who never claimed, and by when the lapse rule ran (self-set); Prize units left over against the team-size rule (self-set)\nFindings (3):\n[info] gear_lead (medium): Gear: many items get backordered - order early for stock risk, keep the receipts, and decide the fate of a leftover unit before the event.\n[check] unsigned (pool): Best hardware hack is funded by Voltrane Parts but the sponsorship is not signed - announcing it before the money lands leaves the organizer owning the gap personally.\n[info] gate (readiness): Pass threshold: 6 of 6 in writing before the brief is published (self-set)."
}
Reply:
{
"task": "structure",
"summary": "Gear, credits and vouchers, and recognition only fund a modest $760 organizer pool against the $1,500 Prizes line, plus $820 sponsor funds outside it; the unsigned Voltrane Parts sponsorship needs settling first.",
"medium": {
"used": [
"Gear",
"Credits and vouchers",
"Recognition only"
],
"deleted": [],
"why": "Gear and credits need no payment rail, and recognition only fits an event meant to reward its winners rather than draw attention, so none of the three media needed removing."
},
"pool": {
"rung": "modest",
"organizer": 760,
"sponsor": 820,
"line": 1500,
"note": "The organizer-funded $760 sits inside the $1,500 Prizes line while the $820 sponsor total, which includes the unsigned Voltrane Parts track, sits outside it."
},
"split": {
"shape": "podium",
"awards": 6,
"note": "The 6 awards split 53/32/16 across the overall podium's 3 places, plus one award each for Best use of the Atlas Maps API, Best hardware hack and Audience choice."
},
"units": {
"count": 16,
"leftover": "returned",
"note": "The 16 per-person units run against the team-size cap of 4, follow the submitted roster, and any leftover unit is returned."
},
"eligibility": {
"blanks": 0,
"note": "With no gates live and no bracketed blanks left, the eligibility-to-receive paragraph is ready to hand to the brief."
},
"payout": {
"rung": "administered",
"handover": "the prize lead",
"claim": "A winner confirms their identity and any other information the organizers are required to collect before the award is handed over.",
"finality": "Code and the demo video must stay public for the award to remain earned, becoming final on 2027-04-15.",
"lapse": "Our design: an award not claimed within a reasonable window lapses and is returned, matching the leftover-unit rule."
},
"exposures": [
{
"category": "Contest-law classification",
"route": "the student union's finance office"
},
{
"category": "Payer-side tax reporting and withholding",
"route": "the student union's finance office"
},
{
"category": "Sponsor employees receiving their own sponsor's prize",
"route": "the funding sponsor's legal team"
},
{
"category": "Organizers, volunteers, mentors and judges receiving anything",
"route": "published practice"
}
],
"signals": [
"Awards actually handed over against awards announced - a gap would trigger a payout-process review.",
"Winners who never claimed, and by when the lapse rule ran - repeat lapses would trigger a revision of the claim window.",
"Prize units left over against the team-size rule - a pattern would trigger a revision of the per-person unit count."
],
"caveats": [
"Best hardware hack's Voltrane Parts sponsorship is unsigned - do not announce it before the money lands."
]
}
The page's check: kept - nothing to flag
Northgate Student Hack (gear and credits) - brief
Modest pool, sized to the Prizes line; Graduated podium, 6 awards; Administered payout; 6 of 6 written.
Input:
{
"task": "brief",
"event": "Event: Northgate Student Hack; a recurring event; the prize is meant to reward the winners after it; effort ceiling normal\nBudget: Prizes line $1,500; team-size cap 4\nMoney: runs under a fiscal host (the student union's finance office); counsel not named\nWho might win: a minor no; outside the organizer's country no; organizers, volunteers, mentors and judges may build, may not win; sponsor employees may compete, not win their own employer's prize\nHandover by the prize lead; awards follow the submitted roster; leftover units returned; continuing condition code and video stay public, final 2027-04-15\nDates: prize structure published 2027-02-01; submissions close 2027-03-07",
"awards": "Tracks as published:\nOverall | overall | funded by organizer | Gear | per person | 3 places\nBest use of the Atlas Maps API | sponsor | funded by Atlas Maps (signed) | Credits and vouchers | per team | 1 place | expiry 2027-12-31, region worldwide\nBest hardware hack | sponsor | funded by Voltrane Parts (NOT signed) | Gear | per person | 1 place\nAudience choice | audience | funded by organizer | Recognition only | per team | 1 place\nAwards (6):\nOverall first: Gear, $100 per person x 4 = $400\nOverall second: Gear, $60 per person x 4 = $240\nOverall third: Gear, $30 per person x 4 = $120\nBest use of the Atlas Maps API first: Credits and vouchers, $500 per team\nBest hardware hack first: Gear, $80 per person x 4 = $320\nAudience choice first: Recognition only\nEligibility-to-receive paragraph, as the page drafted it (no blanks):\nPrizes are awarded to the team named in the submission. Organizers, volunteers, mentors and judges may build projects and demo them, and are not eligible to receive a prize. Employees of a prize sponsor may build and compete, and are not eligible to receive a prize funded by their own employer. Prizes are awarded per team member up to the team-size limit of 4; unclaimed units are returned. Credit prizes carry the expiry date and redemption region stated with each award, and may need an account you create. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim. Your code and demo video must remain public to keep the prize; awards become final on 2027-04-15.",
"facts": "Medium: Gear, Credits and vouchers, Recognition only; nothing deleted; informal handover deleted\nPool: modest - Modest pool, sized to the Prizes line; organizer-funded $760; sponsor-funded $820 (Best hardware hack not signed); Prizes line $1,500 (it bounds the organizer-funded part only; sponsor money sits outside it)\nSplit: podium - Graduated podium; 6 awards; overall shares 53/32/16 (one published podium ran 50/30/20)\nUnits: 16 per-person units against a team cap of 4; leftover returned\nGates (counsel or the fiscal host reviews before announcing): none live\nPayout: administered - Administered payout; handover by the prize lead\nExposure categories live, with routes (never answered here - no threshold, no form name, no amount): Contest-law classification -> the student union's finance office; Payer-side tax reporting and withholding -> the student union's finance office; Sponsor employees receiving their own sponsor's prize -> the funding sponsor's legal team; Organizers, volunteers, mentors and judges receiving anything -> published practice\nPass threshold (6 of 6): The medium written; The total pool with the budget line it fits inside written; The split with an award count written; The eligibility-to-receive text, no blanks written; The payout mechanic naming who hands over what written; Every live exposure category routed to someone written\nSignals to record: Awards actually handed over against awards announced (self-set); Winners who never claimed, and by when the lapse rule ran (self-set); Prize units left over against the team-size rule (self-set)\nFindings (3):\n[info] gear_lead (medium): Gear: many items get backordered - order early for stock risk, keep the receipts, and decide the fate of a leftover unit before the event.\n[check] unsigned (pool): Best hardware hack is funded by Voltrane Parts but the sponsorship is not signed - announcing it before the money lands leaves the organizer owning the gap personally.\n[info] gate (readiness): Pass threshold: 6 of 6 in writing before the brief is published (self-set)."
}
Reply:
{
"task": "brief",
"awards": [
"Overall first - Gear, $100 per person x 4 = $400.",
"Overall second - Gear, $60 per person x 4 = $240.",
"Overall third - Gear, $30 per person x 4 = $120.",
"Best use of the Atlas Maps API first - Credits and vouchers, $500 per team, expiry 2027-12-31, region worldwide.",
"Best hardware hack first - Gear, $80 per person x 4 = $320.",
"Audience choice first - Recognition only."
],
"eligibility": "Prizes are awarded to the team named in the submission. Organizers, volunteers, mentors and judges may build projects and demo them, and are not eligible to receive a prize. Employees of a prize sponsor may build and compete, and are not eligible to receive a prize funded by their own employer. Prizes are awarded per team member up to the team-size limit of 4; unclaimed units are returned. Credit prizes carry the expiry date and redemption region stated with each award, and may need an account you create. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim. Your code and demo video must remain public to keep the prize; awards become final on 2027-04-15.",
"claim": "A winner claims a prize by confirming their identity and any other information the organizers are required to collect, and the prize lead hands over the award once that is confirmed. Any unit a team does not use is returned under the team-size cap of 4, and an award that is never claimed lapses and is returned on the same basis (our design).",
"conditions": "Winners must keep their code and demo video public to keep the prize, with awards becoming final on 2027-04-15.",
"note": "Settle the Best hardware hack sponsorship with Voltrane Parts before publishing; it is listed here but must not go out until the funding is signed."
}
The page's check: kept - nothing to flag
Open Climate Hack (online, cash) - structure
Flagship pool; Graduated podium, 5 awards; Counsel-reviewed payout; 4 of 6 written.
Input:
{
"task": "structure",
"event": "Event: Open Climate Hack (online); a one-off edition; the prize is meant to buy attention before the event; effort ceiling normal\nBudget: Prizes line $10,000; team-size cap 5\nMoney: the team has its own legal entity; counsel outside counsel, to be engaged\nWho might win: a minor maybe; outside the organizer's country yes; organizers, volunteers, mentors and judges may build, may not win; sponsor employees not decided\nHandover by the operations lead; awards follow the submitted roster; leftover units not decided; continuing condition none\nDates: prize structure published 2027-04-10; submissions close 2027-05-02",
"awards": "Tracks as published:\nGrand prize | overall | funded by organizer | Cash | per team | 3 places\nBest use of Nimbus Cloud | sponsor | funded by Nimbus Cloud (signed) | Cash | per team | 1 place\nSeed pitch slot | sponsor | funded by Quayfern Ventures (signed) | Opportunity | per team | 1 place | written commitment NO\nAwards (5):\nGrand prize first: Cash, $5,000 per team\nGrand prize second: Cash, $2,500 per team\nGrand prize third: Cash, $1,000 per team\nBest use of Nimbus Cloud first: Cash, $2,000 per team\nSeed pitch slot first: Opportunity, access rather than money\nEligibility-to-receive paragraph, as the page drafted it (2 blanks still bracketed):\nPrizes are awarded to the team named in the submission. Organizers, volunteers, mentors and judges may build projects and demo them, and are not eligible to receive a prize. [Sponsor employees may / may not receive a prize funded by their own employer.] A winner under the age of majority in [place] will be asked to have a parent or guardian receive the prize on their behalf. Where a prize cannot be delivered to a winner's location, we will agree an alternative of comparable value with that winner. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim.",
"facts": "Medium: Cash, Opportunity; deleted: Cash (a winner could be a minor, a winner could be outside the organizer's country); Opportunity (no written commitment); informal handover deleted\nPool: flagship - Flagship pool; organizer-funded $8,500; sponsor-funded $2,000; Prizes line $10,000 (it bounds the organizer-funded part only; sponsor money sits outside it)\nSplit: podium - Graduated podium; 5 awards; overall shares 59/29/12 (one published podium ran 50/30/20)\nUnits: no per-person units\nGates (counsel or the fiscal host reviews before announcing): a winner could be a minor; a winner could be outside the organizer's country; a sponsor's own employee could win that sponsor's prize\nPayout: counsel - Counsel-reviewed payout; handover by the operations lead\nExposure categories live, with routes (never answered here - no threshold, no form name, no amount): Contest-law classification -> outside counsel, to be engaged; Payer-side tax reporting and withholding -> outside counsel, to be engaged; Cross-border payment -> outside counsel, to be engaged; Minors receiving an award -> outside counsel, to be engaged; Sponsor employees receiving their own sponsor's prize -> the funding sponsor's legal team; Organizers, volunteers, mentors and judges receiving anything -> published practice\nPass threshold (4 of 6): The medium MISSING; The total pool with the budget line it fits inside written; The split with an award count written; The eligibility-to-receive text, no blanks MISSING; The payout mechanic naming who hands over what written; Every live exposure category routed to someone written\nSignals to record: Awards actually handed over against awards announced (self-set); Winners who never claimed, and by when the lapse rule ran (self-set); Any eligibility question raised on the day with no published answer (self-set)\nFindings (7):\n[fix] cash_deleted (medium): Grand prize is a cash prize, but a winner could be a minor and a winner could be outside the organizer's country - cash is deleted: the machinery to pay it correctly does not exist yet. Pick gear, credits or recognition, or route it to counsel before anything is announced.\n[fix] cash_deleted (medium): Best use of Nimbus Cloud is a cash prize, but a winner could be a minor and a winner could be outside the organizer's country - cash is deleted: the machinery to pay it correctly does not exist yet. Pick gear, credits or recognition, or route it to counsel before anything is announced.\n[fix] opportunity_unwritten (medium): Seed pitch slot offers an opportunity with no written commitment from Quayfern Ventures - an opportunity the organizer cannot deliver is the one prize that fails publicly. Deleted until the commitment is in writing.\n[info] flagship (pool): Flagship pool: \"a bigger pool draws more submissions\" is an assumption, not a measurement - one platform's guidance says a modest, well-distributed pool performs as well. Large enough that a losing team might ask who decided? Route the payout to counsel.\n[check] blanks (eligibility): The eligibility paragraph still has 2 bracketed blanks - publishing it with a bracket in it is worse than publishing nothing.\n[check] gates (eligibility): Live gates - a winner could be a minor; a winner could be outside the organizer's country; a sponsor's own employee could win that sponsor's prize. Each carries a legal obligation, not a menu choice: counsel or the fiscal host reviews the payout before the prize is announced.\n[check] gate (readiness): Pass threshold: 4 of 6 in writing before the brief is published - missing: The medium, The eligibility-to-receive text, no blanks (self-set)."
}
Reply:
{
"task": "structure",
"summary": "Cash and the opportunity are deleted, the flagship pool is $8,500 organizer plus $2,000 sponsor against the $10,000 Prizes line, and the first thing to settle is a new medium for the cash awards.",
"medium": {
"used": [
"Cash",
"Opportunity"
],
"deleted": [
"Cash",
"Opportunity"
],
"why": "Cash is deleted because a winner could be a minor and a winner could be outside the organizer's country, and Opportunity is deleted because Quayfern Ventures has given no written commitment."
},
"pool": {
"rung": "flagship",
"organizer": 8500,
"sponsor": 2000,
"line": 10000,
"note": "Flagship is the page's pick though the assumption that a bigger pool draws more submissions is unmeasured, and the $10,000 Prizes line bounds only the $8,500 organizer-funded part while the $2,000 sponsor-funded part sits outside it."
},
"split": {
"shape": "podium",
"awards": 5,
"note": "The page runs a graduated podium across 5 awards with overall shares of 59/29/12, compared with one published podium that ran 50/30/20."
},
"units": {
"count": 0,
"leftover": "",
"note": "No non-cash prize currently carries per-person units, and the page has not yet decided a leftover-unit rule."
},
"eligibility": {
"blanks": 2,
"note": "The sponsor-employee eligibility choice and the minor's jurisdiction still need to be filled in before the two bracketed blanks can be published."
},
"payout": {
"rung": "counsel",
"handover": "the operations lead",
"claim": "A winner confirms their identity and supplies whatever reporting information is required, as they will be told when they claim.",
"finality": "",
"lapse": "Design: with no lapse rule set by the page, an unclaimed award should stay with the operations lead until the organizers decide how it lapses."
},
"exposures": [
{
"category": "Contest-law classification",
"route": "outside counsel, to be engaged"
},
{
"category": "Payer-side tax reporting and withholding",
"route": "outside counsel, to be engaged"
},
{
"category": "Cross-border payment",
"route": "outside counsel, to be engaged"
},
{
"category": "Minors receiving an award",
"route": "outside counsel, to be engaged"
},
{
"category": "Sponsor employees receiving their own sponsor's prize",
"route": "the funding sponsor's legal team"
},
{
"category": "Organizers, volunteers, mentors and judges receiving anything",
"route": "published practice"
}
],
"signals": [
"Awards actually handed over against awards announced - a gap would trigger a review of the payout mechanic.",
"Winners who never claimed, and by when the lapse rule ran - repeated non-claims would trigger writing an explicit lapse rule.",
"Any eligibility question raised on the day with no published answer - would trigger filling the bracketed blanks before the next publication."
],
"caveats": [
"Grand prize is a cash prize but deleted since a winner could be a minor and a winner could be outside the organizer's country; pick gear, credits or recognition, or route it to counsel.",
"Best use of Nimbus Cloud is a cash prize but deleted for the same minor and cross-border reasons; pick gear, credits or recognition, or route it to counsel.",
"Seed pitch slot offers an opportunity with no written commitment from Quayfern Ventures and is deleted until the commitment is in writing.",
"Pass threshold is 4 of 6 before publishing - missing: the medium, and the eligibility-to-receive text with no blanks."
]
}
The page's check: kept - nothing to flag
Riverside Civic Hack Day (no entity) - structure
Modest pool, sized to the Prizes line; Winner take most, 3 awards; Standard-form payout; 1 of 6 written.
Input:
{
"task": "structure",
"event": "Event: Riverside Civic Hack Day; a recurring event; the prize is meant to reward the winners after it; effort ceiling low\nBudget: Prizes line NOT SET; team-size cap 4\nMoney: no legal entity and no fiscal host; counsel not named\nWho might win: a minor no; outside the organizer's country no; organizers, volunteers, mentors and judges may build and win; sponsor employees may not compete\nHandover by not named; awards follow the people present; leftover units not decided; continuing condition none\nDates: prize structure published 2027-05-20; submissions close 2027-05-18",
"awards": "Tracks as published:\nCommunity award | overall | funded by organizer | Recognition only | per team | 1 place\nOpen data track | sponsor | funded by City Library (signed) | Opportunity | per team | 1 place | written commitment yes\nOrganizers' pick | overall | funded by organizer | Gear | per team | 1 place\nAwards (3):\nCommunity award first: Recognition only\nOpen data track first: Opportunity, access rather than money\nOrganizers' pick first: Gear, $60 per team\nEligibility-to-receive paragraph, as the page drafted it (1 blank still bracketed):\nPrizes are awarded to the team members present at the closing ceremony. [Organizers, volunteers, mentors and judges: decide whether they may receive a prize.] Employees of a prize sponsor may not compete for or receive any prize. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim.",
"facts": "Medium: Gear, Opportunity, Recognition only; deleted: Cash (there is no legal entity and no fiscal host); informal handover available for goods to an adult at the venue\nPool: modest - Modest pool, sized to the Prizes line; organizer-funded $60; sponsor-funded $0; Prizes line not set\nSplit: winner - Winner take most; 3 awards\nUnits: no per-person units\nGates (counsel or the fiscal host reviews before announcing): none live\nPayout: standard - Standard-form payout; handover by not named\nExposure categories live, with routes (never answered here - no threshold, no form name, no amount): Contest-law classification -> NOT ROUTED; Payer-side tax reporting and withholding -> NOT ROUTED; Organizers, volunteers, mentors and judges receiving anything -> published practice\nPass threshold (1 of 6): The medium written; The total pool with the budget line it fits inside MISSING; The split with an award count MISSING; The eligibility-to-receive text, no blanks MISSING; The payout mechanic naming who hands over what MISSING; Every live exposure category routed to someone MISSING\nSignals to record: Awards actually handed over against awards announced (self-set); Winners who never claimed, and by when the lapse rule ran (self-set); Any eligibility question raised on the day with no published answer (self-set)\nFindings (12):\n[info] opportunity_open (medium): Open data track is an opportunity: its legal treatment is unsettled - an award working as a job or investment introduction may touch employment or securities rules. An open question to route.\n[info] gear_lead (medium): Gear: many items get backordered - order early for stock risk, keep the receipts, and decide the fate of a leftover unit before the event.\n[fix] no_line (pool): The organizer funds $60 of prizes, but the budget's Prizes line is not set - say so rather than sizing against nothing.\n[fix] winner_deleted (split): Winner take most with a sponsor-funded track - deleted: that sponsor bought a named award and this shape has nowhere to put it. Use a graduated podium beside the sponsor tracks.\n[check] two_overall (split): 2 overall awards - one ranked podium, with every other award a track or a separate layer.\n[fix] staff_win (eligibility): People working the event allowed to receive prizes - published practice keeps organizers, volunteers, mentors and judges out of the receiving pool whatever they may build.\n[check] blanks (eligibility): The eligibility paragraph still has 1 bracketed blank - publishing it with a bracket in it is worse than publishing nothing.\n[check] unrouted (exposure): No one named to route contest-law classification, payer-side tax reporting and withholding to - name the counsel or fiscal host who answers it; never answer it on the page.\n[check] no_entity (payout): No legal entity and no fiscal host: nobody can sign a payout. A fiscal sponsor is the standard route; recognition is the one medium that needs neither.\n[check] no_handover (payout): Name who hands over or transfers each award - \"the sponsor is handling it\" is not a payout plan.\n[fix] publish_late (publication): The prize structure is published 2027-05-20, not before submissions close (2027-05-18) - a prize announced after teams chose what to build has already failed at motivating anything.\n[check] gate (readiness): Pass threshold: 1 of 6 in writing before the brief is published - missing: The total pool with the budget line it fits inside, The split with an award count, The eligibility-to-receive text, no blanks, The payout mechanic naming who hands over what, Every live exposure category routed to someone (self-set)."
}
Reply:
{
"task": "structure",
"summary": "Gear, Opportunity and Recognition only fund a modest $60 organizer pool against a Prizes line that is not yet set, which is the first thing to settle.",
"medium": {
"used": [
"Gear",
"Opportunity",
"Recognition only"
],
"deleted": [
"Cash"
],
"why": "Cash is deleted because there is no legal entity and no fiscal host to run a cash payout."
},
"pool": {
"rung": "modest",
"organizer": 60,
"sponsor": 0,
"line": null,
"note": "The organizer funds $60 of prizes but the Prizes line itself is not set, so the pool cannot yet be sized against it."
},
"split": {
"shape": "winner",
"awards": 3,
"note": "Winner take most is deleted now that City Library funds a sponsor track, so the fix is a graduated podium beside the sponsor track."
},
"units": {
"count": 0,
"leftover": "",
"note": "No medium here carries per-person units, so the leftover rule stays undecided."
},
"eligibility": {
"blanks": 1,
"note": "One bracketed blank - whether organizers, volunteers, mentors and judges may receive a prize - still has to be decided before publication."
},
"payout": {
"rung": "standard",
"handover": "not named",
"claim": "A winner confirms their identity and any other information the event is required to collect.",
"finality": "",
"lapse": "An unclaimed award should revert to the organizer until a published lapse rule says otherwise (our design)."
},
"exposures": [
{
"category": "Contest-law classification",
"route": "not routed"
},
{
"category": "Payer-side tax reporting and withholding",
"route": "not routed"
},
{
"category": "Organizers, volunteers, mentors and judges receiving anything",
"route": "published practice"
}
],
"signals": [
"Awards actually handed over against awards announced - a gap triggers a review of the payout mechanic.",
"Winners who never claimed, and by when the lapse rule ran - repeated misses trigger writing a lapse rule.",
"Any eligibility question raised on the day with no published answer - triggers a rewrite of the eligibility paragraph."
],
"caveats": [
"The organizer funds $60 of prizes, but the Prizes line is not set.",
"Winner take most is deleted because a sponsor funds a track; use a graduated podium instead.",
"Organizers, volunteers, mentors and judges must stay out of the receiving pool, per published practice.",
"The prize structure publishes 2027-05-20, after submissions close 2027-05-18."
]
}
The page's check: kept - nothing to flag
Riverside Civic Hack Day (no entity) - brief
Modest pool, sized to the Prizes line; Winner take most, 3 awards; Standard-form payout; 1 of 6 written.
Input:
{
"task": "brief",
"event": "Event: Riverside Civic Hack Day; a recurring event; the prize is meant to reward the winners after it; effort ceiling low\nBudget: Prizes line NOT SET; team-size cap 4\nMoney: no legal entity and no fiscal host; counsel not named\nWho might win: a minor no; outside the organizer's country no; organizers, volunteers, mentors and judges may build and win; sponsor employees may not compete\nHandover by not named; awards follow the people present; leftover units not decided; continuing condition none\nDates: prize structure published 2027-05-20; submissions close 2027-05-18",
"awards": "Tracks as published:\nCommunity award | overall | funded by organizer | Recognition only | per team | 1 place\nOpen data track | sponsor | funded by City Library (signed) | Opportunity | per team | 1 place | written commitment yes\nOrganizers' pick | overall | funded by organizer | Gear | per team | 1 place\nAwards (3):\nCommunity award first: Recognition only\nOpen data track first: Opportunity, access rather than money\nOrganizers' pick first: Gear, $60 per team\nEligibility-to-receive paragraph, as the page drafted it (1 blank still bracketed):\nPrizes are awarded to the team members present at the closing ceremony. [Organizers, volunteers, mentors and judges: decide whether they may receive a prize.] Employees of a prize sponsor may not compete for or receive any prize. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim.",
"facts": "Medium: Gear, Opportunity, Recognition only; deleted: Cash (there is no legal entity and no fiscal host); informal handover available for goods to an adult at the venue\nPool: modest - Modest pool, sized to the Prizes line; organizer-funded $60; sponsor-funded $0; Prizes line not set\nSplit: winner - Winner take most; 3 awards\nUnits: no per-person units\nGates (counsel or the fiscal host reviews before announcing): none live\nPayout: standard - Standard-form payout; handover by not named\nExposure categories live, with routes (never answered here - no threshold, no form name, no amount): Contest-law classification -> NOT ROUTED; Payer-side tax reporting and withholding -> NOT ROUTED; Organizers, volunteers, mentors and judges receiving anything -> published practice\nPass threshold (1 of 6): The medium written; The total pool with the budget line it fits inside MISSING; The split with an award count MISSING; The eligibility-to-receive text, no blanks MISSING; The payout mechanic naming who hands over what MISSING; Every live exposure category routed to someone MISSING\nSignals to record: Awards actually handed over against awards announced (self-set); Winners who never claimed, and by when the lapse rule ran (self-set); Any eligibility question raised on the day with no published answer (self-set)\nFindings (12):\n[info] opportunity_open (medium): Open data track is an opportunity: its legal treatment is unsettled - an award working as a job or investment introduction may touch employment or securities rules. An open question to route.\n[info] gear_lead (medium): Gear: many items get backordered - order early for stock risk, keep the receipts, and decide the fate of a leftover unit before the event.\n[fix] no_line (pool): The organizer funds $60 of prizes, but the budget's Prizes line is not set - say so rather than sizing against nothing.\n[fix] winner_deleted (split): Winner take most with a sponsor-funded track - deleted: that sponsor bought a named award and this shape has nowhere to put it. Use a graduated podium beside the sponsor tracks.\n[check] two_overall (split): 2 overall awards - one ranked podium, with every other award a track or a separate layer.\n[fix] staff_win (eligibility): People working the event allowed to receive prizes - published practice keeps organizers, volunteers, mentors and judges out of the receiving pool whatever they may build.\n[check] blanks (eligibility): The eligibility paragraph still has 1 bracketed blank - publishing it with a bracket in it is worse than publishing nothing.\n[check] unrouted (exposure): No one named to route contest-law classification, payer-side tax reporting and withholding to - name the counsel or fiscal host who answers it; never answer it on the page.\n[check] no_entity (payout): No legal entity and no fiscal host: nobody can sign a payout. A fiscal sponsor is the standard route; recognition is the one medium that needs neither.\n[check] no_handover (payout): Name who hands over or transfers each award - \"the sponsor is handling it\" is not a payout plan.\n[fix] publish_late (publication): The prize structure is published 2027-05-20, not before submissions close (2027-05-18) - a prize announced after teams chose what to build has already failed at motivating anything.\n[check] gate (readiness): Pass threshold: 1 of 6 in writing before the brief is published - missing: The total pool with the budget line it fits inside, The split with an award count, The eligibility-to-receive text, no blanks, The payout mechanic naming who hands over what, Every live exposure category routed to someone (self-set)."
}
Reply:
{
"task": "brief",
"awards": [
"Community award first - Recognition only, funded by the organizer.",
"Open data track first - Opportunity, access rather than money, funded by City Library under a signed written commitment.",
"Organizers' pick first - Gear, $60 per team, funded by the organizer."
],
"eligibility": "Prizes are awarded to the team members present at the closing ceremony. [Organizers, volunteers, mentors and judges: decide whether they may receive a prize.] Employees of a prize sponsor may not compete for or receive any prize. To receive a prize you will be asked to confirm your identity and any information we are required to collect; we will tell you exactly what that is when you claim.",
"claim": "A winner claims a prize by confirming their identity and any other information the event is required to collect. Who hands over each award has not yet been named. The input does not say what happens to an unclaimed award; as a design choice, an unclaimed award should revert to the organizer until a lapse rule is published.",
"conditions": "",
"note": "Settle the Prizes line, the split shape, the bracketed eligibility blank, and who hands over each award, then publish before the 2027-05-18 close, not after."
}
The page's check: kept - nothing to flag
The awards CSV for the first sample
track,place,medium,funder,per,unit_value,units,value
Overall,first,gear,organizer,person,100,4,400
Overall,second,gear,organizer,person,60,4,240
Overall,third,gear,organizer,person,30,4,120
Best use of the Atlas Maps API,first,credits,Atlas Maps,team,500,1,500
Best hardware hack,first,gear,Voltrane Parts,person,80,4,320
Audience choice,first,recognition,organizer,team,0,1,0
Build the input yourself
Load the engine in node (global.window = global; require("./prizekit.js"); require("./prizereply.js")), then:
const K = PrizeKit, R = PrizeReply;
const f = K.form({ name: "My Hack", prizesLine: "1500", teamCap: "4", entity: "host", hostName: "our fiscal host", handover: "the prize lead",
leftover: "returned", publishDate: "2027-02-01", closeDate: "2027-03-07", tracks: fs.readFileSync("tracks.csv", "utf8") });
// f.media, f.deleted, f.pool, f.baseline, f.sponsorPool, f.split, f.awardCount, f.awards, f.units
// f.paragraph (eligibility to receive), f.blanks, f.gates, f.payout, f.exposures (key, route), f.gate, f.findings
K.awardsCSV(f) // track, place, medium, funder, per, unit_value, units, value
const b = K.buildInput(f, "structure"); // or "brief"
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "structure");
// 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://prize-shape.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://prize-shape.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"]
// prize-shape.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://prize-shape.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://prize-shape.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
}
// PrizeShape.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 PrizeShape {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://prize-shape.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://prize-shape.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://prize-shape.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://prize-shape.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":"prize-shape"}' | 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": "prize-shape"})["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: "prize-shape" })).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": "prize-shape"}, 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", "prize-shape"), 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: "prize-shape" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "prize-shape"])["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 = "prize-shape" }))!["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": "structure", "event": "Event: ...; a recurring event ...", "awards": "Tracks as published: ...", "facts": "Medium: Gear, Credits and vouchers ..."}
# - 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": "structure", "event": "Event: ...; a recurring event ...", "awards": "Tracks as published: ...", "facts": "Medium: Gear, Credits and vouchers ..."}
# - 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: "structure" or "brief", event, awards, 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 prize-shape:<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 structure carries summary, medium, pool, split, units, eligibility, payout, exposures, signals and caveats; a brief section carries awards, eligibility, claim, conditions and note. The code below prints the headline parts of either (list items are plain strings).
KEY="prize-shape:structure:$(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 // .eligibility, ((.signals // .caveats)[] | "\(.signal // .caveat): \(.text // .text)")' reply.json
jq -r '.claim // .note // empty' reply.json # how to claim (brief)
import hashlib, time
key = "prize-shape:" + 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("eligibility", ""))
for s in reply.get("signals") or reply.get("caveats") or []:
print(s.get("signal", s.get("caveat")), "-", s.get("text", s.get("text")))
if reply.get("claim"):
print("claim:", reply["claim"])
import { createHash } from "node:crypto";
const key = `prize-shape:${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.eligibility);
for (const s of reply.signals || reply.caveats || []) console.log(s.signal ?? s.caveat, "-", s.text ?? s.text);
if (reply.claim) console.log("claim:", reply.claim);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "prize-shape:" + 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["signals"].([]any) // signals and caveats are plain strings
fmt.Println(j["charged_credits"], j["truncated"], reply["summary"])
fmt.Println(reply["note"], reply["eligibility"])
for _, x := range items {
c := x.(map[string]any)
fmt.Println(c["signal"], "-", c["status"])
}
if fix, ok := reply["claim"].(string); ok && fix != "" {
fmt.Println("claim:", fix)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "prize-shape:" + 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("eligibility").asText()));
for (JsonNode s : reply.has("signals") ? reply.path("signals") : reply.path("caveats")) System.out.println(s.path("signal").asText(s.path("caveat").asText()) + " - " + s.path("status").asText(s.path("text").asText()));
if (!reply.path("claim").asText().isEmpty()) System.out.println("claim: " + reply.get("claim").asText());
require "digest"
key = "prize-shape:#{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["eligibility"]
(reply["signals"] || reply["caveats"] || []).each { |s| puts "#{s["signal"] || s["caveat"]} - #{s["text"] || s["text"]}" }
puts "claim: #{reply["claim"]}" unless reply["claim"].to_s.empty?
$key = "prize-shape:" . $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["eligibility"] ?? "", "\n";
foreach ($reply["signals"] ?? $reply["caveats"] ?? [] as $s) echo $s["signal"] ?? $s["caveat"], " - ", $s["text"] ?? $s["text"], "\n";
if (!empty($reply["claim"])) echo "claim: ", $reply["claim"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"prize-shape:{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["eligibility"]}");
foreach (var s in (reply["signals"] ?? reply["caveats"]) as JsonArray ?? new JsonArray()) Console.WriteLine($"{s!["signal"] ?? s["caveat"]} - {s["text"] ?? s["text"]}");
if (reply["claim"] is JsonNode fix && fix.GetValue<string>() != "") Console.WriteLine("claim: " + 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: prize-shape:structure:$(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": "structure", "event": "Event: ...; a recurring event ...", "awards": "Tracks as published: ...", "facts": "Medium: Gear, Credits and vouchers ..."}
# - 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": "prize-shape:structure:" + 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": `prize-shape:structure:${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", "prize-shape:structure:"+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", "prize-shape:structure:" + 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" => "prize-shape:structure:#{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: prize-shape:structure:" . 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", $"prize-shape:structure:{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);