@50heads/mcp 2026.1009.1 → 2026.1013.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.
@@ -1,6 +1,6 @@
1
1
  import { McpServer, type ServerContext } from "@modelcontextprotocol/server";
2
2
  import { type Client } from "@50heads/shared/client";
3
- export { MCP_RESOURCE_URL, PROMPT_SCOPES, PUBLIC_RESOURCE_PREFIXES, SERVER_NAME, SERVER_VERSION, TOOL_NAMES, TOOL_SCOPES, resourceScope, type ToolName, } from "./constants.js";
3
+ export { MCP_RESOURCE_URL, PROMPT_SCOPES, PUBLIC_RESOURCE_PREFIXES, SERVER_NAME, SERVER_VERSION, TOOL_ACTIONS, TOOL_NAMES, MAX_TOOLS, WRITE_ACTIONS, ACTION_IDS, actionScope, resourceScope, type ActionId, type ToolName, } from "./constants.js";
4
4
  export type CallLog = {
5
5
  kind: "tool" | "resource" | "prompt";
6
6
  name: string;
@@ -22,13 +22,15 @@ export type ServerOptions = {
22
22
  authenticated: boolean;
23
23
  portalUrl: string;
24
24
  webUrl: string;
25
+ /** The API's public address, for links to it (webhook schemas). Default https://api.50heads.com. */
26
+ apiUrl?: string;
25
27
  /** Protected resource metadata URL, advertised in the com.50heads/auth extension. */
26
28
  resourceMetadataUrl?: string;
27
29
  traceId?: string;
28
- /** Longest a single call may block (wait_for_results), in ms. Default 600 s. */
30
+ /** Longest a single call may block (results, action wait), in ms. Default 600 s. */
29
31
  maxWaitMs?: number;
30
32
  onCall?: (log: CallLog) => void;
31
- /** Fetches public files from webUrl (the help index for search_help). Default: global fetch. */
33
+ /** Fetches public files from webUrl (the help index for help, action search). Default: global fetch. */
32
34
  fetchWeb?: typeof fetch;
33
35
  /** Sent with feedback so support can tell hosts and versions apart, such as "mcp/2026.924.1". */
34
36
  source?: string;
@@ -31,8 +31,8 @@ export type TaskObject = {
31
31
  pollInterval: number;
32
32
  };
33
33
  export declare function isTerminal(status: TaskStatus): boolean;
34
- /** "12 of 50 answered · about 6 minutes left" */
35
- export declare function statusMessage(q: Pick<QuestionV2, "status" | "answered" | "n" | "etaMinutes" | "refusedCategory">): string;
34
+ /** "12 of 50 answered" */
35
+ export declare function statusMessage(q: Pick<QuestionV2, "status" | "answered" | "n" | "refusedCategory">): string;
36
36
  export declare function toTask(q: QuestionV2, now?: string): TaskObject;
37
37
  export type TaskDeps = {
38
38
  /** A client for the calling principal, tagged with the task method. */
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * How one tool that takes an `action` is described to a host and validated for the server.
4
+ *
5
+ * Every action has its own input object. The server validates a call with a discriminated union
6
+ * on `action`, so each action's arguments are checked against that action's schema alone. Hosts
7
+ * need a plain object at the root of a tool's input schema (several refuse oneOf or anyOf there),
8
+ * so tools/list carries one merged object instead: `action` (an enum, required) and every
9
+ * argument of every action, each saying which actions use it. The description lists each action
10
+ * with its arguments, so a model can choose well without reading the schema.
11
+ */
12
+ export type ActionSpec = {
13
+ /** The action's name, such as "get". */
14
+ name: string;
15
+ /** What the action does, in plain words (its part of the tool's description). */
16
+ description: string;
17
+ /** The action's own arguments, without `action`. */
18
+ input: z.ZodObject;
19
+ /** The scope the action needs; null when it needs no sign-in. */
20
+ scope: string | null;
21
+ /** True when the action changes nothing. */
22
+ readOnly: boolean;
23
+ destructive: boolean;
24
+ idempotent: boolean;
25
+ };
26
+ type Json = Record<string, unknown>;
27
+ /** The one plain-object JSON Schema tools/list carries for a tool. */
28
+ export declare function mergedInputSchema(tool: string, actions: ActionSpec[]): Json;
29
+ /** A Standard Schema (with JSON Schema) that validates with the union and shows hosts the merged object. */
30
+ export type PublishedInput = {
31
+ "~standard": {
32
+ version: 1;
33
+ vendor: string;
34
+ validate: (value: unknown) => unknown;
35
+ jsonSchema: {
36
+ input: () => Json;
37
+ output: () => Json;
38
+ };
39
+ };
40
+ };
41
+ export declare function publishedInput(union: z.ZodType, merged: Json): PublishedInput;
42
+ /** The discriminated union a call is validated with. */
43
+ export declare function inputUnion(actions: ActionSpec[]): z.ZodDiscriminatedUnion<[z.ZodObject<{
44
+ [x: string]: any;
45
+ action: z.ZodLiteral<string>;
46
+ }, z.core.$strip>, ...z.ZodObject<{
47
+ [x: string]: any;
48
+ action: z.ZodLiteral<string>;
49
+ }, z.core.$strip>[]], "action">;
50
+ /** The tool's description: its purpose, then every action with its arguments and what it needs. */
51
+ export declare function toolDescription(intro: string, actions: ActionSpec[]): string;
52
+ /** The tool-level hints, true to every action in it. */
53
+ export declare function toolAnnotations(actions: ActionSpec[]): {
54
+ readOnlyHint: boolean;
55
+ idempotentHint: boolean;
56
+ openWorldHint: boolean;
57
+ destructiveHint?: undefined;
58
+ } | {
59
+ readOnlyHint: boolean;
60
+ destructiveHint: boolean;
61
+ idempotentHint: boolean;
62
+ openWorldHint: boolean;
63
+ };
64
+ export {};
@@ -0,0 +1,35 @@
1
+ import { WEBHOOK_EVENTS, type WebhookEndpoint, type WebhookFilter } from "@50heads/shared";
2
+ import type { WebhookDelivery as DeliverySchema } from "@50heads/shared";
3
+ import type { z } from "zod";
4
+ import type { WebhookDeliveryOut, WebhookEndpointOut, WebhookFilterInput } from "./schemas.js";
5
+ /** What each event type means, for 50heads://webhook-events and the tool descriptions. */
6
+ export declare const WEBHOOK_EVENT_NOTES: Record<(typeof WEBHOOK_EVENTS)[number] | "webhook.test", string>;
7
+ /** The API's endpoint as the tools return it (snake_case). */
8
+ export declare function toEndpoint(e: WebhookEndpoint): WebhookEndpointOut;
9
+ export declare function toDelivery(d: z.infer<typeof DeliverySchema>): WebhookDeliveryOut;
10
+ /** snake_case filter → the API's; null stays null (clears), undefined stays undefined. */
11
+ export declare function fromFilter(f: z.infer<typeof WebhookFilterInput> | null | undefined): WebhookFilter | null | undefined;
12
+ /** One line per endpoint for the text content. */
13
+ export declare function endpointLine(e: WebhookEndpointOut): string;
14
+ export declare function deliveryLine(d: WebhookDeliveryOut): string;
15
+ /** The 50heads://webhook-events resource: event types, what they carry and where the schemas are. */
16
+ export declare function webhookEventsDoc(apiUrl: string, webUrl: string): {
17
+ apiVersions: "2026-10-10"[];
18
+ latest: "2026-10-10";
19
+ schemas: string;
20
+ docs: string;
21
+ events: {
22
+ type: "question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "webhook.test" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused";
23
+ description: string;
24
+ schema: string;
25
+ }[];
26
+ envelope: string;
27
+ filters: string;
28
+ signing: {
29
+ standard: string;
30
+ legacy: string;
31
+ rotation: string;
32
+ tolerance: string;
33
+ };
34
+ retries: string;
35
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Workspace apps (the Slack and Microsoft Teams apps) and Zapier connections as the tools return them: snake_case,
3
+ * credits, and a person's name or email for who added it. Read-only here; installs are added and
4
+ * removed in the portal, and Zapier connects from Zapier.
5
+ */
6
+ import type { WorkspaceApp, ZapierConnection } from "@50heads/shared";
7
+ import type { WorkspaceAppOut, ZapierConnectionOut } from "./schemas.js";
8
+ export declare function toWorkspaceApp(a: WorkspaceApp): WorkspaceAppOut;
9
+ /** One line per install: "app_1 · Slack · Acme: 400 of 2,000 credits spent today. Added by Ana on 2026-10-10." */
10
+ export declare function workspaceAppLine(a: WorkspaceAppOut): string;
11
+ export declare function toZapierConnection(z: ZapierConnection): ZapierConnectionOut;
12
+ /** "conn_1 · Zapier: 2 hooks; 1,200 of 5,000 credits spent today. Connected by Ana on 2026-10-09." */
13
+ export declare function zapierLine(z: ZapierConnectionOut): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@50heads/mcp",
3
- "version": "2026.1009.1",
3
+ "version": "2026.1013.2",
4
4
  "mcpName": "com.50heads/mcp",
5
5
  "description": "50heads MCP: ask fifty verified people from Claude, ChatGPT, Cursor or your own agent.",
6
6
  "keywords": [
@@ -57,8 +57,8 @@
57
57
  "@types/node": "^22.18.0",
58
58
  "esbuild": "^0.28.2",
59
59
  "typescript": "^5.9.3",
60
- "@50heads/login": "0.0.0",
61
60
  "@50heads/brand": "0.0.0",
61
+ "@50heads/login": "0.0.0",
62
62
  "@50heads/shared": "0.0.0",
63
63
  "@50heads/tsconfig": "0.0.0"
64
64
  },
@@ -67,7 +67,8 @@
67
67
  "build": "node scripts/build.ts",
68
68
  "typecheck": "tsc -p tsconfig.json",
69
69
  "test": "node --test \"src/**/*.test.ts\" \"scripts/**/*.test.ts\"",
70
- "release": "node scripts/release.ts"
70
+ "release": "node scripts/release.ts",
71
+ "smoke": "node scripts/smoke.ts"
71
72
  },
72
73
  "types": "./dist/types/public.d.ts"
73
74
  }
package/server.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "com.50heads/mcp",
4
4
  "title": "50heads",
5
- "description": "Ask fifty verified people a five-second question and get the split back.",
6
- "version": "2026.1009.1",
5
+ "description": "Ask fifty verified people a five-second question and see what they think.",
6
+ "version": "2026.1013.2",
7
7
  "websiteUrl": "https://50heads.com/agents",
8
8
  "repository": {
9
9
  "url": "https://github.com/50heads/mcp",
@@ -44,7 +44,7 @@
44
44
  "registryType": "npm",
45
45
  "registryBaseUrl": "https://registry.npmjs.org",
46
46
  "identifier": "@50heads/mcp",
47
- "version": "2026.1009.1",
47
+ "version": "2026.1013.2",
48
48
  "runtimeHint": "npx",
49
49
  "transport": {
50
50
  "type": "stdio"
@@ -67,6 +67,11 @@
67
67
  "productivity",
68
68
  "human-feedback"
69
69
  ],
70
+ "documentationUrl": "https://50heads.com/en-us/docs/mcp",
71
+ "privacyPolicyUrl": "https://50heads.com/en-gb/legal/privacy",
72
+ "noTrainStatementUrl": "https://50heads.com/en-gb/help/privacy/ai-and-your-data",
73
+ "termsOfServiceUrl": "https://50heads.com/en-gb/legal/terms",
74
+ "supportUrl": "https://50heads.com/en-gb/contact?purpose=support",
70
75
  "authorization": {
71
76
  "resourceMetadata": "https://mcp.50heads.com/.well-known/oauth-protected-resource",
72
77
  "authorizationServer": "https://auth.50heads.com",
@@ -74,10 +79,19 @@
74
79
  "questions:read",
75
80
  "questions:write",
76
81
  "templates:read",
77
- "account:read"
82
+ "account:read",
83
+ "webhooks:read",
84
+ "webhooks:write",
85
+ "apps:read",
86
+ "apps:write",
87
+ "organization:read",
88
+ "organization:write",
89
+ "members:read",
90
+ "members:write",
91
+ "audit:read"
78
92
  ]
79
93
  },
80
- "skillVersion": "2026.1009.1"
94
+ "skillVersion": "2026.1013.2"
81
95
  }
82
96
  }
83
97
  }
@@ -3,18 +3,18 @@ name: 50heads
3
3
  description: Ask fifty verified 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.1009.1"
6
+ version: "2026.1013.2"
7
7
  homepage: https://50heads.com/agents
8
8
  allowed-tools: "50heads:*"
9
9
  ---
10
10
 
11
11
  # Asking 50heads
12
12
 
13
- 50heads 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.
13
+ 50heads puts one short question to verified people and shows what they think.
14
14
 
15
15
  ## When to ask
16
16
 
17
- Ask 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.
17
+ Ask 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. Check recent `results` (`list`) first.
18
18
 
19
19
  ## Eight rules for a good question
20
20
 
@@ -22,30 +22,35 @@ Ask when the answer is a human reaction: which name, headline, image or menu peo
22
22
  2. Ask about a preference or first impression, never a fact.
23
23
  3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.
24
24
  4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.
25
- 5. Word it neutrally. No leading words, no hint of the answer you want.
26
- 6. Put background in `context`, one line, only when a head needs it.
27
- 7. 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.
28
- 8. 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.
25
+ 5. Word it neutrally, with no hint of the answer you want.
26
+ 6. Put needed background in `context`, one line.
27
+ 7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `yes_no` with `go_rule` to approve, `free_text` (Tier 2) for why.
28
+ 8. 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 real audience's countries and language.
29
29
 
30
30
  ## How to ask
31
31
 
32
- 1. Call `estimate` and, when a person is present, show them the price and time together: "50 answers, about 10 minutes, $12.70."
33
- 2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.
34
- 3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.
32
+ 1. Call `estimate` (`quote`); with a person present, show the price: "50 answers, $12.70." Don't promise a time.
33
+ 2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse it to retry.
34
+ 3. Wait on the task (older hosts: `results`, `wait`).
35
35
 
36
36
  ## Reading results
37
37
 
38
- Lead 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.
38
+ Lead 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. Report underfilled as underfilled. Quote the percentages, not adjectives.
39
+
40
+ With a `go_rule`, lead with the `verdict`: go, no go or unclear against the asker's line. `head_to_head` says who beats whom; overlapping ranges mean the lead may be chance.
39
41
 
40
42
  ## Worked example
41
43
 
42
- "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.
44
+ "Which menu would you order from?": two photos, `ab_image`, 50 heads, Tier 1. B 68%, A 32%: "Menu B wins by 36 points. Confidence high."
43
45
 
44
46
  ## Avoid
45
47
 
46
48
  - Double questions ("Is it clear and friendly?").
47
- - Yes-or-no questions that lead.
48
- - Asking for personal data or anything that identifies someone.
49
- - Re-asking the same question to fish for a different answer.
49
+ - Personal data, or anything that identifies someone.
50
+ - Re-asking to fish for a different answer.
51
+
52
+ ## Your data
53
+
54
+ We don't use your questions, the answers you buy or the reasons heads write to train AI models, and we don't sell them.
50
55
 
51
56
  More: `references/question-types.md`, `pricing.md`, `tools.md`.
@@ -1,6 +1,6 @@
1
1
  # Pricing
2
2
 
3
- Prices 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.
3
+ Prices are in credits. `estimate` (quote and balance) and 50heads://pricing show them in your account currency at the day's rate. You pay per accepted answer; unanswered heads are refunded.
4
4
 
5
5
  Per answer = tier base × length × format, plus images and targeting traits, times 1.5 for rush. Total = per answer × heads.
6
6
 
@@ -26,4 +26,4 @@ Your audience (`answered_by: "private"`): a link you share instead of heads, 5 c
26
26
 
27
27
  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.
28
28
 
29
- Always call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.
29
+ Always call `estimate` (action quote) first: it returns the exact price, the time and any fixes the question needs, and never spends.
@@ -5,11 +5,11 @@ Every question has `type`, `text` (8 to 600 characters), `language` (such as `en
5
5
  The format multiplier is part of the price: per answer = tier base × length × format, plus 5 credits an image.
6
6
 
7
7
  Five-second test: add `exposure_ms` (usually 5000) to an image stimulus on single_choice, multi_choice, yes_no, 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. × 1.3.
8
- Every 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.
8
+ Every 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 `results` (action get) summarizes 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.
9
9
 
10
10
  ## Single choice (`single_choice`)
11
11
 
12
- - Use: Pick one of several: names, headlines, taglines, colours.
12
+ - Use: Pick one of several: names, headlines, taglines, colors.
13
13
  - Options: 2 to 8 options, 40 characters each; images optional. Two to four reads best.
14
14
  - Answer: One option per head; the distribution counts each option.
15
15
  - Tier: any. Format × 1.0.
@@ -91,7 +91,7 @@ Every type except `free_text` can ask for a short written reason with each answe
91
91
 
92
92
  - Use: Compare options two at a time when there are too many to show at once.
93
93
  - Options: 2 to 8 options; each head sees one pair, balanced so every pair is seen about equally.
94
- - Answer: One pick per pair. With 3 or more options each row has count (wins), share (win share), appearances, a Bradley–Terry strength and a rank.
94
+ - Answer: One pick per pair. With 3 or more options each row has count (wins), share (win share), a 95% range on it (low, high), appearances, a Bradley–Terry strength and a rank. When the top two ranges overlap, the lead may be chance. head_to_head says who beats whom: share[i][j] is the share of people who put option i above option j.
95
95
  - Tier: any. Format × 1.1.
96
96
 
97
97
  ```json
@@ -135,13 +135,13 @@ Every type except `free_text` can ask for a short written reason with each answe
135
135
 
136
136
  - Use: Put options in order of preference.
137
137
  - Options: 2 to 8 options; keep to five or fewer for a five-second answer.
138
- - Answer: A full order per head; the distribution carries each option's average rank.
138
+ - Answer: A full order per head; the distribution carries each option's average rank, and head_to_head says who beats whom: share[i][j] is the share of people who put option i above option j.
139
139
  - Tier: any. Format × 1.3.
140
140
 
141
141
  ```json
142
142
  {
143
143
  "type": "ranking",
144
- "text": "Rank these pizza toppings, favourite first.",
144
+ "text": "Rank these pizza toppings, favorite first.",
145
145
  "language": "en",
146
146
  "options": [
147
147
  {
@@ -163,9 +163,9 @@ Every type except `free_text` can ask for a short written reason with each answe
163
163
 
164
164
  ## Yes or no (`yes_no`)
165
165
 
166
- - Use: A binary check. Heads answer Yes or No. This is the yes/no type. For a middle answer, use yes_mostly_no.
166
+ - Use: A binary check. Heads answer Yes or No. This is the yes/no type. For a middle answer, use yes_mostly_no. For an approval-style question, add go_rule to get a go or a no go when Yes clears your line.
167
167
  - Options: Fixed: Yes, No. Do not send options.
168
- - Answer: Yes or No.
168
+ - Answer: Yes or No. With go_rule ({ "go_above": 0.6, "min_answers": 40 }), results carry a verdict: go when the whole 95% range on the Yes share is above the line, no_go when it is all at or below it, otherwise unclear (too few answers, or the range crosses the line). The line is fixed once the question is live and costs nothing extra.
169
169
  - Tier: any. Format × 1.0.
170
170
 
171
171
  ```json
@@ -174,15 +174,19 @@ Every type except `free_text` can ask for a short written reason with each answe
174
174
  "text": "Does this button label tell you what will happen?",
175
175
  "context": "Button: Save and continue",
176
176
  "language": "en",
177
- "n": 50
177
+ "n": 50,
178
+ "go_rule": {
179
+ "go_above": 0.6,
180
+ "min_answers": 40
181
+ }
178
182
  }
179
183
  ```
180
184
 
181
185
  ## Yes, mostly or no (`yes_mostly_no`)
182
186
 
183
- - Use: Yes, Mostly or No. Set this type when you want the middle answer. A plain yes/no question is yes_no.
187
+ - Use: Yes, Mostly or No. Set this type when you want the middle answer. A plain yes/no question is yes_no. Takes go_rule like yes_no.
184
188
  - Options: Fixed: Yes, Mostly, No. Do not send options.
185
- - Answer: One of the three.
189
+ - Answer: One of the three. With go_rule, Mostly counts as half a Yes in the verdict.
186
190
  - Tier: any. Format × 1.0.
187
191
 
188
192
  ```json
@@ -1,27 +1,88 @@
1
1
  # 50heads tools beyond asking
2
2
 
3
- The core loop is `estimate`, `ask`, then the task (or `wait_for_results`) and `get_results`. These tools cover the rest.
3
+ 50heads has nine tools, and every tool takes an `action` plus that action's own arguments: `estimate`, `ask`, `results`, `questions`, `projects`, `webhooks`, `organization`, `audit` and `help`. Below, "`results` action `get`" means the tool `results` called with `action: "get"`. Each action is checked against its own scope, so a connection can read without being able to change anything.
4
+
5
+ The core loop is `estimate` action `quote`, `ask` action `ask`, then the task (or `results` action `wait`) and `results` action `get`. These actions cover the rest.
4
6
 
5
7
  ## Before asking
6
8
 
7
- - `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.
8
- - `list_audiences`: interest audiences, panels of heads who proved they fit (UK commuter cyclists, for example). Put an id in `question.audience` to ask one instead of targeting; the price is the audience's at the minimum grade.
9
- - `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`.
10
- - `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`).
11
- - `templates` and `list_questions`: a fixed-price template may fit, and a recent answer may already cover the question. `list_questions` takes `set_id` to list one set's questions in order.
12
- - `ask_set`: two to ten questions for the person's own audience behind one link, answered in order, about five seconds each. Same question shapes as `ask` without tier, rush, targeting, reasons or free text; `max_answers`, `open_for_days`, `shown_as` and `public_results` are set once for the set. Returns the link, the `question_ids` in order and the reserve (questions × max answers × 5 credits). Results are per question.
9
+ - `estimate` action `quote`: price, time and any fixes for a question; never spends. `estimate` action `balance`: your credits, what live questions have reserved and what is left under the connection's daily cap; check it before asking something large.
10
+ - `ask` action `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.
11
+ - `estimate` action `audiences`: interest audiences, panels of heads who proved they fit (UK commuter cyclists, for example). Put an id in `question.audience` to ask one instead of targeting; the price is the audience's at the minimum grade.
12
+ - `estimate` action `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` action `quote` shows `pool_size`.
13
+ - `ask` action `build_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`).
14
+ - `estimate` action `templates` and `results` action `list`: a fixed-price template may fit, and a recent answer may already cover the question. `results` action `list` takes `set_id` to list one set's questions in order.
15
+ - `ask` action `set`: two to ten questions for the person's own audience behind one link, answered in order, about five seconds each. Same question shapes as `ask` action `ask` without tier, rush, targeting, reasons or free text; `max_answers`, `open_for_days`, `shown_as` and `public_results` are set once for the set. Returns the link, the `question_ids` in order and the reserve (questions × max answers × 5 credits). Results are per question.
16
+
17
+ ## Go or no go, and who beats whom
18
+
19
+ - `go_rule` on a `yes_no` or `yes_mostly_no` question: `{ "go_above": 0.6, "min_answers": 40 }` means go when Yes is above 60%, with at least 40 answers. `go_above` is a whole percentage from 0.5 to 0.95; `min_answers` is at least 5 and at most `n`, and defaults to 80% of `n`. On `yes_mostly_no`, Mostly counts as half a Yes. Use it for approval-style questions, where you will act on the answer: "Is this label clear enough to ship?" Set it before asking: the line cannot move once the question is live, and it costs nothing extra. Other types refuse it.
20
+ - `verdict` in the results: `go` when the whole 95% range on the Yes share (`low` to `high`) is above `go_above`, `no_go` when it is all at or below it, otherwise `unclear`, with `reason` `too_few` (fewer than `min_answers`) or `too_close` (the range crosses the line; `questions` action `add_heads` can settle it). The summary note opens with the same sentence. Report it as how these heads answered against the line, not as a fact.
21
+ - `head_to_head` for `ranking`, and `pairwise` with 3 or more options: `share[i][j]` is the share of people who compared options i and j and put i above j, and `n[i][j]` how many compared them. Use it to say who beats whom, not just who came first.
22
+ - Pairwise rows also carry `low` and `high`, a 95% range on each win share. When the top two ranges overlap, the top two are close and the lead may be chance.
13
23
 
14
24
  ## After the answers
15
25
 
16
- - `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.
17
- - `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.
18
- - `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.
19
- - `export`: `csv` has every answer (with the same filters as `get_answers`); `pdf` is a one-page report and `png` the result card. The API serves all three.
20
- - `cancel`: stops a live question and refunds the heads who have not answered.
26
+ - `results` action `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.
27
+ - `questions` action `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.
28
+ - `questions` action `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.
29
+ - `results` action `export`: `csv` has every answer (with the same filters as `results` action `answers`); `jsonl` has the same answers as JSON lines, one object per answer with `choice` (option indexes) and `cohortHash`, for scripts and model training; `pdf` is a one-page report and `png` the result card. The API serves all four.
30
+ - `choice` on each answer (in `results` action `get`, `results` action `answers` and the JSONL export) is the answer as indexes into the options: `index` (one pick, or a rating's 1 to 5 as 0 to 4), `indexes` (pick several), `order` (ranking, best first) or `pair` (the two options shown in a pairwise matchup of 3 or more options). Absent for free text and click tests.
31
+ - `cohort_hash` in `results` action `get` (`cohortHash` in the JSONL export): the same value means the same kind of audience was asked (who answers, how many, which tier and targeting, language and type). It never covers the words asked and never identifies anyone. Use it to group results from different questions put to the same audience.
32
+ - `questions` action `cancel`: stops a live question and refunds the heads who have not answered.
33
+
34
+ ## Filing questions
35
+
36
+ - `projects` action `list`, `projects` action `create`, `projects` action `update`, `projects` action `delete`: projects group questions under a name and a description. `projects` action `list` shows each with its question count; archived projects only with `include_archived`. `projects` action `update` renames, redescribes or archives one; archiving leaves its questions as they are. `projects` action `delete` removes the project for good but never its questions: they keep their answers and results and are left outside any project. Confirm with the person before deleting.
37
+ - `questions` action `file`: moves one question into a project (`project_id`, or `null` to take it out), replaces its `labels` (up to 10, no commas), archives or bookmarks it, or sets your `external_ref`. Send only what should change. `external_ref` is set once: the same value again is fine, a different one is refused. Cancel a live question before archiving it.
38
+ - `projects` action `labels`: the labels in use, most used first, with counts.
39
+ - `ask` actions `ask` and `set` take `project_id` and `labels` to file as they ask; `results` action `list` filters by `project_id`, `label`, `external_ref`, `bookmarked` and `archived`.
40
+
41
+ With a team key, projects and labels are the team's.
42
+
43
+ ## Webhooks
44
+
45
+ Webhooks send events (a question completed, underfilled or refused, credits low, and more) to an https address as they happen, so a script or a CI job does not have to poll. They need the `webhooks:read` and `webhooks:write` scopes, which a connection gets only when the person grants them. Read `50heads://webhook-events` for every event type, what it carries and the links to its JSON Schema.
46
+
47
+ - `webhooks` action `create`: `url`, `events`, an optional `description`, and a `filter` of `project_ids` or `labels` to send question events only for those. It sends this account's question events to that address, so only use an address the person owns and asked for, and confirm it with them first. The signing secret is in the reply once: give it to the person to store with the receiver. Then `webhooks` action `test`.
48
+ - `webhooks` action `list`: each endpoint with its events, filter, whether it is on and the last delivery's status. Secrets are masked.
49
+ - `webhooks` action `update`: change `url`, `events`, `description` or `filter` (`null` clears it). `enabled: false` pauses; `enabled: true` turns it back on, also after 50heads switched it off for failing.
50
+ - `webhooks` action `rotate_secret`: a new secret, returned once. The old one keeps signing for `overlap_hours` (default 24, up to 168), so the receiver can switch without missing a delivery; `0` cuts over at once.
51
+ - `webhooks` action `test`: sends a signed `webhook.test` event now and says how the receiver answered.
52
+ - `webhooks` action `deliveries`: the latest 100, filtered by `endpoint_id` or `state`. `failed` means out of retries (8 attempts over about a day).
53
+ - `webhooks` action `events`: the latest events of one `type` in this account, newest first (`limit` 1 to 25, default 3), each exactly as a delivery's body carries it. Events are kept whether or not an endpoint listens: use one as a sample payload when writing a receiver.
54
+ - `webhooks` action `replay`: sends a delivery's event again, with the same event id, so receivers that drop duplicates stay safe.
55
+ - `webhooks` action `delete`: removes the endpoint for good; pausing is usually better. Confirm with the person first.
56
+
57
+ Each delivery is signed twice: Standard Webhooks headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`) and `X-50heads-Signature`. Verify either one, refuse timestamps more than 5 minutes old, and drop event ids you have already handled. `50heads webhooks verify` checks a captured delivery offline.
58
+
59
+ ## Slack and Zapier
60
+
61
+ - `organization` action `apps`: the Slack workspaces 50heads is added to, each with its daily spend cap and what it has spent today, in credits, and who added it. People in those workspaces ask with `/50heads ask`, spending this account's credits within that cap. It also lists the Zapier accounts connected to this one (`zapier`), each with its daily cap, today's spend and live hooks, one per Zap trigger. It needs the `apps:read` scope. Adding, changing the cap and removing happen in the portal (Settings, Integrations), not from here; removing a Zapier connection deletes its hooks.
62
+
63
+ ## Organization (admins)
64
+
65
+ These read an organization and never change it. They work with an organization admin's key or connected app that holds the scope, or with one of the organization's own service accounts; anyone else gets `insufficient_scope` or `organization_admin_required`. Changes to domains, sign-in rules, single sign-on, SCIM and service accounts are made by a person in the portal (Settings, Organization).
66
+
67
+ - `organization` action `get` (`organization:read`): the organization of the workspace you act in: workspaces, verified domains, whether single sign-on is set up or required, whether SCIM is connected, its sign-in rules.
68
+ - `organization` action `domains` (`organization:read`): each domain's state and the DNS TXT record that proves it, to pass to whoever manages DNS.
69
+ - `audit` action `list` (`audit:read`): who did what, newest first. Filter with `from`, `to`, `actor` (a user id or a service account id, `sa_…`), `event_action` (an audit action such as `domain.added`, or a group such as `sso`) and `workspace_id`; page with `next_cursor`. Each event says whether a person, a key, a connected app, a service account, SCIM or 50heads acted.
70
+ - `audit` action `export` (`audit:read`): the same events as JSON lines, oldest first, up to `max_events` at a time with a `resume_cursor`. Every export is itself recorded in the log.
71
+ - `organization` action `scim` (`organization:read`): SCIM provisioning: how many people and groups the identity provider has sent, the default role, and its tokens by label and prefix.
72
+ - `organization` action `member_sessions` (`members:read`): where one person is signed in, and how each session started.
73
+ - `organization` action `service_accounts` (`organization:read`): the organization's service accounts, with their workspace, scopes, caps in credits, today's spend and last token. Never their secrets.
74
+
75
+ Events and lists never hold secrets, one-time codes or anything that identifies a head.
21
76
 
22
77
  ## Help
23
78
 
24
- - `search_help`: the help centre, for questions about credits, refunds, verification, tiers, the API or this server.
25
- - `send_feedback`: tells support about a problem or an idea. Signed out, add `email`. Leave out personal data about other people.
79
+ - `help` action `search`: the help center, for questions about credits, refunds, verification, tiers, the API or this server.
80
+ - `help` action `feedback`: tells support about a problem or an idea. Signed out, add `email`. Leave out personal data about other people.
81
+ - `help` action `whoami`: who the connection acts as (email masked), its scopes, daily and per-question caps and today's spend, the account and organization, and the rate limits with what is used. Call it first when something is refused.
82
+ - `help` action `status`: whether 50heads is working and whether asking, answering, MCP asking or buying credits is paused. Needs no sign-in. Check it when calls fail, and do not ask while asking is paused.
83
+
84
+ `questions` actions `add_heads` and `flag_answer`, and `results` action `export` (CSV, JSONL, PDF and PNG), are served by the API. Use them when the result calls for it.
85
+
86
+ ## Your data
26
87
 
27
- `add_heads`, `flag_answer` and `export` (CSV, PDF and PNG) are served by the API. Use them when the result calls for it.
88
+ We don't use your questions, the answers you buy or the reasons heads write to train AI models, and we don't sell them.
@@ -107,7 +107,7 @@ def main():
107
107
  cost = "%d credits" % quote["creditsTotal"]
108
108
  if quote.get("price"):
109
109
  cost += " (%s)" % money(quote["price"])
110
- print("%d answers, about %d minutes, %s." % (n, quote["etaMinutes"], cost))
110
+ print("%d answers, %s." % (n, cost))
111
111
  print(quote.get("breakdownLine", ""))
112
112
  for issue in quote.get("validation", []):
113
113
  print("Fix: " + issue["message"])
Binary file