← Listing Lift
API
Tokens

Run Listing Lift from your own code

Both metered lanes are available over HTTP and each returns one JSON object. Listing Lift is for a B2B SaaS vendor's own listing on Salesforce AppExchange, the HubSpot App Marketplace, the Atlassian Marketplace, the Shopify App Store or the Slack Marketplace - not for consumer app stores. The division of labour: the page classifies the marketplace data-rich or data-dark, matches every buyer term as whole words against every field the marketplace indexes, checks the title convention, the field limits, the gallery, the video, the reviews and the review asks, ranks the instrumentation, optimization and review-velocity ladders, reads the five plan gates, labels every ranking claim with its provenance and confirms that no verdict contradicts a check, then hands it all to the run as facts. The run writes the rewritten listing or the review and refresh program; the page then holds it to its own facts (llreply.js). Listing Lift is not affiliated with or endorsed by Salesforce, HubSpot, Atlassian, Shopify or Slack.

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-lift"}), 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, listing, terms, facts) is missing. The page always sends all nine of task, marketplace, listing, terms, gallery, reviews, context, carry, facts; carry is an empty string for rewrite.
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 listing, then the standing work:

A missing or unknown task is answered as rewrite. Both lanes send the same nine 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 rewrite's fields, first screen and claims into carry for the program lane.

The input

FieldRequiredWhat
taskyesrewrite or program. If it is missing or neither, do rewrite and set "task": "rewrite".
marketplaceyesthe marketplace's name and id, live or pre-submission, its classification, its fields (id, whether it is indexed, the claim behind that and its label, the limit), its title convention and its video ceiling.
listingyesthe listing as pasted, one line per field of this marketplace (id, label, text).
termsyesthe buyer search terms, one per line with their source (support tickets, sales calls or the search-term report).
gallerysend itthe gallery assets, one per line: number, kind, caption, notes; then the demo video's length.
reviewssend itthe review summary: count, average, newest, trailing 90 days, the newest ten dates; or that the marketplace has no review system.
contextsend itthe interview answers: brand and maturity, analytics on, the funnel numbers, the badge and whether procurement gates on it, the review asks in use, the uninstall trend, the owner, the last change, the deadline, the mandate, the effort ceiling and the unfair advantages.
carrysend it (empty for rewrite)program task only: the rewrite's fields, first screen and claims when the rewrite ran first; may be empty.
factsyesthe page's checks, one line each with the area, the id, the subject, the status and the evidence separated by vertical bars, with the claim id and its label in brackets; then the coverage matrix, the three ladders, the badge, the plan gates, the provenance table, the KPIs, the refresh window and the footnote. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it and reply with only the JSON object.

The rules the model applies

From the source skill, @samber/integration-listing-optimization - the only standard the run applies:

House rules

The replies

rewrite

Rewrite the listing: the indexed fields in the marketplace's convention with the buyers' own words, the first screen, the video decision, one fix per failing listing or gallery check, and the provenance of every ranking claim.

  1. summary - one sentence, at most 40 words: what the rewrite changes and which buyer terms it now covers.
  2. fields - one object per indexed field the facts list, and no other field: field (exactly the page's field id: name, subtitle, search_terms, summary, tagline, short_description, keywords or description), text (the rewritten value: within the page's limit, in the marketplace's title convention, using only buyer terms from the input - never an invented claim, price, review count or badge; for a list field such as search_terms or keywords, each term on its own line separated by \n, for example "invoice sync\nquickbooks sync\nsales tax report"), terms (the buyer terms from the input that now appear in that text word for word).
  3. gallery - the first screen, at least the first three assets: n (the asset number from the input), caption (one short caption for it), position (1, 2, 3 and on, each once). Never put an asset the facts drop - pricing or a review claim in the image, a duplicate, or logo-only where the platform hard-rejects it - in positions 1 to 3.
  4. video - an object: decision (keep, add or skip), why (one sentence naming the platform's ceiling with its †, or why video is not worth it here).
  5. claims - one object per ranking claim the reply makes: claim (the page's claim id from the provenance table), label (exactly the page's label).
  6. fixes - one object per check the facts mark fail in the keywords, title and gallery areas: check (the page's check id), subject (the facts' subject for that row, or "" when the row has none), change (one concrete sentence).
  7. footnote - exactly the footnote.
  8. caveats - at most 4 short strings: what the vendor must confirm; [] when there is nothing to say.
{
  "task": "rewrite",
  "summary": "one sentence",
  "fields": [ { "field": "name", "text": "Tallywren Invoice Sync", "terms": ["invoice sync"] } ],
  "gallery": [ { "n": 2, "caption": "Every invoice synced to your ledger", "position": 1 } ],
  "video": { "decision": "keep", "why": "one sentence" },
  "claims": [ { "claim": "shp_brand_first", "label": "vendor_official" } ],
  "fixes": [ { "check": "g_claims", "subject": "#1", "change": "one sentence" } ],
  "footnote": "† verify against the marketplace's current listing rules before relying on it - all five platforms revise them on an ongoing basis.",
  "caveats": [ ]
}

The page holds it to its own facts (check() in llreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL with a scheme; no backticks; a summary of at most 40 words; every platform figure with its dagger, and the exact footnote; no consumer app-store number; no implied endorsement by the marketplace; no deleted review tactic recommended; one row per indexed field and no other, each within the page's limit, the name in Shopify's brand-first convention, no number, price or badge the input never gave, and every covered term the input's and present word for word (matched by field plus term, so "sync" is never found in "async"); only the input's asset numbers, positions 1 and on each once, and no dropped asset in the first three (matched by asset number, never by a caption two assets share); a keep, add or skip video call that keeps no video over the ceiling; a fix for every listing or gallery fail matched by check id and subject; every ranking claim by the page's claim id with exactly its label - a label above the page's is flagged as an upgrade and put back, and a ranking sentence whose claim is not listed is flagged.

program

Plan reviews and refresh: the classification, the review triggers in the page's ladder order, the deleted tactics to stop, the sanctioned ask, the refresh cadence, the KPIs, one fix per failing instrumentation, reviews or refresh check, and the provenance of every ranking claim.

  1. summary - one sentence, at most 40 words: the review and refresh program, or why there is no review program on a data-dark marketplace.
  2. classification - exactly the facts' classification: data_rich or data_dark.
  3. triggers - the facts' five triggers in the facts' order: trigger (platform_triggers, post_support, lifecycle_email, milestone_ask or negative_replies), status (exactly the facts' status: use, add or skip), how (one sentence). On a data-dark marketplace every status is skip.
  4. deleted - one object per deleted tactic the facts show in use: tactic (incentives, team_reviews, review_swaps or selective_asks), stop (one sentence on how to stop it). [] when none is in use.
  5. ask - the sanctioned review ask: neutral, unconditional, no incentive, never asking for a star count or only from happy users, each sentence on its own line. Separate the lines with a newline, written \n inside the JSON string - for example "Thanks for using Quillstead.\nIf you have a minute, an honest review on the listing helps other teams decide.\nYou can turn these requests off in settings." Exactly "" on a data-dark marketplace.
  6. cadence - an object: cycle_days (a number from 30 to 60), owner (exactly the owner the input names, else a role: marketplace owner, product marketing, product, support, customer success, engineering or design), next (a YYYY-MM-DD date inside the facts' refresh window, or exactly "set it" when the facts say so).
  7. kpis - the facts' six KPIs in the facts' order: kpi (views_conversion, keyword_coverage, review_velocity, average_rating, uninstall_rate or badge_progress), now (exactly the facts' value, "unknown" included), where (one short phrase: where to read it).
  8. fixes - one object per check the facts mark fail in the instrumentation, reviews and refresh areas: check, subject ("" when the row has none), change (one concrete sentence).
  9. claims - one object per ranking claim the reply makes: claim (the page's claim id), label (exactly the page's label).
  10. footnote - exactly the footnote.
  11. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "program",
  "summary": "one sentence",
  "classification": "data_rich",
  "triggers": [ { "trigger": "platform_triggers", "status": "use", "how": "one sentence" } ],
  "deleted": [ { "tactic": "incentives", "stop": "one sentence" } ],
  "ask": "first sentence\nsecond sentence",
  "cadence": { "cycle_days": 45, "owner": "Marketplace lead", "next": "2026-11-20" },
  "kpis": [ { "kpi": "views_conversion", "now": "4.5% (430 installs from 9,600 views)", "where": "the vendor console" } ],
  "fixes": [ { "check": "i_native", "subject": "", "change": "one sentence" } ],
  "claims": [ { "claim": "gen_velocity", "label": "corroborated" } ],
  "footnote": "† verify against the marketplace's current listing rules before relying on it - all five platforms revise them on an ongoing basis.",
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; the page's classification (put back if moved); the five triggers in the page's ladder order with the page's status (put back), and none but skip on a data-dark marketplace; a stop for every deleted tactic in use and no invented one; a neutral, unconditional ask with each sentence on its own line, or none on a data-dark marketplace; a cadence of 30 to 60 days with the input's owner (or a role) and a next date inside the page's window, or "set it"; the page's six KPIs in order with the page's values (put back); a fix for every instrumentation, reviews or refresh fail; every ranking claim with exactly its page label.

Worked examples

Three runs from the page's free samples (invented products): the rewrite for a Shopify listing that fails a lot, its review and refresh program carrying that rewrite, and the program for a data-dark Slack listing, where the review program is skipped. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the long fields, strings and lists are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

rewrite - Tallywren on Shopify - fails a lot

The page read it as: not yet - fails to fix first on Shopify App Store (data_rich); keyword coverage 33%; fail checks: i_native, k_field (subtitle), k_coverage, k_limit (search_terms), t_convention, g_first3_unique, g_claims (#1), g_duplicate (#3), g_logo_only (#4), g_video, a_deleted (incentives), f_owner.

Input (facts is 10823 characters; the first 1399 are shown):

{
 "task": "rewrite",
 "marketplace": "Shopify App Store (shopify), live; classification data_rich (a real view-to-install funnel)\nFields (id | indexed | claim | limit): name | indexed | shp_name_subtitle (single_source) | none in the references; subtitle | indexed | shp_name_subtitle (single_source) | none in the references; search_terms | indexed | shp_search_terms (vendor_official) | up to five terms†, one concept per term; description | not known to be indexed | - | none in the references\nTitle convention: brand-first (shp_brand_first, vendor_official)\nVideo ceiling: 2-3 minutes†, promotional tone allowed, screencast capped at 25% of runtime†",
 "listing": "name (App name): Invoice Sync & Bookkeeping - Tallywren\nsubtitle (Subtitle): Sync invoices to your books in one click\nsearch_terms (Search terms, one per line): invoices / bookkeeping software for small stores / tax / reports / exports / receipts\ndescription (Long description): Tallywren keeps your store's books current. Payout reconciliation matches every payout to its bank deposit, multi-currency orders convert at the daily rate, and receipts attach to each entry. It works with the tools your accountant already uses.",
 "terms": "- invoice sync | support tickets\n- quickbooks sync | support tickets\n- sales tax report | search-term report\n- bookkeeping | sales calls\n- multi currency | sales calls\n- payout reconciliation | search-term report",
 "gallery": "#1 | screenshot | Plans and pricing compared | shows pricing\n#2 | screenshot | Every invoice synced to your ledger | hero\n#3 | screenshot | Every invoice synced to your ledger | duplicate of #2\n#4 | logo | Tallywren logo | logo only\n#5 | screenshot | Sales tax report by region | most-loved\n#6 | screenshot | Payouts matched to bank deposits | differentiator\nDemo video: 240 seconds",
 "reviews": "Reviews as of 2026-11-02: count 8; average 3.9; newest 2026-07-01 (124 days old); trailing 90 days 0\nNewest ten dates: 2026-07-01, 2026-06-12, 2026-05-20, 2026-04-02, 2026-02-14, 2025-12-01, 2025-10-11, 2025-08-30",
 "context": "Brand: Tallywren; maturity: an unknown entrant (challenger)\nAnalytics on: none\nFunnel: views 18,400; installs 212\nBadge: none; procurement gates on a badge: no\nReview asks in use: lifecycle (a lifecycle email to all active users); incentive (discounts, gift cards or feature unlocks for reviews)\nUninstall trend: rising; owner: none; last change: 2026-05-10\nDeadline: open-ended; mandate: a compounding asset; effort ceiling: medium\nUnfair advantages: none",
 "carry": "",
 "facts": "The page's checks (12 fail, 12 warn, 4 pass, 9 info, 1 n/a); verdict: not yet - fails to fix first; area | id | subject | status | evidence [claim, label]:\n- instrument | i_class | - | info | data-rich: a real view-to-install funnel\n- instrument | i_native | - | fail | off - turn on the partner dashboard: installs, uninstalls, net installs, ratings and revenue before touching content; an optimization you cannot measure is a guess\n- instrument | i_ledger | - | na | not needed while native dashboards exist\n- instrument | i_external | - | info | off - GA4 Measurement ID and Meta Pixel wiring (Distribution, Manage listing, Tracking), which passes server-side install events; it gives the full listing-view, install-click, install funnel - wire it once weekly decisions depend on funnel data\n- instrument | i_funnel | - | info | 212 installs from 18,400 listing views (1.2% view-to-install)\n- keywords | k_terms | - | info | 6 buyer terms from support tickets, search-term report, sales calls\n- keywords | k_field | name | pass | App name carries invoice sync, bookkeeping [shp_name_subtitle, single_source]\n- keywords | k_field | subtitle | fail | Subtitle carries none of the buyer terms [shp_name_subtitle, single_source]\n- keywords | k_field | search_terms | pass | Search terms, one per line carries bookkeeping [shp_search_terms, vendor_official]\n- keywords | k_coverage | - | fail | 2 of 6\n\n[... 9424 more characters cut from this display ...]"
}

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

{
 "task": "rewrite",
 "summary": "A brand-first name, buyer terms in the subtitle and one-concept search terms, then three unique in-UI screens first; the indexed fields now cover five of the six buyer terms.",
 "fields": [
  {
   "field": "name",
   "text": "Tallywren Invoice Sync",
   "terms": [
    "invoice sync"
   ]
  },
  {
   "field": "subtitle",
   "text": "QuickBooks sync, sales tax report and payout reconciliation for your store",
   "terms": [
    "quickbooks sync",
    "sales tax report",
    "payout reconciliation"
   ]
  },
  {
   "field": "search_terms",
   "text": "invoice sync\nquickbooks sync\nsales tax report\nmulti-currency\nbookkeeping",
   "terms": [
    "invoice sync",
    "quickbooks sync",
    "sales tax report",
    "multi-currency",
    "bookkeeping"
   ]
  }
 ],
 "gallery": [
  {
   "n": 2,
   "caption": "Every invoice synced to your ledger",
   "position": 1
  },
  {
   "n": 6,
   "caption": "Payouts matched to bank deposits",
   "position": 2
  },
  {
   "n": 5,
   "caption": "Sales tax report by region",
   "position": 3
  }
 ],
 "video": {
  "decision": "add",
  "why": "Replace the current cut with a shorter one inside Shopify's 2-3 minutes† ceiling that shows invoices syncing, not marketing."
 },
 "claims": [
  {
   "claim": "shp_brand_first",
   "label": "vendor_official"
  },
  {
   "claim": "shp_search_terms",
   "label": "vendor_official"
  },
  {
   "claim": "shp_name_subtitle",
   "label": "single_source"
  },
  {
   "claim": "ana_order",
   "label": "analogy"
  }
 ],
 "fixes": [
  {
   "check": "k_field",
   "subject": "subtitle",
   "change": "Put quickbooks sync, sales tax report and payout reconciliation in the subtitle, which carries none of the buyer terms today."
  },
  {
   "check": "k_coverage",
   "subject": "",
   "change": "Carry five of the six buyer terms in the indexed fields instead of two, so coverage clears the page's own threshold."
  },
  {
   "check": "k_limit",
   "subject": "search_terms",
   "change": "Keep one concept per line and drop tax, reports, exports and receipts so the field holds no more than Shopify allows."
  },
  {
   "check": "t_convention",
   "subject": "",
   "change": "Start the name with Tallywren and follow it with invoice sync, as Shopify's brand-first convention instructs."
  },
  {
   "check": "g_first3_unique",
   "subject": "",
   "change": "Lead with #2, #6 and #5 so the first three assets are three different screens."
  }
 ],
 "footnote": "† verify against the marketplace's current listing rules before relying on it - all five platforms revise them on an ongoing basis.",
 "caveats": [
  "The name and subtitle carrying about 80%† of keyword ranking weight is one tracker's estimate, so expect direction, not magnitude.",
  "Turn on the partner dashboard before shipping this, or the rewrite cannot be measured."
 ]
}

Cut from this display only (the saved reply and the check use the full text): fixes (9 items, 5 shown).

What step 5 prints for it: k_field subtitle | k_coverage | k_limit search_terms | t_convention | g_first3_unique | g_claims #1 | g_duplicate #3 | g_logo_only #4 | g_video

The page's check: kept - nothing to flag

program - Tallywren on Shopify - fails a lot

The input carries the saved rewrite (R.laneForm fills carry from the rewrite reply), as the page's handoff does. Shopify App Store, data_rich; the triggers: platform_triggers add, post_support add, milestone_ask add, lifecycle_email use, negative_replies add; gates: g_instrument fail, g_keywords fail, g_gallery_asks fail, g_provenance pass, g_refresh fail.

Input (facts is 10986 characters; the first 1399 are shown):

{
 "task": "program",
 "marketplace": "Shopify App Store (shopify), live; classification data_rich (a real view-to-install funnel)\nFields (id | indexed | claim | limit): name | indexed | shp_name_subtitle (single_source) | none in the references; subtitle | indexed | shp_name_subtitle (single_source) | none in the references; search_terms | indexed | shp_search_terms (vendor_official) | up to five terms†, one concept per term; description | not known to be indexed | - | none in the references\nTitle convention: brand-first (shp_brand_first, vendor_official)\nVideo ceiling: 2-3 minutes†, promotional tone allowed, screencast capped at 25% of runtime†",
 "listing": "name (App name): Invoice Sync & Bookkeeping - Tallywren\nsubtitle (Subtitle): Sync invoices to your books in one click\nsearch_terms (Search terms, one per line): invoices / bookkeeping software for small stores / tax / reports / exports / receipts\ndescription (Long description): Tallywren keeps your store's books current. Payout reconciliation matches every payout to its bank deposit, multi-currency orders convert at the daily rate, and receipts attach to each entry. It works with the tools your accountant already uses.",
 "terms": "- invoice sync | support tickets\n- quickbooks sync | support tickets\n- sales tax report | search-term report\n- bookkeeping | sales calls\n- multi currency | sales calls\n- payout reconciliation | search-term report",
 "gallery": "#1 | screenshot | Plans and pricing compared | shows pricing\n#2 | screenshot | Every invoice synced to your ledger | hero\n#3 | screenshot | Every invoice synced to your ledger | duplicate of #2\n#4 | logo | Tallywren logo | logo only\n#5 | screenshot | Sales tax report by region | most-loved\n#6 | screenshot | Payouts matched to bank deposits | differentiator\nDemo video: 240 seconds",
 "reviews": "Reviews as of 2026-11-02: count 8; average 3.9; newest 2026-07-01 (124 days old); trailing 90 days 0\nNewest ten dates: 2026-07-01, 2026-06-12, 2026-05-20, 2026-04-02, 2026-02-14, 2025-12-01, 2025-10-11, 2025-08-30",
 "context": "Brand: Tallywren; maturity: an unknown entrant (challenger)\nAnalytics on: none\nFunnel: views 18,400; installs 212\nBadge: none; procurement gates on a badge: no\nReview asks in use: lifecycle (a lifecycle email to all active users); incentive (discounts, gift cards or feature unlocks for reviews)\nUninstall trend: rising; owner: none; last change: 2026-05-10\nDeadline: open-ended; mandate: a compounding asset; effort ceiling: medium\nUnfair advantages: none",
 "carry": "The rewrite's indexed fields (field: text):\n- name: Tallywren Invoice Sync\n- subtitle: QuickBooks sync, sales tax report and payout reconciliation for your store\n- search_terms: invoice sync / quickbooks sync / sales tax report / multi-currency / bookkeeping\nThe rewrite's first screen: #2, #6, #5\nThe rewrite's claims: shp_brand_first (vendor_official), shp_search_terms (vendor_official), shp_name_subtitle (single_source), ana_order (analogy)",
 "facts": "The page's checks (12 fail, 12 warn, 4 pass, 9 info, 1 n/a); verdict: not yet - fails to fix first; area | id | subject | status | evidence [claim, label]:\n- instrument | i_class | - | info | data-rich: a real view-to-install funnel\n- instrument | i_native | - | fail | off - turn on the partner dashboard: installs, uninstalls, net installs, ratings and revenue before touching content; an optimization you cannot measure is a guess\n- instrument | i_ledger | - | na | not needed while native dashboards exist\n- instrument | i_external | - | info | off - GA4 Measurement ID and Meta Pixel wiring (Distribution, Manage listing, Tracking), which passes server-side install events; it gives the full listing-view, install-click, install funnel - wire it once weekly decisions depend on funnel data\n- instrument | i_funnel | - | info | 212 installs from 18,400 listing views (1.2% view-to-install)\n- keywords | k_terms | - | info | 6 buyer terms from support tickets, search-term report, sales calls\n- keywords | k_field | name | pass | App name carries invoice sync, bookkeeping [shp_name_subtitle, single_source]\n- keywords | k_field | subtitle | fail | Subtitle carries none of the buyer terms [shp_name_subtitle, single_source]\n- keywords | k_field | search_terms | pass | Search terms, one per line carries bookkeeping [shp_search_terms, vendor_official]\n- keywords | k_coverage | - | fail | 2 of 6\n\n[... 9587 more characters cut from this display ...]"
}

Reply:

{
 "task": "program",
 "summary": "Turn on the partner dashboard, stop the discount for reviews, add the Reviews API and post-support asks, and refresh the listing every 45 days under a named marketplace owner.",
 "classification": "data_rich",
 "triggers": [
  {
   "trigger": "platform_triggers",
   "status": "add",
   "how": "Route in-product review prompts through Shopify's Reviews API at a natural moment that never blocks the merchant's work."
  },
  {
   "trigger": "post_support",
   "status": "add",
   "how": "Ask after each positive support interaction, at a natural pause, with an opt-out from future asks."
  },
  {
   "trigger": "milestone_ask",
   "status": "add",
   "how": "A named person asks a specific merchant at a success milestone, such as the first month of reconciled payouts."
  },
  {
   "trigger": "lifecycle_email",
   "status": "use",
   "how": "Keep the lifecycle email unconditional and send it to every qualifying active merchant, with nothing attached."
  },
  {
   "trigger": "negative_replies",
   "status": "add",
   "how": "Reply publicly once to each negative review, aiming to fix the issue and get the review amended."
  }
 ],
 "deleted": [
  {
   "tactic": "incentives",
   "stop": "Stop the discount offered for a review today, in the app and in every email, and never bring it back to rescue a slow month."
  }
 ],
 "ask": "Thanks for keeping your books with Tallywren.\nIf you have a minute, an honest review on the listing helps other merchants decide.\nYou can turn these requests off in settings.",
 "cadence": {
  "cycle_days": 45,
  "owner": "marketplace owner",
  "next": "2026-11-02"
 },
 "kpis": [
  {
   "kpi": "views_conversion",
   "now": "1.2% (212 installs from 18,400 views)",
   "where": "the partner dashboard, once it is on"
  },
  {
   "kpi": "keyword_coverage",
   "now": "33% (2 of 6 terms)",
   "where": "this page, each refresh cycle"
  },
  {
   "kpi": "review_velocity",
   "now": "0 reviews in 90 days",
   "where": "the listing's review history"
  },
  {
   "kpi": "average_rating",
   "now": "3.9 of 5 over 8 reviews",
   "where": "the listing"
  },
  {
   "kpi": "uninstall_rate",
   "now": "rising",
   "where": "the partner dashboard's uninstalls"
  },
  {
   "kpi": "badge_progress",
   "now": "not started",
   "where": "the Built for Shopify criteria"
  }
 ],
 "fixes": [
  {
   "check": "i_native",
   "subject": "",
   "change": "Turn on the partner dashboard now, before the rewrite ships, so installs, uninstalls and ratings show what it changed."
  },
  {
   "check": "f_owner",
   "subject": "",
   "change": "Name one marketplace owner who runs the 45-day refresh and the review program."
  }
 ],
 "claims": [
  {
   "claim": "gen_velocity",
   "label": "corroborated"
  },
  {
   "claim": "shp_uninstall",
   "label": "corroborated"
  },
  {
   "claim": "gen_email",
   "label": "corroborated"
  }
 ],
 "footnote": "† verify against the marketplace's current listing rules before relying on it - all five platforms revise them on an ongoing basis.",
 "caveats": [
  "Uninstalls are rising and uninstall rate feeds ranking negatively, so fix the product gap before pushing for installs.",
  "Review recency and velocity outweigh lifetime totals, so the empty last 90 days is the number to move first."
 ]
}

What step 5 prints for it: platform_triggers add | post_support add | milestone_ask add | lifecycle_email use | negative_replies add

The page's check: kept - nothing to flag

program - Murmeldesk on Slack - data-dark, pre-submission

The input carries the saved rewrite (R.laneForm fills carry from the rewrite reply), as the page's handoff does. Slack Marketplace, data_dark; the triggers: platform_triggers skip, post_support skip, lifecycle_email skip, milestone_ask skip, negative_replies skip; gates: g_instrument fail, g_keywords fail, g_gallery_asks fail, g_provenance pass, g_refresh pass.

Input (facts is 7459 characters; the first 1394 are shown):

{
 "task": "program",
 "marketplace": "Slack Marketplace (slack), pre-submission; classification data_dark (no analytics, no review system)\nFields (id | indexed | claim | limit): name | indexed | ana_fields (analogy) | none in the references; short_description | indexed | ana_fields (analogy) | none in the references; description | not known to be indexed | - | none in the references\nTitle convention: none documented in the references\nVideo ceiling: 30-90 seconds†",
 "listing": "name (App name): Murmeldesk\nshort_description (Short description): Run your team's daily standup without a meeting.\ndescription (Long description): Async standup threads, team check-in reminders and a weekly digest for managers. Answers stay in one thread per day.",
 "terms": "- async standup | support tickets\n- daily standup | sales calls\n- team check in | sales calls\n- standup bot | sales calls",
 "gallery": "#1 | screenshot | One standup thread with every answer | hero\n#2 | screenshot | Reminder schedule per team | -\n#3 | logo | Murmeldesk logo | -\nDemo video: none",
 "reviews": "No review system on Slack Marketplace.",
 "context": "Brand: Murmeldesk; maturity: an unknown entrant (challenger)\nAnalytics on: none\nFunnel: views not given; installs not given\nBadge: none; procurement gates on a badge: no\nReview asks in use: lifecycle (a lifecycle email to all active users); selective (asking only happy users)\nUninstall trend: unknown; owner: Growth PM; last change: not given\nDeadline: a hard date (2026-12-01); mandate: a one-off win; effort ceiling: low\nUnfair advantages: none",
 "carry": "The rewrite's indexed fields (field: text):\n- name: Murmeldesk - async standup and team check-in\n- short_description: A standup bot for your daily standup: async standup threads and team check-in reminders in one place.\nThe rewrite's first screen: #1, #2, #3\nThe rewrite's claims: ana_fields (analogy), ana_order (analogy)",
 "facts": "The page's checks (5 fail, 5 warn, 4 pass, 7 info, 6 n/a); verdict: not yet - fails to fix first; area | id | subject | status | evidence [claim, label]:\n- instrument | i_class | - | info | data-dark: no listing analytics and no review system - write the best listing once, drive traffic from outside, self-track installs, skip the review program\n- instrument | i_native | - | na | the platform exposes no listing analytics to publishers\n- instrument | i_ledger | - | fail | no ledger - on a data-dark marketplace it is the only instrumentation that exists: your auth-token store as the install record plus the app-uninstalled event\n- instrument | i_funnel | - | na | no funnel exists to read\n- keywords | k_terms | - | info | 4 buyer terms from support tickets, sales calls\n- keywords | k_field | name | fail | App name carries none of the buyer terms [ana_fields, analogy]\n- keywords | k_field | short_description | pass | Short description carries daily standup [ana_fields, analogy]\n- keywords | k_coverage | - | fail | 1 of 4 buyer terms appear in an indexed field (25%; the page's own threshold is four terms in five) - coverage, not rank: where the listing ranks for them is a separate KPI\n- keywords | k_term | async standup | warn | only in long description, which this marketplace is not known to index\n- keywords | k_term | team check in | warn | only in long description, which this\n\n[... 6065 more characters cut from this display ...]"
}

Reply:

{
 "task": "program",
 "summary": "Slack is data-dark: skip the review program, build the install ledger from the token store and the uninstall event, stop the happy-users-only email, and refresh after launch.",
 "classification": "data_dark",
 "triggers": [
  {
   "trigger": "platform_triggers",
   "status": "skip",
   "how": "Slack has no review system, so there is no platform trigger to turn on."
  },
  {
   "trigger": "post_support",
   "status": "skip",
   "how": "With no reviews on Slack, a post-support ask has nothing to ask for."
  },
  {
   "trigger": "lifecycle_email",
   "status": "skip",
   "how": "Use lifecycle email for activation, not for review asks, on a marketplace without reviews."
  },
  {
   "trigger": "milestone_ask",
   "status": "skip",
   "how": "Keep milestone conversations for product feedback; there is no listing review to collect."
  },
  {
   "trigger": "negative_replies",
   "status": "skip",
   "how": "There are no public reviews on Slack to reply to."
  }
 ],
 "deleted": [
  {
   "tactic": "selective_asks",
   "stop": "Stop the email that goes only to happy users and send product messages to every qualifying user instead."
  }
 ],
 "ask": "",
 "cadence": {
  "cycle_days": 30,
  "owner": "Growth PM",
  "next": "set it"
 },
 "kpis": [
  {
   "kpi": "views_conversion",
   "now": "unknown",
   "where": "the self-built install ledger"
  },
  {
   "kpi": "keyword_coverage",
   "now": "25% (1 of 4 terms)",
   "where": "this page, each refresh cycle"
  },
  {
   "kpi": "review_velocity",
   "now": "unknown",
   "where": "none - no review system"
  },
  {
   "kpi": "average_rating",
   "now": "unknown",
   "where": "none - no review system"
  },
  {
   "kpi": "uninstall_rate",
   "now": "unknown",
   "where": "the app-uninstalled event"
  },
  {
   "kpi": "badge_progress",
   "now": "unknown",
   "where": "none - no badge named for Slack"
  }
 ],
 "fixes": [
  {
   "check": "i_ledger",
   "subject": "",
   "change": "Record every install from your auth-token store and subscribe to the app-uninstalled event before submission."
  }
 ],
 "claims": [],
 "footnote": "† verify against the marketplace's current listing rules before relying on it - all five platforms revise them on an ongoing basis.",
 "caveats": [
  "Set the first refresh date once the listing is live.",
  "Slack watches usage and uninstalls itself and contacts you, so keep the ledger honest."
 ]
}

What step 5 prints for it: platform_triggers skip | post_support skip | lifecycle_email skip | milestone_ask skip | negative_replies skip

The page's check: kept - nothing to flag

Build the input yourself

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

const K = LlKit, R = LlReply;
const f = K.form(Object.assign(K.blank(), { market: "hubspot", brand: "Contaquill", name: "Contaquill - CRM sync", tagline: "Two-way contact sync",
  terms: "crm sync | support tickets\nsync | sales calls\ncontact dedupe | sales calls", gallery: "#1 | screenshot | Contacts synced | hero" }));
// f.terms (normalized buyer terms), f.matrix[term][field] (whole-word counts), f.coverage, f.gallery, f.rev (count, average,
// newest, trailing 90 days), f.rows (the checks, each with a claim id and its provenance label), f.instr, f.opt and f.trig
// (the ladders), f.badgeV, f.gates, f.kpis, f.cadence, f.contradictions (always empty for a settled board)
// K.coverageCsv(f), K.checksCsv(f), K.galleryCsv(f), K.reviewsMd(f), K.board(f), K.applyPlan(f) - the exports
const b = K.buildInput(f, "rewrite");                        // or "program" (R.laneForm(form, "program", rewriteReply) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "rewrite");
// R.check(reply, b)                                         // {status: kept or flagged, notes, fixed}

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

Step 1 — a tiny client helper

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

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://listing-lift.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-lift"}' | 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": "rewrite", "marketplace": "Shopify App Store (shopify), live; classification data_rich ...", "listing": "name (App name): Invoice Sync & Bookkeeping - Tallywren ...", "terms": "- invoice sync | support tickets ...", "gallery": "#1 | screenshot | Plans and pricing compared | shows pricing ...", "reviews": "Reviews as of 2026-11-02: count 8 ...", "context": "Brand: Tallywren; maturity: an unknown entrant ...", "carry": "", "facts": "The page's checks (12 fail, ...); area | id | subject | status | evidence [claim, label]: ..."}
# - 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-lift:<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 rewrite reply carries summary, fields, gallery, video, claims, fixes, footnote and caveats; a program reply carries summary, classification, triggers, deleted, ask, cadence, kpis, fixes, claims, footnote and caveats. The code below prints the gist of either: each fix of the rewrite with its check, or each trigger with its status.

KEY="listing-lift:$(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 == "program" then (.triggers[] | "\(.trigger) \(.status) -> \(.how)") 
       else (.fixes[] | "\(.check) \(.subject) \(.change)") 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-lift:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json