@50heads/mcp 2026.925.1 → 2026.925.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ Add what changed under Unreleased as you go, written for the people using it. Th
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [2026.925.2] - 2026-09-25
12
+
13
+ Maintenance release. No change to tools, resources or prompts.
14
+
11
15
  ## [2026.925.1] - 2026-09-25
12
16
 
13
17
  ### Added
package/dist/cli.js CHANGED
@@ -5150,7 +5150,8 @@ function pricingTable(currency, rates = FALLBACK_RATES) {
5150
5150
  note: "Age, gender and each tag group are one trait each, at any tier. Countries are free. PickFu charges about $0.40 per trait."
5151
5151
  },
5152
5152
  heads: { min: HEADS_LIMITS.web.min, max: HEADS_LIMITS.web.max, default: 50 },
5153
- connectionDailyCapDefault: price(DEFAULT_CONNECTION_CAP_PENCE, currency, rates),
5153
+ /** Caps are in credits, so they never move with exchange rates. */
5154
+ connectionDailyCapDefaultCredits: DEFAULT_CONNECTION_CAP_PENCE,
5154
5155
  formula: "per answer = tier base \xD7 length \xD7 format (\xD7 five-second) + images + traits (\xD7 rush); total = per answer \xD7 heads"
5155
5156
  };
5156
5157
  }
@@ -5181,11 +5182,14 @@ body{font-family:'DM Sans',system-ui,-apple-system,'Segoe UI',sans-serif;font-we
5181
5182
  .pill{display:inline-flex;align-items:center;height:44px;padding:0 20px;border-radius:${radius.pill}px;border:0;font:inherit;font-weight:700;cursor:pointer}
5182
5183
  .ask{background:var(--tomato);color:var(--ink)}
5183
5184
  .ask:hover{background:var(--tomato-deep)}
5185
+ .ask:disabled{cursor:default;opacity:.5}
5184
5186
  .pill:focus-visible{outline:3px solid var(--tomato);outline-offset:2px}
5185
5187
  .issues{margin:12px 0 0;padding-left:18px}
5186
5188
  .issues li{margin:4px 0}
5187
5189
  .foot{margin-top:16px;display:flex;flex-wrap:wrap;gap:12px;align-items:center}
5188
- .lede{font-size:22px;line-height:1.35;margin:8px 0 4px}
5190
+ .lede{font-size:22px;line-height:1.3;font-weight:700;margin:8px 0 6px}
5191
+ .facts{min-height:1.4em}
5192
+ .quote .foot{margin-top:16px;padding-top:16px;border-top:1px solid var(--track)}
5189
5193
  .note{font-size:17px;line-height:1.45;margin:4px 0}
5190
5194
  .why{margin:16px 0 0;padding:16px 0 0;border-top:1px solid var(--track)}
5191
5195
  .themes{list-style:none;padding:0;margin:8px 0 0;display:grid;gap:10px}
@@ -5202,14 +5206,15 @@ window.addEventListener("message",function(e){var m=e.data;if(!m||m.jsonrpc!=="2
5202
5206
  if(m.id!=null&&pending[m.id]){var r=pending[m.id];delete pending[m.id];r(m.result||null);return}
5203
5207
  if(m.method==="ui/notifications/tool-result"){render(data(m.params))}
5204
5208
  if(m.method==="ui/notifications/host-context-changed"&&m.params){theme(m.params.theme)}});
5205
- function size(){note("ui/notifications/size-changed",{width:document.documentElement.scrollWidth,height:document.documentElement.scrollHeight})}
5209
+ function size(){var h=Math.ceil(document.body.getBoundingClientRect().height);note("ui/notifications/size-changed",{width:document.documentElement.scrollWidth,height:h});
5210
+ if(window.openai&&window.openai.notifyIntrinsicHeight)window.openai.notifyIntrinsicHeight(h)}
5206
5211
  function say(text){if(window.openai&&window.openai.sendFollowUpMessage){window.openai.sendFollowUpMessage({prompt:text});return}
5207
5212
  rpc("ui/message",{role:"user",content:[{type:"text",text:text}]})}
5208
5213
  function start(fn){render=function(d){if(d){fn(d);size()}};
5209
5214
  rpc("ui/initialize",{protocolVersion:"2026-01-26",appInfo:{name:"50heads",version:"1"},appCapabilities:{}}).then(function(r){if(r&&r.hostContext)theme(r.hostContext.theme);note("ui/notifications/initialized")});
5210
5215
  if(window.openai&&window.openai.toolOutput)render(window.openai.toolOutput);
5211
5216
  window.addEventListener("openai:set_globals",function(){if(window.openai&&window.openai.toolOutput)render(window.openai.toolOutput)});
5212
- if(window.ResizeObserver)new ResizeObserver(size).observe(document.body)}
5217
+ if(window.ResizeObserver)new ResizeObserver(size).observe(document.body);size()}
5213
5218
  function el(tag,cls,text){var n=document.createElement(tag);if(cls)n.className=cls;if(text!=null)n.textContent=text;return n}
5214
5219
  function money(p){try{return new Intl.NumberFormat(${JSON.stringify(MONEY_LOCALES)}[p.currency]||"en-US",{style:"currency",currency:p.currency,currencyDisplay:"narrowSymbol"}).format(p.amount)}catch(e){return p.amount+" "+p.currency}}
5215
5220
  function mins(m){return m<90?"about "+m+" minute"+(m===1?"":"s"):"about "+Math.round(m/60)+" hours"}
@@ -5238,13 +5243,13 @@ if(s.suggested_follow_up){btn.hidden=false;btn.onclick=function(){say("Ask the h
5238
5243
  var quoteScript = `
5239
5244
  ${bridge}
5240
5245
  start(function(d){
5241
- var line=document.getElementById("line"),bd=document.getElementById("bd"),issues=document.getElementById("issues"),pool=document.getElementById("pool"),btn=document.getElementById("go");
5242
- var n=d.credits_per_answer?Math.round(d.credits_total/d.credits_per_answer):0;
5246
+ var line=document.getElementById("line"),facts=document.getElementById("facts"),issues=document.getElementById("issues"),btn=document.getElementById("go");
5247
+ var n=d.credits_per_answer?Math.round(d.credits_total/d.credits_per_answer):0,bad=(d.validation||[]).length>0;
5243
5248
  line.textContent=n+" answers, "+mins(d.eta_minutes)+", "+money(d.price)+".";
5244
- bd.textContent=(d.breakdown||[]).map(function(b){return b.value+" "+b.label}).join(" \xB7 ");
5245
- issues.textContent="";(d.validation||[]).forEach(function(v){issues.appendChild(el("li",null,v.message))});issues.hidden=!(d.validation||[]).length;
5246
- pool.textContent=d.pool_size!=null?d.pool_size.toLocaleString("en-GB")+" heads could answer":"";
5247
- btn.textContent="Ask "+n+" heads";btn.hidden=(d.validation||[]).length>0;btn.onclick=function(){say("Ask it: "+line.textContent)}});
5249
+ var f=[d.credits_per_answer+" credits a head",d.credits_total.toLocaleString("en-GB")+" credits in all"];
5250
+ if(d.pool_size!=null)f.push(d.pool_size.toLocaleString("en-GB")+" heads could answer");facts.textContent=f.join(" \xB7 ");
5251
+ issues.textContent="";(d.validation||[]).forEach(function(v){issues.appendChild(el("li",null,v.message))});issues.hidden=!bad;
5252
+ btn.textContent="Ask "+n+" heads";btn.hidden=bad;btn.disabled=false;btn.onclick=function(){say("Ask it: "+line.textContent)}});
5248
5253
  `;
5249
5254
  function page(title, body, script) {
5250
5255
  return `<!doctype html><html lang="en-GB"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title><style>${css}</style></head><body>${body}<script>${script}</script></body></html>`;
@@ -5263,10 +5268,10 @@ function resultsViewHtml() {
5263
5268
  function quoteViewHtml() {
5264
5269
  return page(
5265
5270
  "50heads estimate",
5266
- `<main aria-live="polite"><p class="label">50heads \xB7 Estimate</p>
5267
- <h1 class="lede" id="line">Working out the price.</h1><p class="cap" id="bd"></p>
5268
- <ul class="issues" id="issues" hidden></ul><p class="cap" id="pool"></p>
5269
- <div class="foot"><button class="pill ask" id="go" hidden>Ask 50 heads</button><span class="cap">Nothing is spent until you ask.</span></div></main>`,
5271
+ `<main class="quote" aria-live="polite"><p class="label">50heads \xB7 Estimate</p>
5272
+ <h1 class="lede" id="line">Working out the price.</h1><p class="cap facts" id="facts"></p>
5273
+ <ul class="issues" id="issues" hidden></ul>
5274
+ <div class="foot"><button class="pill ask" id="go" disabled>Ask 50 heads</button><span class="cap">Nothing is spent until you ask.</span></div></main>`,
5270
5275
  quoteScript
5271
5276
  );
5272
5277
  }
@@ -5277,6 +5282,12 @@ function uiMeta(uri) {
5277
5282
  "openai/outputTemplate": uri
5278
5283
  };
5279
5284
  }
5285
+ var VIEW_RESOURCE_META = {
5286
+ ui: { csp: { connectDomains: [], resourceDomains: [] }, prefersBorder: true },
5287
+ "openai/widgetCSP": { connect_domains: [], resource_domains: [] },
5288
+ "openai/widgetDomain": "https://mcp.50heads.com",
5289
+ "openai/widgetPrefersBorder": true
5290
+ };
5280
5291
 
5281
5292
  // src/draft.ts
5282
5293
  var toOption = (o) => ({
@@ -5355,12 +5366,12 @@ function fromDraft(d) {
5355
5366
  }
5356
5367
 
5357
5368
  // src/generated/skill.ts
5358
- var SKILL_VERSION = "2026.925.1";
5369
+ var SKILL_VERSION = "2026.925.2";
5359
5370
  var SKILL_BODY = '# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n"Which menu would you order from?" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: "Menu B wins by 36 points. Confidence high." Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions ("Is it clear and friendly?").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n';
5360
5371
  var SKILL_FILES = {
5361
5372
  "LICENSE": 'MIT License\n\nCopyright (c) 2026 50heads\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n',
5362
- "SKILL.md": '---\nname: 50heads\ndescription: Ask fifty verified real people a five-second question and get a distribution back; use when the work in hand needs a human preference, first impression or sanity check that a model cannot supply.\nlicense: MIT\nmetadata:\n version: "2026.925.1"\n homepage: https://50heads.com/agents\nallowed-tools: "50heads:*"\n---\n\n# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n"Which menu would you order from?" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: "Menu B wins by 36 points. Confidence high." Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions ("Is it clear and friendly?").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n',
5363
- "references/pricing.md": "# Pricing\n\nPrices are in credits. `estimate`, `balance` and 50heads://pricing show them in your account currency at the day's rate. You pay per accepted answer; unanswered heads are refunded.\n\nPer answer = tier base \xD7 length \xD7 format, plus images and targeting traits, times 1.5 for rush. Total = per answer \xD7 heads.\n\n| Tier | Heads | Credits an answer | Credits for 50 answers |\n| --- | --- | --- | --- |\n| 1 | phone-verified | 20 | 1000 |\n| 2 | ID-verified | 42 | 2100 |\n| 3 | trusted panel | 80 | 4000 |\n\nLength (question, context and options together): up to 150 characters \xD7 1.0, up to 300 \xD7 1.1, up to 600 \xD7 1.25.\n\nFormat: `single_choice` \xD7 1.0, `multi_choice` \xD7 1.1, `ab_image` \xD7 1.0, `pairwise` \xD7 1.1, `scale_1_5` \xD7 1.0, `ranking` \xD7 1.3, `yes_mostly_no` \xD7 1.0, `free_text` \xD7 2.0, `click_test` \xD7 1.5, `reaction` \xD7 1.0.\n\nA reason with every answer (`reason`, not for free_text) adds to the format: optional + 0.3, required + 0.6. A required reason on a Tier 1 single choice is 20 \xD7 1.6 = 32 credits an answer. Heads get the same share of the higher price.\n\nImages: 7 credits an answer for each image (audio counts the same). Rush: \xD7 1.5. Five-second test: \xD7 1.3.\n\nTargeting: countries are free. Age, gender and each tag group are one trait each, 3 credits an answer per trait, up to 4, at any tier (PickFu charges about $0.40 per trait). See 50heads://targeting.\n\nHeads: 10 to 5000, default 50.\n\nEvery connection has a daily spend cap (default 5000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.\n\nAlways call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.\n",
5373
+ "SKILL.md": '---\nname: 50heads\ndescription: Ask fifty verified real people a five-second question and get a distribution back; use when the work in hand needs a human preference, first impression or sanity check that a model cannot supply.\nlicense: MIT\nmetadata:\n version: "2026.925.2"\n homepage: https://50heads.com/agents\nallowed-tools: "50heads:*"\n---\n\n# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n"Which menu would you order from?" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: "Menu B wins by 36 points. Confidence high." Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions ("Is it clear and friendly?").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n',
5374
+ "references/pricing.md": "# Pricing\n\nPrices are in credits. `estimate`, `balance` and 50heads://pricing show them in your account currency at the day's rate. You pay per accepted answer; unanswered heads are refunded.\n\nPer answer = tier base \xD7 length \xD7 format, plus images and targeting traits, times 1.5 for rush. Total = per answer \xD7 heads.\n\n| Tier | Heads | Credits an answer | Credits for 50 answers |\n| --- | --- | --- | --- |\n| 1 | phone-verified | 20 | 1000 |\n| 2 | ID-verified | 42 | 2100 |\n| 3 | trusted panel | 80 | 4000 |\n\nLength (question, context and options together): up to 150 characters \xD7 1.0, up to 300 \xD7 1.1, up to 600 \xD7 1.25.\n\nFormat: `single_choice` \xD7 1.0, `multi_choice` \xD7 1.1, `ab_image` \xD7 1.0, `pairwise` \xD7 1.1, `scale_1_5` \xD7 1.0, `ranking` \xD7 1.3, `yes_mostly_no` \xD7 1.0, `free_text` \xD7 2.0, `click_test` \xD7 1.5, `reaction` \xD7 1.0.\n\nA reason with every answer (`reason`, not for free_text) adds to the format: optional + 0.3, required + 0.6. A required reason on a Tier 1 single choice is 20 \xD7 1.6 = 32 credits an answer. Heads get the same share of the higher price.\n\nImages: 7 credits an answer for each image (audio counts the same). Rush: \xD7 1.5. Five-second test: \xD7 1.3.\n\nTargeting: countries are free. Age, gender and each tag group are one trait each, 3 credits an answer per trait, up to 4, at any tier (PickFu charges about $0.40 per trait). See 50heads://targeting.\n\nHeads: 10 to 5000, default 50.\n\nEvery connection has a daily spend cap (default 5,000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.\n\nAlways call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.\n",
5364
5375
  "references/question-types.md": '# Question types\n\nEvery question has `type`, `text` (8 to 600 characters), `language` (such as `en` or `pt-BR`), and optionally `context` (one line, 120 characters), `stimulus` (an image or a line of text shown above the question), `n` (heads, default 50), `tier` (1 to 3, default 1), `rush` and `targeting`.\n\nThe format multiplier is part of the price: per answer = tier base \xD7 length \xD7 format, plus 5 credits an image.\n\nFive-second test: add `exposure_ms` (usually 5000) to an image stimulus on single_choice, multi_choice, yes_mostly_no, scale_1_5, reaction or free_text. The app shows the image for that long, hides it, then asks, so the answer is a first impression or what people remember. \xD7 1.3.\nEvery type except `free_text` can ask for a short written reason with each answer: `reason: "optional"` (format + 0.3) or `"required"` (format + 0.6), 10 to 140 characters. Reasons come back on each answer, translated into your language when heads wrote in another, and `get_results` summarises them (themes with counts and quotes, a sentiment split and a takeaway). Use it instead of a follow-up when you want the which and the why from the same heads.\n\n## Single choice (`single_choice`)\n\n- Use: Pick one of several: names, headlines, taglines, colours.\n- Options: 2 to 8 options, 40 characters each; images optional. Two to four reads best.\n- Answer: One option per head; the distribution counts each option.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "single_choice",\n "text": "Which name sounds most like a bakery?",\n "language": "en",\n "options": [\n {\n "label": "Crumb & Co"\n },\n {\n "label": "Loafers"\n },\n {\n "label": "Proof"\n }\n ],\n "n": 50\n}\n```\n\n## Multiple choice (`multi_choice`)\n\n- Use: Tick all that apply: which features matter, which words fit.\n- Options: 2 to 8 options, 40 characters each. Shares add up to more than 100%.\n- Answer: Any number of options per head; each option\'s share is of all heads.\n- Tier: any. Format \xD7 1.1.\n\n```json\n{\n "type": "multi_choice",\n "text": "Which of these would make you try a new coffee shop?",\n "language": "en",\n "options": [\n {\n "label": "Oat milk at no extra cost"\n },\n {\n "label": "Quiet seating"\n },\n {\n "label": "Loyalty card"\n }\n ],\n "n": 50\n}\n```\n\n## A or B image (`ab_image`)\n\n- Use: Two images side by side: menus, logos, screenshots, packaging.\n- Options: Exactly 2 options, each with an image_url (https, or a pre-signed upload). Labels optional.\n- Answer: One image per head.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "ab_image",\n "text": "Which menu would you order from?",\n "language": "en",\n "options": [\n {\n "label": "Menu A",\n "image_url": "https://example.com/menu-a.png"\n },\n {\n "label": "Menu B",\n "image_url": "https://example.com/menu-b.png"\n }\n ],\n "n": 50\n}\n```\n\n## Pairwise (`pairwise`)\n\n- Use: Compare options two at a time when there are too many to show at once.\n- Options: 2 to 8 options; each head sees one pair, balanced so every pair is seen about equally.\n- Answer: One pick per pair. With 3 or more options each row has count (wins), share (win share), appearances, a Bradley\u2013Terry strength and a rank.\n- Tier: any. Format \xD7 1.1.\n\n```json\n{\n "type": "pairwise",\n "text": "Which subject line would you open first?",\n "language": "en",\n "options": [\n {\n "label": "Your order is on its way"\n },\n {\n "label": "Good news: it has shipped"\n },\n {\n "label": "Tracking number inside"\n }\n ],\n "n": 100\n}\n```\n\n## Scale 1 to 5 (`scale_1_5`)\n\n- Use: How strongly: clarity, appeal, trust. Report the mean and the split.\n- Options: Fixed: 1, 2, 3, 4, 5. Do not send options.\n- Answer: A number from 1 to 5; results include the mean.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "scale_1_5",\n "text": "How clear is this sentence? 1 is not clear, 5 is very clear.",\n "context": "Your parcel will be left in a safe place if you are out.",\n "language": "en",\n "n": 50\n}\n```\n\n## Ranking (`ranking`)\n\n- Use: Put options in order of preference.\n- Options: 2 to 8 options; keep to five or fewer for a five-second answer.\n- Answer: A full order per head; the distribution carries each option\'s average rank.\n- Tier: any. Format \xD7 1.3.\n\n```json\n{\n "type": "ranking",\n "text": "Rank these pizza toppings, favourite first.",\n "language": "en",\n "options": [\n {\n "label": "Mushroom"\n },\n {\n "label": "Pepperoni"\n },\n {\n "label": "Olives"\n },\n {\n "label": "Pineapple"\n }\n ],\n "n": 50\n}\n```\n\n## Yes, mostly or no (`yes_mostly_no`)\n\n- Use: A quick check with room for a middle answer: does this make sense, would you trust it.\n- Options: Fixed: Yes, Mostly, No. Do not send options.\n- Answer: One of the three.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "yes_mostly_no",\n "text": "Does this button label tell you what will happen?",\n "context": "Button: Save and continue",\n "language": "en",\n "n": 50\n}\n```\n\n## Free text (`free_text`)\n\n- Use: Why, in the head\'s own words. Best as a follow-up to a choice.\n- Options: No options. Needs Tier 2 or 3. Answers are up to 200 characters.\n- Answer: Short text per head; results carry the texts and a summary.\n- Tier: 2 or 3. Format \xD7 2.0.\n\n```json\n{\n "type": "free_text",\n "text": "Why would you order from Menu B?",\n "language": "en",\n "n": 20,\n "tier": 2\n}\n```\n\n## Click test (`click_test`)\n\n- Use: Where people look or tap first on a page, ad, pack or screen. Results are a heatmap.\n- Options: No options. stimulus.image_url is required; max_taps 1 to 5 (default 1).\n- Answer: One to max_taps points per head, 0\u20131 from the top left. Results carry every tap, the busiest areas with counts and a heatmap PNG.\n- Tier: any. Format \xD7 1.5.\n\n```json\n{\n "type": "click_test",\n "text": "Where would you tap to buy this?",\n "language": "en",\n "stimulus": {\n "image_url": "https://example.com/product-page.png"\n },\n "max_taps": 1,\n "n": 50\n}\n```\n\n## Reaction (`reaction`)\n\n- Use: A gut reaction to an image or line: a logo, a cover, an ad. Five faces drawn as glyphs.\n- Options: Fixed: Love it, Like it, Not sure, Dislike it, Hate it. Do not send options.\n- Answer: One of the five; results add a positive, neutral and negative split.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "reaction",\n "text": "How does this cover make you feel?",\n "language": "en",\n "stimulus": {\n "image_url": "https://example.com/cover.png"\n },\n "n": 50\n}\n```\n',
5365
5376
  "references/tools.md": "# 50heads tools beyond asking\n\nThe core loop is `estimate`, `ask`, then the task (or `wait_for_results`) and `get_results`. These tools cover the rest.\n\n## Before asking\n\n- `upload_image`: for a local file or a generated image, send it as base64 in `data` (JPEG, PNG or WebP, up to 5 MB; about 700 KB through the hosted server, which takes 1 MB requests). For an image already online, pass `image_url` and 50heads copies it. Use the returned `image_url` in the question. Costs nothing.\n- `list_targeting`: countries with the languages their heads read and pool size bands, and the tag ids you can target at Tier 2 and 3. Each condition shrinks the pool; `estimate` shows `pool_size`.\n- `build_ask_link`: when a person should press Ask themselves, give them this link. It opens the portal composer with the question filled in and asks nothing. It also opens a template (`template_id`), a follow-up (`follow_up_of`) or a re-ask of the unanswered part (`reask`).\n- `templates` and `list_questions`: a fixed-price template may fit, and a recent answer may already cover the question.\n\n## After the answers\n\n- `get_answers`: individual answers, 50 a page (up to 200), newest first, with `next_cursor`. Filter by `option` (0 is the first), `tier`, `country`, `age_band` or a keyword `q` in written answers and reasons. Country and age filters show nothing when fewer than 5 answers match, so no head can be singled out.\n- `add_heads`: when the margin is under 10 points with 50 heads, ask more of the same question instead of a new one. It needs its own fresh `idempotency_key` and spends at the same price per answer; say the price and time before you do it when a person is present.\n- `flag_answer`: an answer that is off-topic, low effort, offensive or a duplicate, by its `attestation_ref`. Upheld flags remove the answer and refund it. Never flag an answer because you dislike it.\n- `export`: `csv` has every answer (with the same filters as `get_answers`); `pdf` is a one-page report and `png` the result card where the API serves them.\n- `cancel`: stops a live question and refunds the heads who have not answered.\n\n## Help\n\n- `search_help`: the help centre, for questions about credits, refunds, verification, tiers, the API or this server.\n- `send_feedback`: tells support about a problem or an idea. Signed out, add `email`. Leave out personal data about other people.\n\nTools that are new to the API (`add_heads`, `flag_answer`, PDF and PNG `export`) answer `unavailable` until the API serves them. That is not a failure of your call: carry on without them.\n",
5366
5377
  "scripts/estimate.py": '#!/usr/bin/env python3\n"""Price a 50heads question before asking it. Never spends.\n\nUsage:\n python3 scripts/estimate.py question.json\n echo \'{"type":"single_choice","text":"Which name sounds most like a bakery?","language":"en","options":[{"label":"Crumb & Co"},{"label":"Loafers"}]}\' | python3 scripts/estimate.py -\n\nThe question is the same object the MCP `estimate` and `ask` tools take (see\nreferences/question-types.md). FIFTYHEADS_API_KEY is optional: estimates are public, but a key\ngives a higher rate limit. FIFTYHEADS_API_URL overrides https://api.50heads.com.\nStandard library only.\n"""\n\nimport json\nimport os\nimport sys\nimport urllib.error\nimport urllib.request\n\nAPI_URL = os.environ.get("FIFTYHEADS_API_URL", "https://api.50heads.com").rstrip("/")\n\n\ndef to_draft(q):\n """The MCP question object (snake_case) to the API draft (camelCase)."""\n draft = {\n "type": q["type"],\n "text": q["text"],\n "language": q.get("language", "en"),\n "options": [\n {k: v for k, v in {"label": o.get("label", ""), "imageUrl": o.get("image_url")}.items() if v is not None}\n for o in q.get("options", [])\n ],\n "n": q.get("n", 50),\n "tier": q.get("tier", 2 if q["type"] == "free_text" else 1),\n "rush": q.get("rush", False),\n "neither": q.get("neither", False),\n }\n if q.get("context"):\n draft["context"] = q["context"]\n stimulus = q.get("stimulus") or {}\n if stimulus:\n draft["stimulus"] = {k: v for k, v in {"imageUrl": stimulus.get("image_url"), "text": stimulus.get("text")}.items() if v}\n targeting = q.get("targeting") or {}\n if targeting:\n draft["targeting"] = {"countries": targeting.get("country", []), "tags": targeting.get("tags", [])}\n return draft\n\n\ndef money(price):\n """{"amount": 12.70, "currency": "USD"} as "$12.70", "\xA310.00", "11,80 \u20AC"."""\n amount, currency = price["amount"], price["currency"]\n if currency == "EUR":\n return ("%.2f" % amount).replace(".", ",") + "\\u00a0\u20AC"\n symbol = {"GBP": "\xA3", "USD": "$", "CAD": "$"}.get(currency)\n return symbol + "%.2f" % amount if symbol else "%.2f %s" % (amount, currency)\n\n\ndef main():\n if len(sys.argv) != 2:\n print(__doc__.strip(), file=sys.stderr)\n return 2\n source = sys.stdin if sys.argv[1] == "-" else open(sys.argv[1], encoding="utf-8")\n question = json.load(source)\n body = json.dumps({"draft": to_draft(question), "surface": "web"}).encode("utf-8")\n headers = {"content-type": "application/json", "accept": "application/json", "x-50heads-source": "mcp"}\n key = os.environ.get("FIFTYHEADS_API_KEY")\n if key:\n headers["authorization"] = "Bearer " + key\n request = urllib.request.Request(API_URL + "/v1/billing/estimate", data=body, headers=headers, method="POST")\n try:\n with urllib.request.urlopen(request, timeout=20) as response:\n quote = json.load(response)\n except urllib.error.HTTPError as err:\n detail = json.loads(err.read().decode("utf-8") or "{}").get("error", {})\n print(detail.get("message", "The estimate failed (HTTP %d)." % err.code), file=sys.stderr)\n return 1\n n = question.get("n", 50)\n # The price is in the key\'s account currency (credits alone when the API sends none).\n cost = "%d credits" % quote["creditsTotal"]\n if quote.get("price"):\n cost += " (%s)" % money(quote["price"])\n print("%d answers, about %d minutes, %s." % (n, quote["etaMinutes"], cost))\n print(quote.get("breakdownLine", ""))\n for issue in quote.get("validation", []):\n print("Fix: " + issue["message"])\n return 0\n\n\nif __name__ == "__main__":\n sys.exit(main())\n'
@@ -5952,7 +5963,7 @@ async function handleTaskRequest(method, rawParams, deps) {
5952
5963
 
5953
5964
  // src/constants.ts
5954
5965
  var SERVER_NAME = "50heads";
5955
- var SERVER_VERSION = "2026.925.1";
5966
+ var SERVER_VERSION = "2026.925.2";
5956
5967
  var USER_AGENT = `50heads-mcp/${SERVER_VERSION}`;
5957
5968
  var MCP_RESOURCE_URL = "https://mcp.50heads.com/mcp";
5958
5969
  var TOOL_SCOPES = {
@@ -6894,7 +6905,6 @@ More: call get_answers with cursor "${out.next_cursor}".` : "";
6894
6905
  async () => body
6895
6906
  );
6896
6907
  }
6897
- const appCsp = { connectDomains: [], resourceDomains: [] };
6898
6908
  resource(
6899
6909
  "results-view",
6900
6910
  RESULTS_VIEW_URI,
@@ -6903,7 +6913,7 @@ More: call get_answers with cursor "${out.next_cursor}".` : "";
6903
6913
  async () => resultsViewHtml(),
6904
6914
  null,
6905
6915
  "public",
6906
- { ui: { csp: appCsp, prefersBorder: true } }
6916
+ VIEW_RESOURCE_META
6907
6917
  );
6908
6918
  resource(
6909
6919
  "quote-view",
@@ -6913,7 +6923,7 @@ More: call get_answers with cursor "${out.next_cursor}".` : "";
6913
6923
  async () => quoteViewHtml(),
6914
6924
  null,
6915
6925
  "public",
6916
- { ui: { csp: appCsp, prefersBorder: true } }
6926
+ VIEW_RESOURCE_META
6917
6927
  );
6918
6928
  server.server.setRequestHandler("resources/read", async (request) => {
6919
6929
  const uri = request.params.uri;
package/dist/index.js CHANGED
@@ -4927,7 +4927,8 @@ function pricingTable(currency, rates = FALLBACK_RATES) {
4927
4927
  note: "Age, gender and each tag group are one trait each, at any tier. Countries are free. PickFu charges about $0.40 per trait."
4928
4928
  },
4929
4929
  heads: { min: HEADS_LIMITS.web.min, max: HEADS_LIMITS.web.max, default: 50 },
4930
- connectionDailyCapDefault: price(DEFAULT_CONNECTION_CAP_PENCE, currency, rates),
4930
+ /** Caps are in credits, so they never move with exchange rates. */
4931
+ connectionDailyCapDefaultCredits: DEFAULT_CONNECTION_CAP_PENCE,
4931
4932
  formula: "per answer = tier base \xD7 length \xD7 format (\xD7 five-second) + images + traits (\xD7 rush); total = per answer \xD7 heads"
4932
4933
  };
4933
4934
  }
@@ -4958,11 +4959,14 @@ body{font-family:'DM Sans',system-ui,-apple-system,'Segoe UI',sans-serif;font-we
4958
4959
  .pill{display:inline-flex;align-items:center;height:44px;padding:0 20px;border-radius:${radius.pill}px;border:0;font:inherit;font-weight:700;cursor:pointer}
4959
4960
  .ask{background:var(--tomato);color:var(--ink)}
4960
4961
  .ask:hover{background:var(--tomato-deep)}
4962
+ .ask:disabled{cursor:default;opacity:.5}
4961
4963
  .pill:focus-visible{outline:3px solid var(--tomato);outline-offset:2px}
4962
4964
  .issues{margin:12px 0 0;padding-left:18px}
4963
4965
  .issues li{margin:4px 0}
4964
4966
  .foot{margin-top:16px;display:flex;flex-wrap:wrap;gap:12px;align-items:center}
4965
- .lede{font-size:22px;line-height:1.35;margin:8px 0 4px}
4967
+ .lede{font-size:22px;line-height:1.3;font-weight:700;margin:8px 0 6px}
4968
+ .facts{min-height:1.4em}
4969
+ .quote .foot{margin-top:16px;padding-top:16px;border-top:1px solid var(--track)}
4966
4970
  .note{font-size:17px;line-height:1.45;margin:4px 0}
4967
4971
  .why{margin:16px 0 0;padding:16px 0 0;border-top:1px solid var(--track)}
4968
4972
  .themes{list-style:none;padding:0;margin:8px 0 0;display:grid;gap:10px}
@@ -4979,14 +4983,15 @@ window.addEventListener("message",function(e){var m=e.data;if(!m||m.jsonrpc!=="2
4979
4983
  if(m.id!=null&&pending[m.id]){var r=pending[m.id];delete pending[m.id];r(m.result||null);return}
4980
4984
  if(m.method==="ui/notifications/tool-result"){render(data(m.params))}
4981
4985
  if(m.method==="ui/notifications/host-context-changed"&&m.params){theme(m.params.theme)}});
4982
- function size(){note("ui/notifications/size-changed",{width:document.documentElement.scrollWidth,height:document.documentElement.scrollHeight})}
4986
+ function size(){var h=Math.ceil(document.body.getBoundingClientRect().height);note("ui/notifications/size-changed",{width:document.documentElement.scrollWidth,height:h});
4987
+ if(window.openai&&window.openai.notifyIntrinsicHeight)window.openai.notifyIntrinsicHeight(h)}
4983
4988
  function say(text){if(window.openai&&window.openai.sendFollowUpMessage){window.openai.sendFollowUpMessage({prompt:text});return}
4984
4989
  rpc("ui/message",{role:"user",content:[{type:"text",text:text}]})}
4985
4990
  function start(fn){render=function(d){if(d){fn(d);size()}};
4986
4991
  rpc("ui/initialize",{protocolVersion:"2026-01-26",appInfo:{name:"50heads",version:"1"},appCapabilities:{}}).then(function(r){if(r&&r.hostContext)theme(r.hostContext.theme);note("ui/notifications/initialized")});
4987
4992
  if(window.openai&&window.openai.toolOutput)render(window.openai.toolOutput);
4988
4993
  window.addEventListener("openai:set_globals",function(){if(window.openai&&window.openai.toolOutput)render(window.openai.toolOutput)});
4989
- if(window.ResizeObserver)new ResizeObserver(size).observe(document.body)}
4994
+ if(window.ResizeObserver)new ResizeObserver(size).observe(document.body);size()}
4990
4995
  function el(tag,cls,text){var n=document.createElement(tag);if(cls)n.className=cls;if(text!=null)n.textContent=text;return n}
4991
4996
  function money(p){try{return new Intl.NumberFormat(${JSON.stringify(MONEY_LOCALES)}[p.currency]||"en-US",{style:"currency",currency:p.currency,currencyDisplay:"narrowSymbol"}).format(p.amount)}catch(e){return p.amount+" "+p.currency}}
4992
4997
  function mins(m){return m<90?"about "+m+" minute"+(m===1?"":"s"):"about "+Math.round(m/60)+" hours"}
@@ -5015,13 +5020,13 @@ if(s.suggested_follow_up){btn.hidden=false;btn.onclick=function(){say("Ask the h
5015
5020
  var quoteScript = `
5016
5021
  ${bridge}
5017
5022
  start(function(d){
5018
- var line=document.getElementById("line"),bd=document.getElementById("bd"),issues=document.getElementById("issues"),pool=document.getElementById("pool"),btn=document.getElementById("go");
5019
- var n=d.credits_per_answer?Math.round(d.credits_total/d.credits_per_answer):0;
5023
+ var line=document.getElementById("line"),facts=document.getElementById("facts"),issues=document.getElementById("issues"),btn=document.getElementById("go");
5024
+ var n=d.credits_per_answer?Math.round(d.credits_total/d.credits_per_answer):0,bad=(d.validation||[]).length>0;
5020
5025
  line.textContent=n+" answers, "+mins(d.eta_minutes)+", "+money(d.price)+".";
5021
- bd.textContent=(d.breakdown||[]).map(function(b){return b.value+" "+b.label}).join(" \xB7 ");
5022
- issues.textContent="";(d.validation||[]).forEach(function(v){issues.appendChild(el("li",null,v.message))});issues.hidden=!(d.validation||[]).length;
5023
- pool.textContent=d.pool_size!=null?d.pool_size.toLocaleString("en-GB")+" heads could answer":"";
5024
- btn.textContent="Ask "+n+" heads";btn.hidden=(d.validation||[]).length>0;btn.onclick=function(){say("Ask it: "+line.textContent)}});
5026
+ var f=[d.credits_per_answer+" credits a head",d.credits_total.toLocaleString("en-GB")+" credits in all"];
5027
+ if(d.pool_size!=null)f.push(d.pool_size.toLocaleString("en-GB")+" heads could answer");facts.textContent=f.join(" \xB7 ");
5028
+ issues.textContent="";(d.validation||[]).forEach(function(v){issues.appendChild(el("li",null,v.message))});issues.hidden=!bad;
5029
+ btn.textContent="Ask "+n+" heads";btn.hidden=bad;btn.disabled=false;btn.onclick=function(){say("Ask it: "+line.textContent)}});
5025
5030
  `;
5026
5031
  function page(title, body, script) {
5027
5032
  return `<!doctype html><html lang="en-GB"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title><style>${css}</style></head><body>${body}<script>${script}</script></body></html>`;
@@ -5040,10 +5045,10 @@ function resultsViewHtml() {
5040
5045
  function quoteViewHtml() {
5041
5046
  return page(
5042
5047
  "50heads estimate",
5043
- `<main aria-live="polite"><p class="label">50heads \xB7 Estimate</p>
5044
- <h1 class="lede" id="line">Working out the price.</h1><p class="cap" id="bd"></p>
5045
- <ul class="issues" id="issues" hidden></ul><p class="cap" id="pool"></p>
5046
- <div class="foot"><button class="pill ask" id="go" hidden>Ask 50 heads</button><span class="cap">Nothing is spent until you ask.</span></div></main>`,
5048
+ `<main class="quote" aria-live="polite"><p class="label">50heads \xB7 Estimate</p>
5049
+ <h1 class="lede" id="line">Working out the price.</h1><p class="cap facts" id="facts"></p>
5050
+ <ul class="issues" id="issues" hidden></ul>
5051
+ <div class="foot"><button class="pill ask" id="go" disabled>Ask 50 heads</button><span class="cap">Nothing is spent until you ask.</span></div></main>`,
5047
5052
  quoteScript
5048
5053
  );
5049
5054
  }
@@ -5054,6 +5059,12 @@ function uiMeta(uri) {
5054
5059
  "openai/outputTemplate": uri
5055
5060
  };
5056
5061
  }
5062
+ var VIEW_RESOURCE_META = {
5063
+ ui: { csp: { connectDomains: [], resourceDomains: [] }, prefersBorder: true },
5064
+ "openai/widgetCSP": { connect_domains: [], resource_domains: [] },
5065
+ "openai/widgetDomain": "https://mcp.50heads.com",
5066
+ "openai/widgetPrefersBorder": true
5067
+ };
5057
5068
 
5058
5069
  // src/draft.ts
5059
5070
  var toOption = (o) => ({
@@ -5132,12 +5143,12 @@ function fromDraft(d) {
5132
5143
  }
5133
5144
 
5134
5145
  // src/generated/skill.ts
5135
- var SKILL_VERSION = "2026.925.1";
5146
+ var SKILL_VERSION = "2026.925.2";
5136
5147
  var SKILL_BODY = '# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n"Which menu would you order from?" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: "Menu B wins by 36 points. Confidence high." Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions ("Is it clear and friendly?").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n';
5137
5148
  var SKILL_FILES = {
5138
5149
  "LICENSE": 'MIT License\n\nCopyright (c) 2026 50heads\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n',
5139
- "SKILL.md": '---\nname: 50heads\ndescription: Ask fifty verified real people a five-second question and get a distribution back; use when the work in hand needs a human preference, first impression or sanity check that a model cannot supply.\nlicense: MIT\nmetadata:\n version: "2026.925.1"\n homepage: https://50heads.com/agents\nallowed-tools: "50heads:*"\n---\n\n# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n"Which menu would you order from?" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: "Menu B wins by 36 points. Confidence high." Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions ("Is it clear and friendly?").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n',
5140
- "references/pricing.md": "# Pricing\n\nPrices are in credits. `estimate`, `balance` and 50heads://pricing show them in your account currency at the day's rate. You pay per accepted answer; unanswered heads are refunded.\n\nPer answer = tier base \xD7 length \xD7 format, plus images and targeting traits, times 1.5 for rush. Total = per answer \xD7 heads.\n\n| Tier | Heads | Credits an answer | Credits for 50 answers |\n| --- | --- | --- | --- |\n| 1 | phone-verified | 20 | 1000 |\n| 2 | ID-verified | 42 | 2100 |\n| 3 | trusted panel | 80 | 4000 |\n\nLength (question, context and options together): up to 150 characters \xD7 1.0, up to 300 \xD7 1.1, up to 600 \xD7 1.25.\n\nFormat: `single_choice` \xD7 1.0, `multi_choice` \xD7 1.1, `ab_image` \xD7 1.0, `pairwise` \xD7 1.1, `scale_1_5` \xD7 1.0, `ranking` \xD7 1.3, `yes_mostly_no` \xD7 1.0, `free_text` \xD7 2.0, `click_test` \xD7 1.5, `reaction` \xD7 1.0.\n\nA reason with every answer (`reason`, not for free_text) adds to the format: optional + 0.3, required + 0.6. A required reason on a Tier 1 single choice is 20 \xD7 1.6 = 32 credits an answer. Heads get the same share of the higher price.\n\nImages: 7 credits an answer for each image (audio counts the same). Rush: \xD7 1.5. Five-second test: \xD7 1.3.\n\nTargeting: countries are free. Age, gender and each tag group are one trait each, 3 credits an answer per trait, up to 4, at any tier (PickFu charges about $0.40 per trait). See 50heads://targeting.\n\nHeads: 10 to 5000, default 50.\n\nEvery connection has a daily spend cap (default 5000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.\n\nAlways call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.\n",
5150
+ "SKILL.md": '---\nname: 50heads\ndescription: Ask fifty verified real people a five-second question and get a distribution back; use when the work in hand needs a human preference, first impression or sanity check that a model cannot supply.\nlicense: MIT\nmetadata:\n version: "2026.925.2"\n homepage: https://50heads.com/agents\nallowed-tools: "50heads:*"\n---\n\n# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n"Which menu would you order from?" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: "Menu B wins by 36 points. Confidence high." Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions ("Is it clear and friendly?").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n',
5151
+ "references/pricing.md": "# Pricing\n\nPrices are in credits. `estimate`, `balance` and 50heads://pricing show them in your account currency at the day's rate. You pay per accepted answer; unanswered heads are refunded.\n\nPer answer = tier base \xD7 length \xD7 format, plus images and targeting traits, times 1.5 for rush. Total = per answer \xD7 heads.\n\n| Tier | Heads | Credits an answer | Credits for 50 answers |\n| --- | --- | --- | --- |\n| 1 | phone-verified | 20 | 1000 |\n| 2 | ID-verified | 42 | 2100 |\n| 3 | trusted panel | 80 | 4000 |\n\nLength (question, context and options together): up to 150 characters \xD7 1.0, up to 300 \xD7 1.1, up to 600 \xD7 1.25.\n\nFormat: `single_choice` \xD7 1.0, `multi_choice` \xD7 1.1, `ab_image` \xD7 1.0, `pairwise` \xD7 1.1, `scale_1_5` \xD7 1.0, `ranking` \xD7 1.3, `yes_mostly_no` \xD7 1.0, `free_text` \xD7 2.0, `click_test` \xD7 1.5, `reaction` \xD7 1.0.\n\nA reason with every answer (`reason`, not for free_text) adds to the format: optional + 0.3, required + 0.6. A required reason on a Tier 1 single choice is 20 \xD7 1.6 = 32 credits an answer. Heads get the same share of the higher price.\n\nImages: 7 credits an answer for each image (audio counts the same). Rush: \xD7 1.5. Five-second test: \xD7 1.3.\n\nTargeting: countries are free. Age, gender and each tag group are one trait each, 3 credits an answer per trait, up to 4, at any tier (PickFu charges about $0.40 per trait). See 50heads://targeting.\n\nHeads: 10 to 5000, default 50.\n\nEvery connection has a daily spend cap (default 5,000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.\n\nAlways call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.\n",
5141
5152
  "references/question-types.md": '# Question types\n\nEvery question has `type`, `text` (8 to 600 characters), `language` (such as `en` or `pt-BR`), and optionally `context` (one line, 120 characters), `stimulus` (an image or a line of text shown above the question), `n` (heads, default 50), `tier` (1 to 3, default 1), `rush` and `targeting`.\n\nThe format multiplier is part of the price: per answer = tier base \xD7 length \xD7 format, plus 5 credits an image.\n\nFive-second test: add `exposure_ms` (usually 5000) to an image stimulus on single_choice, multi_choice, yes_mostly_no, scale_1_5, reaction or free_text. The app shows the image for that long, hides it, then asks, so the answer is a first impression or what people remember. \xD7 1.3.\nEvery type except `free_text` can ask for a short written reason with each answer: `reason: "optional"` (format + 0.3) or `"required"` (format + 0.6), 10 to 140 characters. Reasons come back on each answer, translated into your language when heads wrote in another, and `get_results` summarises them (themes with counts and quotes, a sentiment split and a takeaway). Use it instead of a follow-up when you want the which and the why from the same heads.\n\n## Single choice (`single_choice`)\n\n- Use: Pick one of several: names, headlines, taglines, colours.\n- Options: 2 to 8 options, 40 characters each; images optional. Two to four reads best.\n- Answer: One option per head; the distribution counts each option.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "single_choice",\n "text": "Which name sounds most like a bakery?",\n "language": "en",\n "options": [\n {\n "label": "Crumb & Co"\n },\n {\n "label": "Loafers"\n },\n {\n "label": "Proof"\n }\n ],\n "n": 50\n}\n```\n\n## Multiple choice (`multi_choice`)\n\n- Use: Tick all that apply: which features matter, which words fit.\n- Options: 2 to 8 options, 40 characters each. Shares add up to more than 100%.\n- Answer: Any number of options per head; each option\'s share is of all heads.\n- Tier: any. Format \xD7 1.1.\n\n```json\n{\n "type": "multi_choice",\n "text": "Which of these would make you try a new coffee shop?",\n "language": "en",\n "options": [\n {\n "label": "Oat milk at no extra cost"\n },\n {\n "label": "Quiet seating"\n },\n {\n "label": "Loyalty card"\n }\n ],\n "n": 50\n}\n```\n\n## A or B image (`ab_image`)\n\n- Use: Two images side by side: menus, logos, screenshots, packaging.\n- Options: Exactly 2 options, each with an image_url (https, or a pre-signed upload). Labels optional.\n- Answer: One image per head.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "ab_image",\n "text": "Which menu would you order from?",\n "language": "en",\n "options": [\n {\n "label": "Menu A",\n "image_url": "https://example.com/menu-a.png"\n },\n {\n "label": "Menu B",\n "image_url": "https://example.com/menu-b.png"\n }\n ],\n "n": 50\n}\n```\n\n## Pairwise (`pairwise`)\n\n- Use: Compare options two at a time when there are too many to show at once.\n- Options: 2 to 8 options; each head sees one pair, balanced so every pair is seen about equally.\n- Answer: One pick per pair. With 3 or more options each row has count (wins), share (win share), appearances, a Bradley\u2013Terry strength and a rank.\n- Tier: any. Format \xD7 1.1.\n\n```json\n{\n "type": "pairwise",\n "text": "Which subject line would you open first?",\n "language": "en",\n "options": [\n {\n "label": "Your order is on its way"\n },\n {\n "label": "Good news: it has shipped"\n },\n {\n "label": "Tracking number inside"\n }\n ],\n "n": 100\n}\n```\n\n## Scale 1 to 5 (`scale_1_5`)\n\n- Use: How strongly: clarity, appeal, trust. Report the mean and the split.\n- Options: Fixed: 1, 2, 3, 4, 5. Do not send options.\n- Answer: A number from 1 to 5; results include the mean.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "scale_1_5",\n "text": "How clear is this sentence? 1 is not clear, 5 is very clear.",\n "context": "Your parcel will be left in a safe place if you are out.",\n "language": "en",\n "n": 50\n}\n```\n\n## Ranking (`ranking`)\n\n- Use: Put options in order of preference.\n- Options: 2 to 8 options; keep to five or fewer for a five-second answer.\n- Answer: A full order per head; the distribution carries each option\'s average rank.\n- Tier: any. Format \xD7 1.3.\n\n```json\n{\n "type": "ranking",\n "text": "Rank these pizza toppings, favourite first.",\n "language": "en",\n "options": [\n {\n "label": "Mushroom"\n },\n {\n "label": "Pepperoni"\n },\n {\n "label": "Olives"\n },\n {\n "label": "Pineapple"\n }\n ],\n "n": 50\n}\n```\n\n## Yes, mostly or no (`yes_mostly_no`)\n\n- Use: A quick check with room for a middle answer: does this make sense, would you trust it.\n- Options: Fixed: Yes, Mostly, No. Do not send options.\n- Answer: One of the three.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "yes_mostly_no",\n "text": "Does this button label tell you what will happen?",\n "context": "Button: Save and continue",\n "language": "en",\n "n": 50\n}\n```\n\n## Free text (`free_text`)\n\n- Use: Why, in the head\'s own words. Best as a follow-up to a choice.\n- Options: No options. Needs Tier 2 or 3. Answers are up to 200 characters.\n- Answer: Short text per head; results carry the texts and a summary.\n- Tier: 2 or 3. Format \xD7 2.0.\n\n```json\n{\n "type": "free_text",\n "text": "Why would you order from Menu B?",\n "language": "en",\n "n": 20,\n "tier": 2\n}\n```\n\n## Click test (`click_test`)\n\n- Use: Where people look or tap first on a page, ad, pack or screen. Results are a heatmap.\n- Options: No options. stimulus.image_url is required; max_taps 1 to 5 (default 1).\n- Answer: One to max_taps points per head, 0\u20131 from the top left. Results carry every tap, the busiest areas with counts and a heatmap PNG.\n- Tier: any. Format \xD7 1.5.\n\n```json\n{\n "type": "click_test",\n "text": "Where would you tap to buy this?",\n "language": "en",\n "stimulus": {\n "image_url": "https://example.com/product-page.png"\n },\n "max_taps": 1,\n "n": 50\n}\n```\n\n## Reaction (`reaction`)\n\n- Use: A gut reaction to an image or line: a logo, a cover, an ad. Five faces drawn as glyphs.\n- Options: Fixed: Love it, Like it, Not sure, Dislike it, Hate it. Do not send options.\n- Answer: One of the five; results add a positive, neutral and negative split.\n- Tier: any. Format \xD7 1.0.\n\n```json\n{\n "type": "reaction",\n "text": "How does this cover make you feel?",\n "language": "en",\n "stimulus": {\n "image_url": "https://example.com/cover.png"\n },\n "n": 50\n}\n```\n',
5142
5153
  "references/tools.md": "# 50heads tools beyond asking\n\nThe core loop is `estimate`, `ask`, then the task (or `wait_for_results`) and `get_results`. These tools cover the rest.\n\n## Before asking\n\n- `upload_image`: for a local file or a generated image, send it as base64 in `data` (JPEG, PNG or WebP, up to 5 MB; about 700 KB through the hosted server, which takes 1 MB requests). For an image already online, pass `image_url` and 50heads copies it. Use the returned `image_url` in the question. Costs nothing.\n- `list_targeting`: countries with the languages their heads read and pool size bands, and the tag ids you can target at Tier 2 and 3. Each condition shrinks the pool; `estimate` shows `pool_size`.\n- `build_ask_link`: when a person should press Ask themselves, give them this link. It opens the portal composer with the question filled in and asks nothing. It also opens a template (`template_id`), a follow-up (`follow_up_of`) or a re-ask of the unanswered part (`reask`).\n- `templates` and `list_questions`: a fixed-price template may fit, and a recent answer may already cover the question.\n\n## After the answers\n\n- `get_answers`: individual answers, 50 a page (up to 200), newest first, with `next_cursor`. Filter by `option` (0 is the first), `tier`, `country`, `age_band` or a keyword `q` in written answers and reasons. Country and age filters show nothing when fewer than 5 answers match, so no head can be singled out.\n- `add_heads`: when the margin is under 10 points with 50 heads, ask more of the same question instead of a new one. It needs its own fresh `idempotency_key` and spends at the same price per answer; say the price and time before you do it when a person is present.\n- `flag_answer`: an answer that is off-topic, low effort, offensive or a duplicate, by its `attestation_ref`. Upheld flags remove the answer and refund it. Never flag an answer because you dislike it.\n- `export`: `csv` has every answer (with the same filters as `get_answers`); `pdf` is a one-page report and `png` the result card where the API serves them.\n- `cancel`: stops a live question and refunds the heads who have not answered.\n\n## Help\n\n- `search_help`: the help centre, for questions about credits, refunds, verification, tiers, the API or this server.\n- `send_feedback`: tells support about a problem or an idea. Signed out, add `email`. Leave out personal data about other people.\n\nTools that are new to the API (`add_heads`, `flag_answer`, PDF and PNG `export`) answer `unavailable` until the API serves them. That is not a failure of your call: carry on without them.\n",
5143
5154
  "scripts/estimate.py": '#!/usr/bin/env python3\n"""Price a 50heads question before asking it. Never spends.\n\nUsage:\n python3 scripts/estimate.py question.json\n echo \'{"type":"single_choice","text":"Which name sounds most like a bakery?","language":"en","options":[{"label":"Crumb & Co"},{"label":"Loafers"}]}\' | python3 scripts/estimate.py -\n\nThe question is the same object the MCP `estimate` and `ask` tools take (see\nreferences/question-types.md). FIFTYHEADS_API_KEY is optional: estimates are public, but a key\ngives a higher rate limit. FIFTYHEADS_API_URL overrides https://api.50heads.com.\nStandard library only.\n"""\n\nimport json\nimport os\nimport sys\nimport urllib.error\nimport urllib.request\n\nAPI_URL = os.environ.get("FIFTYHEADS_API_URL", "https://api.50heads.com").rstrip("/")\n\n\ndef to_draft(q):\n """The MCP question object (snake_case) to the API draft (camelCase)."""\n draft = {\n "type": q["type"],\n "text": q["text"],\n "language": q.get("language", "en"),\n "options": [\n {k: v for k, v in {"label": o.get("label", ""), "imageUrl": o.get("image_url")}.items() if v is not None}\n for o in q.get("options", [])\n ],\n "n": q.get("n", 50),\n "tier": q.get("tier", 2 if q["type"] == "free_text" else 1),\n "rush": q.get("rush", False),\n "neither": q.get("neither", False),\n }\n if q.get("context"):\n draft["context"] = q["context"]\n stimulus = q.get("stimulus") or {}\n if stimulus:\n draft["stimulus"] = {k: v for k, v in {"imageUrl": stimulus.get("image_url"), "text": stimulus.get("text")}.items() if v}\n targeting = q.get("targeting") or {}\n if targeting:\n draft["targeting"] = {"countries": targeting.get("country", []), "tags": targeting.get("tags", [])}\n return draft\n\n\ndef money(price):\n """{"amount": 12.70, "currency": "USD"} as "$12.70", "\xA310.00", "11,80 \u20AC"."""\n amount, currency = price["amount"], price["currency"]\n if currency == "EUR":\n return ("%.2f" % amount).replace(".", ",") + "\\u00a0\u20AC"\n symbol = {"GBP": "\xA3", "USD": "$", "CAD": "$"}.get(currency)\n return symbol + "%.2f" % amount if symbol else "%.2f %s" % (amount, currency)\n\n\ndef main():\n if len(sys.argv) != 2:\n print(__doc__.strip(), file=sys.stderr)\n return 2\n source = sys.stdin if sys.argv[1] == "-" else open(sys.argv[1], encoding="utf-8")\n question = json.load(source)\n body = json.dumps({"draft": to_draft(question), "surface": "web"}).encode("utf-8")\n headers = {"content-type": "application/json", "accept": "application/json", "x-50heads-source": "mcp"}\n key = os.environ.get("FIFTYHEADS_API_KEY")\n if key:\n headers["authorization"] = "Bearer " + key\n request = urllib.request.Request(API_URL + "/v1/billing/estimate", data=body, headers=headers, method="POST")\n try:\n with urllib.request.urlopen(request, timeout=20) as response:\n quote = json.load(response)\n except urllib.error.HTTPError as err:\n detail = json.loads(err.read().decode("utf-8") or "{}").get("error", {})\n print(detail.get("message", "The estimate failed (HTTP %d)." % err.code), file=sys.stderr)\n return 1\n n = question.get("n", 50)\n # The price is in the key\'s account currency (credits alone when the API sends none).\n cost = "%d credits" % quote["creditsTotal"]\n if quote.get("price"):\n cost += " (%s)" % money(quote["price"])\n print("%d answers, about %d minutes, %s." % (n, quote["etaMinutes"], cost))\n print(quote.get("breakdownLine", ""))\n for issue in quote.get("validation", []):\n print("Fix: " + issue["message"])\n return 0\n\n\nif __name__ == "__main__":\n sys.exit(main())\n'
@@ -5699,7 +5710,7 @@ async function handleTaskRequest(method, rawParams, deps) {
5699
5710
 
5700
5711
  // src/constants.ts
5701
5712
  var SERVER_NAME = "50heads";
5702
- var SERVER_VERSION = "2026.925.1";
5713
+ var SERVER_VERSION = "2026.925.2";
5703
5714
  var USER_AGENT = `50heads-mcp/${SERVER_VERSION}`;
5704
5715
  var MCP_RESOURCE_URL = "https://mcp.50heads.com/mcp";
5705
5716
  var TOOL_SCOPES = {
@@ -6641,7 +6652,6 @@ More: call get_answers with cursor "${out.next_cursor}".` : "";
6641
6652
  async () => body
6642
6653
  );
6643
6654
  }
6644
- const appCsp = { connectDomains: [], resourceDomains: [] };
6645
6655
  resource(
6646
6656
  "results-view",
6647
6657
  RESULTS_VIEW_URI,
@@ -6650,7 +6660,7 @@ More: call get_answers with cursor "${out.next_cursor}".` : "";
6650
6660
  async () => resultsViewHtml(),
6651
6661
  null,
6652
6662
  "public",
6653
- { ui: { csp: appCsp, prefersBorder: true } }
6663
+ VIEW_RESOURCE_META
6654
6664
  );
6655
6665
  resource(
6656
6666
  "quote-view",
@@ -6660,7 +6670,7 @@ More: call get_answers with cursor "${out.next_cursor}".` : "";
6660
6670
  async () => quoteViewHtml(),
6661
6671
  null,
6662
6672
  "public",
6663
- { ui: { csp: appCsp, prefersBorder: true } }
6673
+ VIEW_RESOURCE_META
6664
6674
  );
6665
6675
  server.server.setRequestHandler("resources/read", async (request) => {
6666
6676
  const uri = request.params.uri;
@@ -10,7 +10,10 @@ export declare const RESULTS_VIEW_URI = "ui://50heads/results.html";
10
10
  export declare const QUOTE_VIEW_URI = "ui://50heads/quote.html";
11
11
  /** The results view: winner line, bars, confidence, a follow-up button. */
12
12
  export declare function resultsViewHtml(): string;
13
- /** The quote card: cost and time in one sentence, the breakdown, fixes, an ask button. */
13
+ /**
14
+ * The quote card: cost and time in one sentence, credits and pool, fixes, an ask button. The
15
+ * button is there (disabled) before the result lands so the card keeps its height.
16
+ */
14
17
  export declare function quoteViewHtml(): string;
15
18
  /** `_meta` a tool or resource carries to declare its view. */
16
19
  export declare function uiMeta(uri: string): {
@@ -20,3 +23,22 @@ export declare function uiMeta(uri: string): {
20
23
  "ui/resourceUri": string;
21
24
  "openai/outputTemplate": string;
22
25
  };
26
+ /**
27
+ * `_meta` each view resource carries: no outside hosts and a border. ChatGPT also reads its own
28
+ * keys and shows "CSP off" without them; `ui.domain` is left to each host (its format differs).
29
+ */
30
+ export declare const VIEW_RESOURCE_META: {
31
+ ui: {
32
+ csp: {
33
+ connectDomains: never[];
34
+ resourceDomains: never[];
35
+ };
36
+ prefersBorder: boolean;
37
+ };
38
+ "openai/widgetCSP": {
39
+ connect_domains: never[];
40
+ resource_domains: never[];
41
+ };
42
+ "openai/widgetDomain": string;
43
+ "openai/widgetPrefersBorder": boolean;
44
+ };
@@ -4,9 +4,9 @@
4
4
  */
5
5
  export declare const SERVER_NAME = "50heads";
6
6
  /** Calendar version (YYYY.MDD.N). Stamped at release by scripts/release.ts; never edit by hand. */
7
- export declare const SERVER_VERSION = "2026.925.1";
7
+ export declare const SERVER_VERSION = "2026.925.2";
8
8
  /** Sent by the CLI on every request, so the API can tell releases apart. */
9
- export declare const USER_AGENT = "50heads-mcp/2026.925.1";
9
+ export declare const USER_AGENT = "50heads-mcp/2026.925.2";
10
10
  export declare const MCP_RESOURCE_URL = "https://mcp.50heads.com/mcp";
11
11
  /** The scope each tool needs (spec: Authorization → Scopes). null: no sign-in needed. */
12
12
  export declare const TOOL_SCOPES: {
@@ -1,5 +1,5 @@
1
1
  /** The skill's version (front matter metadata.version); advertised as skillVersion. */
2
- export declare const SKILL_VERSION = "2026.925.1";
2
+ export declare const SKILL_VERSION = "2026.925.2";
3
3
  /** SKILL.md without its front matter: the 50heads://guide resource. */
4
4
  export declare const SKILL_BODY = "# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: \"50 answers, about 10 minutes, $12.70.\"\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n\"Which menu would you order from?\" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: \"Menu B wins by 36 points. Confidence high.\" Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions (\"Is it clear and friendly?\").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n";
5
5
  /** Every file in the skill bundle, by path relative to skills/50heads. */
@@ -78,7 +78,8 @@ export declare function pricingTable(currency: Currency, rates?: Rates): {
78
78
  max: 5000;
79
79
  default: number;
80
80
  };
81
- connectionDailyCapDefault: Price;
81
+ /** Caps are in credits, so they never move with exchange rates. */
82
+ connectionDailyCapDefaultCredits: number;
82
83
  formula: string;
83
84
  };
84
85
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@50heads/mcp",
3
- "version": "2026.925.1",
3
+ "version": "2026.925.2",
4
4
  "mcpName": "com.50heads/mcp",
5
5
  "description": "50heads MCP: ask fifty verified real people from Claude, ChatGPT, Cursor or your own agent.",
6
6
  "keywords": [
@@ -60,8 +60,8 @@
60
60
  "typescript": "^5.9.3",
61
61
  "@50heads/brand": "0.0.0",
62
62
  "@50heads/login": "0.0.0",
63
- "@50heads/shared": "0.0.0",
64
- "@50heads/tsconfig": "0.0.0"
63
+ "@50heads/tsconfig": "0.0.0",
64
+ "@50heads/shared": "0.0.0"
65
65
  },
66
66
  "scripts": {
67
67
  "generate": "node scripts/generate.ts",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "com.50heads/mcp",
4
4
  "title": "50heads",
5
5
  "description": "Ask fifty verified real people a five-second question and get the split back.",
6
- "version": "2026.925.1",
6
+ "version": "2026.925.2",
7
7
  "websiteUrl": "https://50heads.com/agents",
8
8
  "repository": {
9
9
  "url": "https://github.com/50heads/mcp",
@@ -45,7 +45,7 @@
45
45
  "registryType": "npm",
46
46
  "registryBaseUrl": "https://registry.npmjs.org",
47
47
  "identifier": "@50heads/mcp",
48
- "version": "2026.925.1",
48
+ "version": "2026.925.2",
49
49
  "runtimeHint": "npx",
50
50
  "transport": {
51
51
  "type": "stdio"
@@ -78,7 +78,7 @@
78
78
  "account:read"
79
79
  ]
80
80
  },
81
- "skillVersion": "2026.925.1"
81
+ "skillVersion": "2026.925.2"
82
82
  }
83
83
  }
84
84
  }
@@ -3,7 +3,7 @@ name: 50heads
3
3
  description: Ask fifty verified real people a five-second question and get a distribution back; use when the work in hand needs a human preference, first impression or sanity check that a model cannot supply.
4
4
  license: MIT
5
5
  metadata:
6
- version: "2026.925.1"
6
+ version: "2026.925.2"
7
7
  homepage: https://50heads.com/agents
8
8
  allowed-tools: "50heads:*"
9
9
  ---
@@ -22,6 +22,6 @@ Targeting: countries are free. Age, gender and each tag group are one trait each
22
22
 
23
23
  Heads: 10 to 5000, default 50.
24
24
 
25
- Every connection has a daily spend cap (default 5000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.
25
+ Every connection has a daily spend cap (default 5,000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.
26
26
 
27
27
  Always call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.
Binary file