← App Vetting
API
Tokens

Run App Vetting from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page ranks the pipeline by data reach and reviewer capacity, works out the publish-channel controls, and checks the re-review triggers, the appeal and the enforcement ladder, then hands it all to the run as facts. The run writes the review process or the developer policy; the page then holds it to its own facts (avreply.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":"app-vetting"}), 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, marketplace, controls, facts) is missing. The page always sends all five of task, marketplace, controls, carry, facts; carry is an empty string for process.
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 - for the reviewers, then for the developers:

A missing or unknown task is answered as process. 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 process into carry for the policy lane.

The input

FieldRequiredWhat
taskyesprocess or policy. If it is missing or neither, do process and set "task": "process".
marketplaceyesthe marketplace and what prompted this, review today, reviewers and tooling, the maximum data reach, volumes, propagation, curation, the channel controls in place, the appeal and runway; "not given" means not given.
controlsyesthe re-review triggers drafted so far (each read as objective or subjective) and the enforcement ladder drafted so far.
carrysend it (empty for process)policy task only: the process decisions, when the process task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, the verdict and the rankings. 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-review - the only standard the run applies:

House rules

The replies

process

Write the review-process design the platform team will staff.

  1. summary - one sentence, at most 40 words: the pipeline shape, and the first thing that must change.
  2. tiers - at least two objects - the tier most apps sit in and the tier an app escalates to: tier (a name), reach (the data the apps in it can reach), depth (one sentence: what review it gets), trigger (one sentence: the objective condition that moves an app into it, which a developer can check alone - named in this marketplace's own scopes and data, never example scopes it does not offer).
  3. pipeline - shape (hard, staged, ladder or functional, exactly as the facts rank first), why (one sentence naming the moves), automated and human (short strings: what each side handles).
  4. admission - at least four objects: check (one check of this marketplace's admission review that applies to these apps - skip billing-flow tracing when no billing runs through the app), mode (automated or human).
  5. rereview - rung (fastpath, recert or expiry, exactly as the facts rank first), triggers (short strings: the objective triggers), floor (one sentence: the annual expiry floor when apps reach regulated data, else empty).
  6. channel - one object per control the facts require, no more: control (fido, tokens, hold, oidc or signed), setting (one sentence: how it is set on this marketplace - the hold at 24 to 48 hours).
  7. appeal - rung (defined, dashboard, channel or resubmit, exactly as the facts rank first), turnaround (a number of days, written like "7 days (proposed)" when the input gives none), independence (one sentence: who reviews the appeal instead of the original reviewer).
  8. kpis - at least three short strings.
  9. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "process",
  "summary": "one sentence",
  "tiers": [ { "tier": "a name", "reach": "the data", "depth": "one sentence", "trigger": "one sentence" } ],
  "pipeline": { "shape": "hard", "why": "one sentence", "automated": [ "a check" ], "human": [ "a judgment" ] },
  "admission": [ { "check": "one check", "mode": "automated" } ],
  "rereview": { "rung": "fastpath", "triggers": [ "new OAuth scopes" ], "floor": "" },
  "channel": [ { "control": "fido", "setting": "one sentence" } ],
  "appeal": { "rung": "defined", "turnaround": "5 business days", "independence": "one sentence" },
  "kpis": [ "a KPI" ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in avreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no web link; no backticks; a summary of at most 40 words; the ranked pipeline shape; tiers each with an escalation trigger; at least four admission checks marked automated or human; the ranked re-review rung with objective triggers for scopes, endpoints and data handling, nothing subjective, and an annual floor for regulated reach; exactly the required channel controls, the hold at 24-48 hours; the ranked appeal with a turnaround and an independent reviewer; at least three KPIs.

policy

Write what the marketplace publishes to developers, and the enforcement ladder it runs.

  1. summary - one sentence, at most 40 words.
  2. minor_update - one or two sentences: what counts as a minor update on this marketplace - no new scopes, no new external endpoints, no change in data handling.
  3. escalation - one sentence: the tier-escalation trigger a developer checks before submitting.
  4. triggers - short strings: the objective triggers for a full re-review.
  5. ladder - exactly six objects in this order - finding raised, remediation SLA by severity, listing hidden, new installs suspended, tokens revoked or kill switch, delisting with customer notice: rung (its name for this marketplace), owner (the role, as the input names it or appointed and said so), notice (the developer-notice period - for the remediation rung, one period per severity the input gives, others proposed), evidence (what fires it).
  6. publisher - one sentence: the publisher-level consequence for chronic or unresponsive submitters, distinct from one app - labelled proposed when the input names none.
  7. appeal_steps - short strings in order, starting from the policy the rejection cites.
  8. runway - one sentence: the notice before a new requirement applies to listed apps - at least six months.
  9. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "policy",
  "summary": "one sentence",
  "minor_update": "one or two sentences",
  "escalation": "one sentence",
  "triggers": [ "new OAuth scopes" ],
  "ladder": [ { "rung": "Finding raised", "owner": "a role", "notice": "a period", "evidence": "what fires it" } ],
  "publisher": "one sentence",
  "appeal_steps": [ "a step" ],
  "runway": "one sentence",
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; a minor update defined by scopes, endpoints and data handling; a self-assessable escalation trigger; objective triggers only; six ladder rungs in the order finding, remediation SLA, hidden, installs suspended, tokens revoked, delisting - each with an owner, a notice period and its evidence; publisher-level enforcement; appeal steps that start from the cited policy; a runway of at least six months.

Worked examples

Three runs from the page's free samples (invented companies): the process for a helpdesk marketplace with an open back door, the policy for the same marketplace carrying that process, and the process for an accounting marketplace on regulated data. 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.

process - A helpdesk marketplace with an open back door

The page read it as: the back door is open - harden the publish channel first; pipeline staged, hard, ladder, functional; re-review fastpath, recert, expiry; appeal defined, dashboard, channel; channel controls fido, tokens, hold, oidc; 0 of 5 gates; failing checks: tiers, minor, channel, appeal, ladder.

Input (facts is 2938 characters; the first 1393 are shown):

{
 "task": "process",
 "marketplace": "Marketplace: Helmquill Apps\nWhat it is and what prompted this: The app marketplace of a helpdesk SaaS; 120 listed apps read and write tickets, and review is whoever is free that week.\nReview today: Live, review is ad hoc; reviewers: Borrowed product engineers; reviewer hours a week: 10; existing SAST/secret scanning: no; scanning on every release: no; deterministic automated test suite: no\nMaximum data an app can reach: Read-write on business records; new apps a month: 6; updates a month: 40; publishers: 70; updates auto-propagate to installed tenants; clients run auto-updated code: no\nCuration chosen by strategy: hybrid; developer friction is the binding risk: no; tier trigger self-assessable: no; EU business users: no\nPublish channel: FIDO 2FA no, short-lived tokens no, propagation hold none, OIDC publishing no, signed artifacts no\nAppeals: An escalation channel; rejections cite the policy: no; independent reviewer: no; turnaround: not given; runway for new requirements: not given; hard date: no; a compounding trust program; effort ceiling: medium; as-of: 2026-11-01",
 "controls": "Re-review triggers (trigger | reading):\n- Substantial changes to functionality | SUBJECTIVE - nobody can self-assess it\n- New OAuth scopes | objective: scopes\n\nEnforcement ladder (rung | kind | owner | notice | evidence):\n- Listing removed | delist | owner: Support lead | notice: none | evidence: customer complaints",
 "carry": "",
 "facts": "The page's checks (5 fail, 2 warn, 0 pass, 2 n/a); verdict: the back door is open - harden the publish channel first:\n- Pipeline and tiers | shape | na | Staged gate with an objective fast path > Single hard gate > Two-rung ladder (list, then certify) > Functional-only review - about 276 reviewer-hours a month of full reviews against 43 available - reviewer cost is the constraint, so the staged fast path; run it as automated pre-screen then human triage\n- Pipeline and tiers | tiers | fail | write the escalation trigger so a developer can self-assess it: restricted scopes (mailbox, file, CRM read-write) or write access to customer data move an app up one tier\n- Updates and the publish channel | minor | fail | subjective trigger nobody can self-assess: \"Substantial changes to functionality\"; no trigger for new external endpoints, changed data handling\n- Updates and the publish channel | rereview | na | Objective minor-update fast path > Unconditional re-certification > Periodic expiry re-review; scan every release, or the fast path becomes the gap\n- Updates and the publish channel | scanning | warn | the fast path skips review for minor updates - without scanning on every release it is the gap\n- Updates and the publish channel | channel | fail | missing: Phishing-resistant (FIDO) 2FA on publisher accounts (the default rung - closes the credential-theft vector that actually\n\n[... 1545 more characters cut from this display ...]"
}

Reply:

{
 "task": "process",
 "summary": "Helmquill Apps moves to a staged gate with an objective fast path, and the first change is closing the publish channel: FIDO 2FA, short-lived publish tokens, a held propagation with a kill switch, then OIDC publishing.",
 "tiers": [
  {
   "tier": "Standard",
   "reach": "Read-only access to helpdesk tickets and account metadata, with no write scopes.",
   "depth": "Automated pre-screen plus a human triage of only the critical and high findings it raises.",
   "trigger": "An app lands here when its manifest requests no write scope on tickets or any other customer record."
  },
  {
   "tier": "Restricted",
   "reach": "Read-write access to tickets and other business records in a tenant.",
   "depth": "Full review by a product engineer: scope necessity, data destinations after consent, secrets handling and the OAuth, billing and install flows.",
   "trigger": "An app moves up to this tier when its manifest requests any write scope on tickets or customer records, which a developer can read off their own manifest before submitting."
  }
 ],
 "pipeline": {
  "shape": "staged",
  "why": "About 276 reviewer-hours a month of full reviews against 43 available makes reviewer time the constraint, so new apps and major versions get full review while objectively minor updates clear the fast path under scanning.",
  "automated": [
   "Secret and SAST scanning of every submission and every release",
   "Manifest scope diff against the last approved version",
   "Outbound endpoint diff against the last approved version",
   "Tickets raised only for critical and high findings"
  ],
  "human": [
   "Triage of ambiguous scanner findings",
   "Scope-necessity judgment for ticket write access",
   "Full review of new apps and major versions in the Restricted tier"
  ]
 },
 "admission": [
  {
   "check": "Scan the codebase for secrets in the repository, in URLs and in logs.",
   "mode": "automated"
  },
  {
   "check": "Inspect the manifest and list every requested scope against the tier it implies.",
   "mode": "automated"
  },
  {
   "check": "Confirm each ticket write scope is needed by a feature the reviewer can see working in the app.",
   "mode": "human"
  },
  {
   "check": "Trace the OAuth, billing and install flows end to end in a test helpdesk tenant.",
   "mode": "human"
  },
  {
   "check": "Record every external endpoint, subprocessor and retention period that ticket data reaches after consent.",
   "mode": "human"
  },
  {
   "check": "Verify the app UI matches the listing and the consent screen.",
   "mode": "human"
  }
 ],
 "rereview": {
  "rung": "fastpath",
  "triggers": [
   "New OAuth scopes",
   "New external endpoints or domains",
   "Changed data handling, storage or subprocessors",
   "A payment-model change",
   "A new app type"
  ],
  "floor": ""
 },
 "channel": [
  {
   "control": "fido",
   "setting": "All 70 publisher accounts must sign in with a FIDO security key or passkey before they can publish to Helmquill Apps."
  },
  {
   "control": "tokens",
   "setting": "Publish tokens are scoped to one app with a 7-day default lifetime and a 90-day maximum."
  },
  {
   "control": "hold",
   "setting": "Each update waits a proposed 48 hours before it propagates to installed tenants, with a kill switch to halt it and an expedite path for security fixes."
  },
  {
   "control": "oidc",
   "setting": "Publishers ship releases from CI through OIDC trusted publishing, so no long-lived token sits in their pipelines."
  }
 ],
 "appeal": {
  "rung": "defined",
  "turnaround": "10 business days (proposed)",
  "independence": "A product engineer who did not make the original rejection decides the appeal, against the policy the rejection cites (proposed assignment)."
 },
 "kpis": [
  "First-pass approval rate for new submissions",
  "Time to first decision against the published SLA",
  "Share of the 40 monthly updates that clear the fast path",
  "Appeal overturn rate",
  "Post-approval findings and time to remediation",
  "Scanner false-positive rate"
 ],
 "caveats": [
  "Today updates auto-propagate with no hold, no FIDO 2FA, no short-lived tokens and no OIDC, and nothing scans releases, so the fast path stays closed until per-release scanning runs.",
  "The trigger Substantial changes to functionality is retired; the five objective triggers above replace it.",
  "Rejections do not cite a policy today and the escalation channel is not an appeal; every rejection must cite one before appeals open.",
  "No runway is set for new requirements; listed apps need at least six months, and the enforcement ladder has only delisting today."
 ]
}

What step 5 prints for it: staged: Helmquill Apps moves to a staged gate with an objective fast path, and the first change is closing the publish channel: FIDO 2FA, short-lived publish tokens, a held propagation with a kill switch, then OIDC publishing.

The page's check: kept - nothing to flag

policy - A helpdesk marketplace with an open back door

The input carries the saved process (R.laneForm fills carry from the process reply), as the page's handoff does. Failing checks on the page: tiers, minor, channel, appeal, ladder.

Input (facts is 3087 characters; the first 1393 are shown):

{
 "task": "policy",
 "marketplace": "Marketplace: Helmquill Apps\nWhat it is and what prompted this: The app marketplace of a helpdesk SaaS; 120 listed apps read and write tickets, and review is whoever is free that week.\nReview today: Live, review is ad hoc; reviewers: Borrowed product engineers; reviewer hours a week: 10; existing SAST/secret scanning: no; scanning on every release: no; deterministic automated test suite: no\nMaximum data an app can reach: Read-write on business records; new apps a month: 6; updates a month: 40; publishers: 70; updates auto-propagate to installed tenants; clients run auto-updated code: no\nCuration chosen by strategy: hybrid; developer friction is the binding risk: no; tier trigger self-assessable: no; EU business users: no\nPublish channel: FIDO 2FA no, short-lived tokens no, propagation hold none, OIDC publishing no, signed artifacts no\nAppeals: An escalation channel; rejections cite the policy: no; independent reviewer: no; turnaround: not given; runway for new requirements: not given; hard date: no; a compounding trust program; effort ceiling: medium; as-of: 2026-11-01",
 "controls": "Re-review triggers (trigger | reading):\n- Substantial changes to functionality | SUBJECTIVE - nobody can self-assess it\n- New OAuth scopes | objective: scopes\n\nEnforcement ladder (rung | kind | owner | notice | evidence):\n- Listing removed | delist | owner: Support lead | notice: none | evidence: customer complaints",
 "carry": "Pipeline: Staged gate with an objective fast path - About 276 reviewer-hours a month of full reviews against 43 available makes reviewer time the constraint, so new apps and major versions get full review while objectively minor updates clear the fast path under scanning.\nTiers: Standard (An app lands here when its manifest requests no write scope on tickets or any other customer record.); Restricted (An app moves up to this tier when its manifest requests any write scope on tickets or customer records, which a developer can read off their own manifest before submitting.)\nRe-review: Objective minor-update fast path - triggers: New OAuth scopes; New external endpoints or domains; Changed data handling, storage or subprocessors; A payment-model change; A new app type\nAppeal: A defined, human-reviewed appeal, 10 business days (proposed)",
 "facts": "The page's checks (5 fail, 2 warn, 0 pass, 2 n/a); verdict: the back door is open - harden the publish channel first:\n- Pipeline and tiers | shape | na | Staged gate with an objective fast path > Single hard gate > Two-rung ladder (list, then certify) > Functional-only review - about 276 reviewer-hours a month of full reviews against 43 available - reviewer cost is the constraint, so the staged fast path; run it as automated pre-screen then human triage\n- Pipeline and tiers | tiers | fail | write the escalation trigger so a developer can self-assess it: restricted scopes (mailbox, file, CRM read-write) or write access to customer data move an app up one tier\n- Updates and the publish channel | minor | fail | subjective trigger nobody can self-assess: \"Substantial changes to functionality\"; no trigger for new external endpoints, changed data handling\n- Updates and the publish channel | rereview | na | Objective minor-update fast path > Unconditional re-certification > Periodic expiry re-review; scan every release, or the fast path becomes the gap\n- Updates and the publish channel | scanning | warn | the fast path skips review for minor updates - without scanning on every release it is the gap\n- Updates and the publish channel | channel | fail | missing: Phishing-resistant (FIDO) 2FA on publisher accounts (the default rung - closes the credential-theft vector that actually\n\n[... 1694 more characters cut from this display ...]"
}

Reply (long strings shortened for display - see the cuts below):

{
 "task": "policy",
 "summary": "Helmquill Apps publishes an objective minor-update rule, a manifest-based tier trigger for ticket write scopes, five objective re-review triggers, a six-rung owned enforcement ladder, a cited-policy appeal and six months of runway.",
 "minor_update": "On Helmquill Apps a minor update requests no new OAuth scope, calls no new external endpoint or domain, and makes no change to how ticket or customer data is handled, stored, retained or shared with subprocessors. It also keeps the same [... 120 more characters cut from this display]",
 "escalation": "Before submitting, check your own manifest: if it requests any write scope on tickets or any other customer record, the app enters the Restricted tier and gets a full review.",
 "triggers": [
  "New OAuth scopes",
  "New external endpoints or domains",
  "Changed data handling, storage or subprocessors",
  "A payment-model change",
  "A new app type"
 ],
 "ladder": [
  {
   "rung": "Finding raised",
   "owner": "Marketplace review lead (appointed, proposed)",
   "notice": "Immediate written notice to the publisher (proposed)",
   "evidence": "A critical or high scanner hit on a release, a reviewer finding, or a customer or researcher report the triage confirms"
  },
  {
   "rung": "Remediation SLA by severity",
   "owner": "Marketplace review lead (appointed, proposed)",
   "notice": "7 days for critical, 30 days for high (proposed)",
   "evidence": "A confirmed critical or high finding still open after triage"
  },
  {
   "rung": "Listing hidden from search and browse",
   "owner": "Marketplace review lead (appointed, proposed)",
   "notice": "48 hours (proposed)",
   "evidence": "A missed remediation deadline on a critical or high finding"
  },
  {
   "rung": "New installs suspended",
   "owner": "Marketplace review lead (appointed, proposed)",
   "notice": "24 hours (proposed)",
   "evidence": "A confirmed exploitable flaw that can expose or alter tickets in installed tenants"
  },
  {
   "rung": "Tokens revoked and kill switch on the propagating update",
   "owner": "Platform security on-call (appointed, proposed)",
   "notice": "Immediate, with notice to the publisher as the action is taken (proposed)",
   "evidence": "Active exploitation, ticket data exposure, or a compromised publisher account"
  },
  {
   "rung": "Delisting with customer notice",
   "owner": "Support lead",
   "notice": "14 days to the publisher and to every installed tenant (proposed)",
   "evidence": "Customer complaints confirmed as a refused or failed remediation, or a repeat of a confirmed critical finding"
  }
 ],
 "publisher": "A publisher with two or more apps past their remediation deadline, or who does not answer findings within the notice period, loses fast-path clearance across all their apps and has every new submission held for full review until the [... 59 more characters cut from this display]",
 "appeal_steps": [
  "The rejection names the specific Helmquill Apps policy the app violates and the evidence behind it.",
  "Within 30 days (proposed) the developer files an appeal that answers that cited policy, with any fix or explanation attached.",
  "A product engineer who did not make the original decision reviews the appeal against the cited policy (proposed assignment).",
  "The developer receives a written decision within 10 business days (proposed), stating whether the rejection is upheld or overturned and why."
 ],
 "runway": "Any new requirement Helmquill Apps adds applies to already listed apps no sooner than six months after it is published to developers (proposed), with security fixes under the expedite path as the only exception.",
 "caveats": [
  "Substantial changes to functionality is retired as a trigger; the five objective triggers replace it.",
  "Rejections do not cite a policy today and the escalation channel is not an appeal; both change before appeals open.",
  "The ladder today has only Listing removed, owned by the Support lead with no notice; every other owner and notice period above is proposed.",
  "Releases are not scanned today, so the minor-update fast path stays closed until per-release scanning runs."
 ]
}

Cut from this display only (the saved reply and the check use the full text): minor_update (356 characters, 236 shown); publisher (291 characters, 232 shown).

What step 5 prints for it: Finding raised: Marketplace review lead (appointed, proposed) | Remediation SLA by severity: Marketplace review lead (appointed, proposed) | Listing hidden from search and browse: Marketplace review lead (appointed, proposed) | New installs suspended: Marketplace review lead (appointed, proposed) | Tokens revoked and kill switch on the propagating update: Platform security on-call (appointed, proposed) | Delisting with customer notice: Support lead

The page's check: kept - nothing to flag

process - An accounting marketplace on regulated data

The page read it as: a review process you can staff and defend; pipeline hard, staged, ladder, functional; re-review fastpath, recert, expiry; appeal defined, dashboard, channel; channel controls fido, tokens, hold; 5 of 5 gates; failing checks: none.

Input (facts is 2197 characters; the first 1396 are shown):

{
 "task": "process",
 "marketplace": "Marketplace: Tallymoor Marketplace\nWhat it is and what prompted this: The marketplace of an accounting platform; listed apps read and write ledgers and payroll, so every app touches regulated financial data.\nReview today: Formal process, revisiting for scale or friction; reviewers: Dedicated security engineers; reviewer hours a week: 40; existing SAST/secret scanning: yes; scanning on every release: yes; deterministic automated test suite: no\nMaximum data an app can reach: Regulated data (health, financial, personal); new apps a month: 4; updates a month: 15; publishers: 35; updates auto-propagate to installed tenants; clients run auto-updated code: no\nCuration chosen by strategy: hard; developer friction is the binding risk: no; tier trigger self-assessable: yes; EU business users: no\nPublish channel: FIDO 2FA yes, short-lived tokens yes, propagation hold 48 hours, OIDC publishing no, signed artifacts no\nAppeals: A defined, human-reviewed appeal; rejections cite the policy: yes; independent reviewer: yes; turnaround: 5 days; runway for new requirements: 6 months; hard date: no; a compounding trust program; effort ceiling: medium; as-of: 2026-11-01",
 "controls": "Re-review triggers (trigger | reading):\n- New OAuth scopes | objective: scopes\n- New external endpoints or domains | objective: endpoints\n- Changed data handling, storage or subprocessors | objective: data\n- A payment-model change | objective: payment\n- A new app type | objective: apptype\n\nEnforcement ladder (rung | kind | owner | notice | evidence):\n- Finding raised | finding | owner: Marketplace security lead | notice: none - immediate | evidence: a scanner hit or disclosure report\n- Remediation SLA by severity | remediation | owner: Marketplace security lead | notice: 7 days critical, 30 days high | evidence: an open finding past triage\n- Listing hidden from search | hidden | owner: Marketplace operations manager | notice: 48 hours | evidence: a missed critical SLA\n- New installs suspended | suspend | owner: Marketplace operations manager | notice: 24 hours | evidence: a confirmed exploitable flaw\n- Tokens revoked, kill switch | revoke | owner: Marketplace security lead | notice: immediate | evidence: active exploitation or data exposure\n- Delisting with customer notice | delist | owner: Head of marketplace | notice: 14 days | evidence: a refused or failed remediation",
 "carry": "",
 "facts": "The page's checks (0 fail, 1 warn, 6 pass, 1 n/a); verdict: a review process you can staff and defend:\n- Pipeline and tiers | shape | na | Single hard gate > Staged gate with an objective fast path > Two-rung ladder (list, then certify) > Functional-only review - apps can reach regulated data - the hard gate holds regardless of volume; run it as automated pre-screen then human triage\n- Pipeline and tiers | tiers | pass | restricted scopes or write access to customer data move an app up a tier, and developers can check it before submitting\n- Updates and the publish channel | minor | pass | objective triggers: new OAuth scopes, new external endpoints, changed data handling, a payment-model change, a new app type\n- Updates and the publish channel | rereview | warn | Objective minor-update fast path > Unconditional re-certification > Periodic expiry re-review; add an annual expiry floor for the restricted-scope tier\n- Updates and the publish channel | channel | pass | in place: Phishing-resistant (FIDO) 2FA on publisher accounts, Short-lived, scoped publish tokens, A propagation hold before auto-update reaches tenants\n- Appeals and enforcement | appeal | pass | A defined, human-reviewed appeal within 5 days; ranked: A defined, human-reviewed appeal > A dashboard-integrated appeal > An escalation channel\n- Appeals and enforcement | ladder | pass | all six rungs, each owned, with\n\n[... 801 more characters cut from this display ...]"
}

Reply:

{
 "task": "process",
 "summary": "Tallymoor Marketplace keeps a single hard gate because every app reaches regulated ledger and payroll data, and the first change is an annual expiry re-review for apps holding read-write ledger or payroll scopes.",
 "tiers": [
  {
   "tier": "Regulated read",
   "reach": "Read-only access to ledgers or payroll records.",
   "depth": "Full hard-gate review by a security engineer after the automated pre-screen, covering scope necessity, data destinations and secrets handling.",
   "trigger": "Every app enters at least here, because any ledger or payroll scope in its manifest reaches regulated financial data."
  },
  {
   "tier": "Regulated read-write",
   "reach": "Write access to ledgers or payroll records in a customer tenant.",
   "depth": "Full hard-gate review plus a traced test of every write path into ledger and payroll records, with an annual expiry re-review.",
   "trigger": "An app moves up to this tier when its manifest requests any write scope on ledgers or payroll, which a developer can check in their own manifest before submitting."
  }
 ],
 "pipeline": {
  "shape": "hard",
  "why": "Apps reach regulated financial data, so every new app and every update that hits a trigger passes one full gate run as automated pre-screen then human triage, whatever the volume.",
  "automated": [
   "SAST and secret scanning on every submission and every release",
   "Manifest scope diff against the last approved version",
   "Outbound endpoint and subprocessor diff",
   "Tickets raised only for critical and high findings"
  ],
  "human": [
   "Triage of ambiguous scanner findings",
   "Scope-necessity judgment for ledger and payroll write access",
   "Full gate review of every new app and triggered update"
  ]
 },
 "admission": [
  {
   "check": "Scan the codebase for secrets in the repository, in URLs and in logs.",
   "mode": "automated"
  },
  {
   "check": "Inspect the manifest for every ledger and payroll scope and the tier it implies.",
   "mode": "automated"
  },
  {
   "check": "Require demonstrated necessity for each ledger or payroll write scope.",
   "mode": "human"
  },
  {
   "check": "Record every endpoint, subprocessor and retention period that financial data reaches after consent.",
   "mode": "human"
  },
  {
   "check": "Trace the OAuth, billing and install flows in a test accounting tenant.",
   "mode": "human"
  },
  {
   "check": "Verify the app UI matches the listing and the consent screen.",
   "mode": "human"
  }
 ],
 "rereview": {
  "rung": "fastpath",
  "triggers": [
   "New OAuth scopes",
   "New external endpoints or domains",
   "Changed data handling, storage or subprocessors",
   "A payment-model change",
   "A new app type"
  ],
  "floor": "Apps in the Regulated read-write tier expire for a full re-review every year, whether or not an update fired a trigger."
 },
 "channel": [
  {
   "control": "fido",
   "setting": "FIDO 2FA stays mandatory on all 35 publisher accounts."
  },
  {
   "control": "tokens",
   "setting": "Publish tokens stay short-lived and scoped to one app."
  },
  {
   "control": "hold",
   "setting": "Updates keep the 48-hour hold before reaching installed tenants, with a kill switch and an expedite path for security fixes."
  }
 ],
 "appeal": {
  "rung": "defined",
  "turnaround": "5 days",
  "independence": "A security engineer who did not make the original decision reviews the appeal against the policy the rejection cites."
 },
 "kpis": [
  "First-pass approval rate",
  "Time to first decision against the published SLA",
  "Share of the 15 monthly updates that clear the fast path",
  "Appeal overturn rate",
  "Post-approval findings and time to remediation",
  "Scanner false-positive rate"
 ],
 "caveats": [
  "No annual expiry re-review exists yet for the read-write tier; the floor above closes that gap."
 ]
}

What step 5 prints for it: hard: Tallymoor Marketplace keeps a single hard gate because every app reaches regulated ledger and payroll data, and the first change is an annual expiry re-review for apps holding read-write ledger or payroll scopes.

The page's check: kept - nothing to flag

Build the input yourself

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

const K = VettingKit, R = VettingReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", reach: "readwrite", propagation: "auto", fido: "yes", tokens: "yes",
  triggers: "New OAuth scopes\nSubstantial changes", ladder: "Listing removed | Ops lead | 7 days | refused fix" }));
// f.shapeRank, f.rereviewRank, f.appealRank (the ranked menus), f.channel (controls needed), f.triggerRows, f.ladderRows, f.gate
// K.selfCheckMd(f), K.ladderCsv(f) - the developer self-check and the ladder table
const b = K.buildInput(f, "process");                        // or "policy" (R.laneForm(form, "policy", process) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "process");
// 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://app-vetting.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":"app-vetting"}' | 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": "process", "marketplace": "Marketplace: Helmquill Apps
What it is and what prompted this: ...", "controls": "Re-review triggers (trigger | reading):
- Substantial changes to functionality | SUBJECTIVE ...", "carry": "", "facts": "The page's checks (...); verdict: the back door is open - harden the publish channel first:
- Pipeline and tiers | shape | na | ..."}
# - 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 app-vetting:<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 process reply carries summary, tiers, pipeline, admission, rereview, channel, appeal, kpis and caveats; a policy reply carries summary, minor_update, escalation, triggers, ladder, publisher, appeal_steps, runway and caveats. The code below prints the gist of either: the pipeline shape and the summary, or the owner of each enforcement rung.

KEY="app-vetting:$(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 == "policy" then (.ladder[] | "\(.rung): \(.owner)") 
       else "\(.pipeline.shape): \(.summary)" 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: app-vetting:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json