← Listing Rulebook
API
Tokens

Run Listing Rulebook from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads every draft rule as objective or principles-only, checks the five rulebook gates, ranks the rungs from the interview, sets the video ceiling and lints the sample listing for the named triggers, then hands it all to the run as facts. The run writes the rulebook or the listing review; the page then holds it to its own facts (lrreply.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":"listing-rulebook"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, market, draft, facts) is missing. The page always sends all five of task, market, draft, carry, facts; carry is an empty string for rulebook.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the standard, then one submission:

A missing or unknown task is answered as rulebook. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the rulebook's decisions into carry for the review lane.

The input

FieldRequiredWhat
taskyesrulebook or review. If it is missing or neither, do rulebook and set "task": "rulebook".
marketyesthe marketplace, the platform's name and the interview: stage, volume, who maintains listings, curation, who evaluates, releases, languages, badges, deadline, mandate, effort and the assets already owned; "not given" means not given.
draftyesthe draft rules as the page read them (each field's rule and limit, principles-only rows marked), the media rows, the video ceiling, the category cap, the keyword repetition count, the decay and badge rows, and the sample listing when one is given.
carrysend it (empty for rulebook)review task only: the rulebook's decisions, when the rulebook task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, and the ranked rungs. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/app-marketplace-listing-standards - the only standard the run applies:

House rules

The replies

rulebook

Write the published rulebook - the target, not a transcript of the draft. Turning a principle into a number or a named trigger, choosing a geometry or a cap, adding a core field the draft lacks, picking a ratio for an asset that has none and setting the runway length are design decisions; say what changed.

  1. summary - one sentence, at most 40 words: what would let two reviewers disagree today, and the first fix.
  2. rungs - the six menus in the facts' order: menu (strictness, media, taxonomy, localization, badges or decay), rung (the facts' rung id), why (one sentence).
  3. fields - one object per field, at least name, hook, body, screenshots, categories, landing, support and privacy: field (the field key), requirement (one sentence), reject_criterion (one sentence holding a number or a trigger id a submitter can self-check).
  4. media - geometry (objects: asset, ratio such as 16:9, count such as 3-5 - one per asset type), video_seconds (a number), accessibility (short strings).
  5. triggers - objects: id (keyword-spam, unattributed-testimonial, guarantee, trademark-order, misrepresentation, landing-page, support-channel, privacy-policy or out-of-date - each once, all nine), rule (one sentence; keyword-spam names its count, trademark-order its accepted and rejected pair).
  6. taxonomy - cap (a number), mechanism (one sentence: how drift is enforced).
  7. decay - objects: trigger, owner, consequence (short strings).
  8. badges - objects: badge, location, currency (short strings); a planned program counts as a program; [] only when there is none.
  9. runway - one sentence: the notice period before a new requirement applies to listed apps.
  10. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "rulebook",
  "summary": "one sentence",
  "rungs": [ { "menu": "strictness", "rung": "triggers", "why": "one sentence" } ],
  "fields": [ { "field": "name", "requirement": "one sentence", "reject_criterion": "one sentence" } ],
  "media": { "geometry": [ { "asset": "screenshot", "ratio": "16:9", "count": "3-5" } ], "video_seconds": 30, "accessibility": [ "alt text on informative images" ] },
  "triggers": [ { "id": "keyword-spam", "rule": "one sentence" } ],
  "taxonomy": { "cap": 2, "mechanism": "one sentence" },
  "decay": [ { "trigger": "short", "owner": "short", "consequence": "short" } ],
  "badges": [ ],
  "runway": "one sentence",
  "caveats": [ ]
}

The page holds it to its own facts (check() in lrreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no percentage; a summary of at most 40 words; the six rungs as ranked, each with a reason; each core field once with a requirement and a self-checkable reject criterion; one ratio per asset, the screenshot geometry published, a video ceiling within the evaluator's, an accessibility floor; all nine triggers once, keyword spam with its count, the naming rule naming the platform; the draft's category cap (or one or two) and how drift is enforced; complete decay rows; badges [] without a program, complete rows with one; a runway; no external listing-quality certification.

review

Review the sample listing against the rulebook - what a reviewer would send the submitter.

  1. summary - one sentence, at most 40 words.
  2. verdict - reject or accept.
  3. violations - objects: trigger (the page's trigger id, or judgment for a finding the page cannot compute, such as a body that leads with mechanism instead of outcome, or misrepresentation), field (the listing field), finding (one sentence citing the listing - describe an address, never copy it), fix (one sentence: what the submitter changes).
  4. passes - short strings: what already meets the bar. A check the page marks fix (warn), not fail, is not a rejection on its own: put it in caveats, or make it a judgment violation only when the rulebook makes it rejectable.
  5. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "review",
  "summary": "one sentence",
  "verdict": "reject",
  "violations": [ { "trigger": "trademark-order", "field": "name", "finding": "one sentence", "fix": "one sentence" } ],
  "passes": [ "a short string" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; a verdict of reject or accept, reject whenever the page found a violation and only with a judgment finding when it found none; every page violation once with its trigger id on its field; no trigger the page passed; a finding and a fix for every violation.

Worked examples

Three runs from the page's free samples (invented marketplaces): the rulebook for a marketplace opening to vendors at scale, the review of its sample listing carrying that rulebook, and the review for a small operator-run marketplace. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

rulebook - A marketplace opening to vendors at scale

The page read it as: the rulebook has gaps; 0 of 5 gates; rungs: strictness exact, media fixed, taxonomy curated, localization optin, badges trust-surface, decay downgrade-policy; failing rulebook checks: objective, geometry, taxonomy, decay, badges.

Input (facts is 3337 characters; the first 1398 are shown):

{
 "task": "rulebook",
 "market": "Marketplace: Pemryx Marketplace; platform name: Pemryx\nWhat it sells and to whom: Third-party apps for Pemryx, a project-management platform sold to IT departments; admins vet every install.\nStage: Designing from scratch, pre-launch; listings: Thousands; Vendors maintain their own listings; curation: Hybrid, with a certified tier; Admins or committees do diligence\nVersioned releases or scheduled deprecations: no; languages: one; machine translation: no; region-gated visibility: no; badges: A badge program exists, lapsable thresholds no\nHard deadline: yes; A compounding asset; effort ceiling: Medium; partner console: no; ranked search: no; link crawler: no; search-led discovery: no",
 "draft": "Draft rules (field | rule | limit):\n- name | max characters | 40\n- hook | should be compelling | - | principles only\n- description | must show clear value to customers | - | principles only\n- screenshots | high-quality images of the app | - | principles only\n- landing page | required | -\n- support | must link a support channel | -\nMedia (asset | ratio | count): screenshot 16:9 3-5; screenshot 8:5 3-5; icon 1:1 1-1; video ceiling: 120; category cap: none; keyword repetition count: 3\nDecay (trigger | owner | consequence): Screenshots older than the current UI | - | Downgraded to uncategorized\nBadges (badge | location | currency): Verified Partner | next to the app name | -\nSample listing: name \"Pemryx Timesheets Pro\"; hook \"Track time in Pemryx\"; body:\nPemryx Timesheets Pro is the timesheet app for Pemryx. Timesheets sync with Pemryx tasks, timesheets roll up by project, and timesheets export to payroll. Guaranteed to cut reporting time in half. \"We stopped chasing timesheets the week we installed it.\"\ncategories: Productivity, Finance, HR, Reporting; screenshots: 1920x1080, 1280x800, 1600x900; video: 95 s; landing: https://example.com/timesheets-brochure.pdf; support: none; privacy: https://example.com/privacy",
 "carry": "",
 "facts": "The page's checks (12 fail, 2 warn, 4 pass, 3 n/a); gates passed 0 of 5:\n- The rulebook | objective | fail | no rule for categories, privacy; principles only: hook (\"should be compelling\"); description (\"must show clear value to customers\"); screenshots (\"high-quality images of the app\")\n- The rulebook | geometry | fail | several geometries for screenshot\n- The rulebook | taxonomy | fail | uncapped self-selection - every listing will claim every category\n- The rulebook | decay | fail | Screenshots older than the current UI: no owner\n- The rulebook | badges | fail | Verified Partner: no currency rule\n- The rulebook | trademark | warn | no naming-order rule - three benchmarked operators enforce it\n- The rulebook | video | warn | 120 s; diligence buyers: 30-second function proof, marketing footage banned, captions required\n- The sample listing | name-length | pass | 21 characters against a limit of 40\n- The sample listing | hook-length | na | the draft sets no limit for the hook\n- The sample listing | body-length | na | the draft sets no limit for the body\n- The sample listing | trademark-order | fail | \"Pemryx Timesheets Pro\" leads with Pemryx - the accepted shape is \"<App> for Pemryx\"\n- The sample listing | keyword-spam | fail | timesheets x6, pemryx x5 - more than 3\n- The sample listing | unattributed-testimonial | fail | 1 quoted testimonial with no named source\n- The sample\n\n[... 1939 more characters cut from this display ...]"
}

Reply:

{
 "task": "rulebook",
 "summary": "Three principles-only rules, two screenshot ratios and no category cap let two reviewers reject different listings today; first replace the hook, body and screenshot principles with exact limits and named triggers.",
 "rungs": [
  {
   "menu": "strictness",
   "rung": "exact",
   "why": "Vendors maintain thousands of their own listings, so consistency has to come from an exact limit on every field rather than from the reviewer."
  },
  {
   "menu": "media",
   "rung": "fixed",
   "why": "One published ratio and count per asset type, plus content rules, gives every submitter the same target."
  },
  {
   "menu": "taxonomy",
   "rung": "curated",
   "why": "A compounding mandate with a certified tier calls for operator-curated layers on top of a hard category cap, never purchasable."
  },
  {
   "menu": "localization",
   "rung": "optin",
   "why": "The marketplace runs in one language with no machine translation, so markets are added per market when needed."
  },
  {
   "menu": "badges",
   "rung": "trust-surface",
   "why": "Verified Partner moves to a trust section apart from the marketing copy and gains a written currency rule."
  },
  {
   "menu": "decay",
   "rung": "downgrade-policy",
   "why": "With no versioned releases, the staleness downgrade is the floor and out-of-date content becomes a named violation found by an operator sweep."
  }
 ],
 "fields": [
  {
   "field": "name",
   "requirement": "The app name follows the naming order App for Pemryx and stays within 40 characters.",
   "reject_criterion": "Reject past 40 characters or on trademark-order."
  },
  {
   "field": "hook",
   "requirement": "Changed from a principle to a limit: one sentence stating the customer outcome, at most 80 characters.",
   "reject_criterion": "Reject past 80 characters or on keyword-spam."
  },
  {
   "field": "body",
   "requirement": "Changed from a principle to a limit: at most 1500 characters, opening with the customer outcome and tying each feature claim to a measurable result.",
   "reject_criterion": "Reject past 1500 characters or on keyword-spam, unattributed-testimonial, guarantee or misrepresentation."
  },
  {
   "field": "screenshots",
   "requirement": "Changed from a principle to a spec: 3 to 5 images at 16:9, at least one of the real app UI, with no PII and no pricing or review claims.",
   "reject_criterion": "Reject fewer than 3 or more than 5, any image off 16:9, or no image of the real app UI."
  },
  {
   "field": "categories",
   "requirement": "New rule: choose at most 2 categories from the published list.",
   "reject_criterion": "Reject more than 2 categories or any category off the published list."
  },
  {
   "field": "landing",
   "requirement": "A public web page about the app; a PDF or a code repository does not count.",
   "reject_criterion": "Reject on landing-page."
  },
  {
   "field": "support",
   "requirement": "A support channel with a response commitment stated in business days.",
   "reject_criterion": "Reject on support-channel when no channel or no response time is stated."
  },
  {
   "field": "privacy",
   "requirement": "New rule: a linked privacy policy covering data collected, data stored, retention and deletion requests.",
   "reject_criterion": "Reject on privacy-policy when the link is missing or any of the 4 coverage items is absent."
  }
 ],
 "media": {
  "geometry": [
   {
    "asset": "screenshot",
    "ratio": "16:9",
    "count": "3-5"
   },
   {
    "asset": "icon",
    "ratio": "1:1",
    "count": "1"
   }
  ],
  "video_seconds": 30,
  "accessibility": [
   "alt text on every informative image",
   "captions on every video",
   "WCAG 2.2 AA contrast"
  ]
 },
 "triggers": [
  {
   "id": "keyword-spam",
   "rule": "Any term other than common words repeated more than 3 times across the hook and body."
  },
  {
   "id": "unattributed-testimonial",
   "rule": "Any quote without a named person, role and organisation."
  },
  {
   "id": "guarantee",
   "rule": "Any guaranteed or promised result the listing cannot substantiate."
  },
  {
   "id": "trademark-order",
   "rule": "Pemryx comes last: Task Tracker for Pemryx is accepted, Pemryx Task Tracker is rejected."
  },
  {
   "id": "misrepresentation",
   "rule": "Any claim the app does not deliver, or a third-party dependency or its cost left undisclosed."
  },
  {
   "id": "landing-page",
   "rule": "The landing page link is missing, not public, or points to a PDF or a code repository."
  },
  {
   "id": "support-channel",
   "rule": "No support channel, or no stated response commitment."
  },
  {
   "id": "privacy-policy",
   "rule": "No privacy policy link, or a policy missing an item of the coverage checklist."
  },
  {
   "id": "out-of-date",
   "rule": "Screenshots or copy that no longer match the current app UI or behaviour."
  }
 ],
 "taxonomy": {
  "cap": 2,
  "mechanism": "Listings past the cap or with a lapsed category are downgraded to uncategorized rather than delisted, and the certified tier is curated by the operator and never sold."
 },
 "decay": [
  {
   "trigger": "Screenshots older than the current UI",
   "owner": "Marketplace operations, quarterly sweep",
   "consequence": "out-of-date violation, then downgraded to uncategorized"
  }
 ],
 "badges": [
  {
   "badge": "Verified Partner",
   "location": "trust section of the listing, apart from the marketing copy",
   "currency": "removed 30 days after eligibility lapses"
  }
 ],
 "runway": "Every new requirement applies to listed apps 60 days after notice to their vendors.",
 "caveats": [
  "Hook and body limits, the second screenshot ratio's removal and the 30-second video ceiling are design decisions new to this rulebook.",
  "Content quality is the marketplace's own rubric; no outside body certifies listing quality."
 ]
}

What step 5 prints for it: name | hook | body | screenshots | categories | landing | support | privacy

The page's check: kept - nothing to flag

review - A marketplace opening to vendors at scale

The input carries the saved rulebook's decisions (R.laneForm fills carry from the rulebook reply), as the page's handoff does. The page's listing violations: trademark-order, keyword-spam, unattributed-testimonial, guarantee, landing-page, support-channel, screenshot-geometry.

Input (facts is 2941 characters; the first 1398 are shown):

{
 "task": "review",
 "market": "Marketplace: Pemryx Marketplace; platform name: Pemryx\nWhat it sells and to whom: Third-party apps for Pemryx, a project-management platform sold to IT departments; admins vet every install.\nStage: Designing from scratch, pre-launch; listings: Thousands; Vendors maintain their own listings; curation: Hybrid, with a certified tier; Admins or committees do diligence\nVersioned releases or scheduled deprecations: no; languages: one; machine translation: no; region-gated visibility: no; badges: A badge program exists, lapsable thresholds no\nHard deadline: yes; A compounding asset; effort ceiling: Medium; partner console: no; ranked search: no; link crawler: no; search-led discovery: no",
 "draft": "Draft rules (field | rule | limit):\n- name | max characters | 40\n- hook | should be compelling | - | principles only\n- description | must show clear value to customers | - | principles only\n- screenshots | high-quality images of the app | - | principles only\n- landing page | required | -\n- support | must link a support channel | -\nMedia (asset | ratio | count): screenshot 16:9 3-5; screenshot 8:5 3-5; icon 1:1 1-1; video ceiling: 120; category cap: none; keyword repetition count: 3\nDecay (trigger | owner | consequence): Screenshots older than the current UI | - | Downgraded to uncategorized\nBadges (badge | location | currency): Verified Partner | next to the app name | -\nSample listing: name \"Pemryx Timesheets Pro\"; hook \"Track time in Pemryx\"; body:\nPemryx Timesheets Pro is the timesheet app for Pemryx. Timesheets sync with Pemryx tasks, timesheets roll up by project, and timesheets export to payroll. Guaranteed to cut reporting time in half. \"We stopped chasing timesheets the week we installed it.\"\ncategories: Productivity, Finance, HR, Reporting; screenshots: 1920x1080, 1280x800, 1600x900; video: 95 s; landing: https://example.com/timesheets-brochure.pdf; support: none; privacy: https://example.com/privacy",
 "carry": "Rungs: strictness exact, media fixed, taxonomy curated, localization optin, badges trust-surface, decay downgrade-policy\nReject criteria: name - Reject past 40 characters or on trademark-order.; hook - Reject past 80 characters or on keyword-spam.; body - Reject past 1500 characters or on keyword-spam, unattributed-testimonial, guarantee or misrepresentation.; screenshots - Reject fewer than 3 or more than 5, any image off 16:9, or no image of the real app UI.; categories - Reject more than 2 categories or any category off the published list.; landing - Reject on landing-page.; support - Reject on support-channel when no channel or no response time is stated.; privacy - Reject on privacy-policy when the link is missing or any of the 4 coverage items is absent.\nMedia: screenshot 16:9 x3-5, icon 1:1 x1; video 30 s; category cap 2\nTriggers: keyword-spam - Any term other than common words repeated more than 3 times across the hook and body.; unattributed-testimonial - Any quote without a named person, role and organisation.; guarantee - Any guaranteed or promised result the listing cannot substantiate.; trademark-order - Pemryx comes last: Task Tracker for Pemryx is accepted, Pemryx Task Tracker is rejected.; misrepresentation - Any claim the app does not deliver, or a third-party dependency or its cost left undisclosed.; landing-page - The landing page link is missing, not public, or points to a PDF or a code repository.; support-channel - No support channel, or no stated response commitment.; privacy-policy - No privacy policy link, or a policy missing an item of the coverage checklist.; out-of-date - Screenshots or copy that no longer match the current app UI or behaviour.",
 "facts": "The page's checks (12 fail, 2 warn, 4 pass, 3 n/a); gates passed 0 of 5:\n- The rulebook | objective | fail | no rule for categories, privacy; principles only: hook (\"should be compelling\"); description (\"must show clear value to customers\"); screenshots (\"high-quality images of the app\")\n- The rulebook | geometry | fail | several geometries for screenshot\n- The rulebook | taxonomy | fail | uncapped self-selection - every listing will claim every category\n- The rulebook | decay | fail | Screenshots older than the current UI: no owner\n- The rulebook | badges | fail | Verified Partner: no currency rule\n- The rulebook | trademark | warn | no naming-order rule - three benchmarked operators enforce it\n- The rulebook | video | warn | 120 s; diligence buyers: 30-second function proof, marketing footage banned, captions required\n- The sample listing | name-length | pass | 21 characters against a limit of 40\n- The sample listing | hook-length | na | the draft sets no limit for the hook\n- The sample listing | body-length | na | the draft sets no limit for the body\n- The sample listing | trademark-order | fail | \"Pemryx Timesheets Pro\" leads with Pemryx - the accepted shape is \"<App> for Pemryx\"\n- The sample listing | keyword-spam | fail | timesheets x6, pemryx x5 - more than 3\n- The sample listing | unattributed-testimonial | fail | 1 quoted testimonial with no named source\n- The sample\n\n[... 1543 more characters cut from this display ...]"
}

Reply:

{
 "task": "review",
 "summary": "Rejected on seven named triggers plus three judgment findings; rename to Timesheets for Pemryx, cut the repetition, guarantee and anonymous quote, and supply a public landing page and a support channel.",
 "verdict": "reject",
 "violations": [
  {
   "trigger": "trademark-order",
   "field": "name",
   "finding": "Pemryx Timesheets Pro leads with the platform name.",
   "fix": "Rename to the App for Pemryx shape, for example Timesheets Pro for Pemryx."
  },
  {
   "trigger": "keyword-spam",
   "field": "body",
   "finding": "Timesheets appears 6 times and Pemryx 5 times, past the limit of 3.",
   "fix": "Cut each term to 3 uses or fewer across the hook and body."
  },
  {
   "trigger": "unattributed-testimonial",
   "field": "body",
   "finding": "The quote about no longer chasing timesheets names no source.",
   "fix": "Attribute it to a named person, role and organisation, or remove it."
  },
  {
   "trigger": "guarantee",
   "field": "body",
   "finding": "Guaranteed to cut reporting time in half is an unsubstantiated guarantee.",
   "fix": "Remove the guarantee or replace it with a sourced, measured result."
  },
  {
   "trigger": "landing-page",
   "field": "landing",
   "finding": "The landing page link points to a PDF brochure.",
   "fix": "Link a public web page about the app."
  },
  {
   "trigger": "support-channel",
   "field": "support",
   "finding": "No support channel is given.",
   "fix": "Add a support channel with a response time in business days."
  },
  {
   "trigger": "screenshot-geometry",
   "field": "screenshots",
   "finding": "The 1280x800 image is off the published 16:9.",
   "fix": "Replace it with a 16:9 capture such as 1920x1080."
  },
  {
   "trigger": "judgment",
   "field": "body",
   "finding": "The body opens by naming the app and then lists sync, roll-up and export mechanics instead of the customer outcome.",
   "fix": "Open with the result for the admin's team and tie each feature to a measurable result."
  },
  {
   "trigger": "judgment",
   "field": "categories",
   "finding": "Four categories (Productivity, Finance, HR, Reporting) exceed the new cap of 2.",
   "fix": "Keep the 2 categories that best fit the app."
  },
  {
   "trigger": "judgment",
   "field": "video",
   "finding": "The 95-second video exceeds the new 30-second ceiling for diligence buyers.",
   "fix": "Cut it to a captioned function proof of 30 seconds or less with no marketing footage."
  }
 ],
 "passes": [
  "Name is 21 characters against 40",
  "Screenshot count of 3 is within 3-5",
  "A privacy policy is linked"
 ],
 "caveats": [
  "The page confirmed the privacy link exists, not that it covers the 4 checklist items."
 ]
}

What step 5 prints for it: reject | name trademark-order | body keyword-spam | body unattributed-testimonial | body guarantee | landing landing-page | support support-channel | screenshots screenshot-geometry | body judgment | categories judgment | video judgment

The page's check: kept - nothing to flag

review - A small operator-run marketplace

The input carries the saved rulebook's decisions (R.laneForm fills carry from the rulebook reply), as the page's handoff does. The page's listing violations: unattributed-testimonial.

Input (draft is 1582 characters; the first 1399 are shown; facts is 2313 characters; the first 1392 are shown):

{
 "task": "review",
 "market": "Marketplace: Lanternly Apps; platform name: Lanternly\nWhat it sells and to whom: A curated set of add-ons for Lanternly, a scheduling suite for clinics; clinic staff install add-ons themselves.\nStage: Live, content rules ad hoc; listings: Under 100 listings in 12 months; Our team maintains listings for vendors; curation: Hard gate; End users install self-serve\nVersioned releases or scheduled deprecations: no; languages: several; machine translation: no; region-gated visibility: no; badges: No badge program\nHard deadline: no; A compounding asset; effort ceiling: Low; partner console: no; ranked search: no; link crawler: yes; search-led discovery: no",
 "draft": "Draft rules (field | rule | limit):\n- name | at most 30 characters; trademark order: <App> for Lanternly | 30\n- hook | at most 80 characters | 80\n- body | at most 1500 characters; reject if keyword-spam, guarantee or unattributed-testimonial | 1500\n- screenshots | 3 to 5 images of the real app UI, no PII | 5\n- categories | at most 2 from the published list | 2\n- landing page | must be a public web page, not a PDF or repository | -\n- support | must state a response time in business days | -\n- privacy | must link a policy covering data stored and retention | -\nMedia (asset | ratio | count): screenshot 16:10 3-5; video ceiling: 150; category cap: 2; keyword repetition count: 4\nDecay (trigger | owner | consequence): Listing not updated in 12 months | Marketplace editor | Moved to uncategorized until updated; App UI changed in a release | Vendor | Out-of-date content violation, 30-day fix window\nBadges (badge | location | currency): none\nSample listing: name \"Reminders for Lanternly\"; hook \"Fewer no-shows with text reminders patients actually answer.\"; body:\nClinics using Reminders cut missed appointments by sending a confirmation text two days ahead and a reminder on the morning of the visit. Patients reply to confirm or reschedule, and the change lands in the Lanternly calendar. \"Our front desk gets an hour back every day.\"\ncategories: Patient engagement, Scheduling; screenshots:\n\n[... 183 more characters cut from this display ...]",
 "carry": "Rungs: strictness triggers, media content-first, taxonomy capped, localization optin, badges none, decay downgrade-policy\nReject criteria: name - Reject past 30 characters or on trademark-order.; hook - Reject past 80 characters or on keyword-spam.; body - Reject past 1500 characters or on keyword-spam, guarantee, unattributed-testimonial or misrepresentation.; screenshots - Reject fewer than 3 or more than 5, any image off 16:10, or any PII shown.; categories - Reject more than 2 categories or any category off the published list.; landing - Reject on landing-page.; support - Reject on support-channel when no response time in business days is stated.; privacy - Reject on privacy-policy when either coverage item is missing.\nMedia: screenshot 16:10 x3-5; video 150 s; category cap 2\nTriggers: keyword-spam - Any term other than common words repeated more than 4 times across the hook and body.; unattributed-testimonial - Any quote without a named person, role and clinic or organisation.; guarantee - Any guaranteed result the listing cannot substantiate.; trademark-order - Lanternly comes last: Task Tracker for Lanternly is accepted, Lanternly Task Tracker is rejected.; misrepresentation - New trigger: any claim the add-on does not deliver, or an undisclosed third-party dependency or its cost.; landing-page - The landing page link is missing, not public, or a PDF or repository.; support-channel - No support channel, or no response time in business days.; privacy-policy - No policy link, or a policy that omits data stored or retention.; out-of-date - Screenshots or copy that no longer match the add-on after a release changed its UI.",
 "facts": "The page's checks (1 fail, 0 warn, 18 pass, 1 n/a); gates passed 4 of 4:\n- The rulebook | objective | pass | 8 fields, every one objective\n- The rulebook | geometry | pass | screenshot 16:10\n- The rulebook | taxonomy | pass | at most 2 categories per listing\n- The rulebook | decay | pass | 2 triggers\n- The rulebook | badges | na | no badge program - a legitimate architecture\n- The rulebook | trademark | pass | present\n- The rulebook | video | pass | 150 s; self-serve installs: up to 2-3 minutes, promotional tone allowed, captions required\n- The sample listing | name-length | pass | 23 characters against a limit of 30\n- The sample listing | hook-length | pass | 60 characters against a limit of 80\n- The sample listing | body-length | pass | 272 characters against a limit of 1500\n- The sample listing | trademark-order | pass | \"<App> for Lanternly\"\n- The sample listing | keyword-spam | pass | no term past 4\n- The sample listing | unattributed-testimonial | fail | 1 quoted testimonial with no named source\n- The sample listing | guarantee | pass | no guarantee claims\n- The sample listing | landing-page | pass | a public page\n- The sample listing | support-channel | pass | with a response commitment\n- The sample listing | privacy-policy | pass | linked\n- The sample listing | screenshot-geometry | pass | 3 screenshots at 16:10\n- The sample listing | screenshot-count | pass | 3\n\n[... 921 more characters cut from this display ...]"
}

Reply:

{
 "task": "review",
 "summary": "Rejected only for an anonymous testimonial; everything else meets the rulebook, so attributing or removing the front-desk quote clears the listing.",
 "verdict": "reject",
 "violations": [
  {
   "trigger": "unattributed-testimonial",
   "field": "body",
   "finding": "The quote about the front desk getting an hour back names no source.",
   "fix": "Attribute it to a named person, role and clinic, or remove it."
  }
 ],
 "passes": [
  "Name follows the App for Lanternly order at 23 characters",
  "Hook and body lead with the clinic's outcome, fewer no-shows",
  "3 screenshots at 16:10",
  "2 categories against a cap of 2",
  "Public landing page, a support reply within 1 business day and a linked privacy policy"
 ],
 "caveats": [
  "The page confirmed the privacy link exists, not that it covers data stored and retention."
 ]
}

What step 5 prints for it: reject | body unattributed-testimonial

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./lrkit.js"); require("./lrreply.js")), then:

const K = RuleKit, R = RuleReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme Apps", platform: "Acme",
  fields: "name | at most 30 characters | 30\nhook | should be compelling |", catcap: "2", kwmax: "3",
  lname: "Acme Sync", lbody: "Guaranteed results." }));
// f.rules (each read as objective or not), f.gate (the five gates), f.ladder (the rungs), f.lints and f.violations (the listing)
// K.selfCheckMd(f) - the submitter self-check
const b = K.buildInput(f, "rulebook");                       // or "review" (R.laneForm(form, "review", rulebook) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "rulebook");
// R.check(reply, b)                                         // {status: kept or flagged, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://listing-rulebook.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"}
}

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":"listing-rulebook"}' | jq -r '.data.token')

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}' 

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": "rulebook", "market": "Marketplace: Pemryx Marketplace; platform name: Pemryx\nWhat it sells and to whom: ...", "draft": "Draft rules (field | rule | limit):\n- name | max characters | 40\n- hook | should be compelling | - | principles only ...", "carry": "", "facts": "The page's checks (13 fail, ...); gates passed 0 of 5:\n- The rulebook | objective | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

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 listing-rulebook:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A rulebook reply carries summary, rungs, fields, media, triggers, taxonomy, decay, badges, runway and caveats; a review reply carries summary, verdict, violations, passes and caveats. The code below prints the gist of either: each field and its reject criterion, or the verdict and each violation.

KEY="listing-rulebook:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "review" then (.verdict, (.violations[] | "\(.field) \(.trigger)")) 
       else (.fields[] | "\(.field): \(.reject_criterion)") end' reply.json

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: listing-rulebook:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json