Connect an AI assistant with MCP
An MCP human in the loop step lets an assistant pause for a quick judgement before it acts. The invitation-only sandbox currently returns synthetic answers, so you can test the integration before live human questions open.
Last checked against each client's own documentation. Settings screens change often: if a step doesn't match what you see, tell us through the developer route.
On this page
MCP human in the loop: when to pause an agent
Use a human in the loop MCP server when a decision depends on taste or tone: which of two replies is clearer, whether a person would send a delicate message, or which image makes the offer easier to understand. The agent asks for a quote with get_quote, sends a neutral question with ask_question, then calls get_results with the returned cursor. Each wait can last up to 25 seconds. When the pool closes, the owner can read the reasons and verify each live answer’s receipt before deciding what to do.
The sandbox follows the same flow with synthetic test answers and separate sandbox receipts. Keys are issued by invitation. This is useful for testing your code; it is not evidence that a person preferred an option. See the REST human judgement API and the agent accountability rules for the wider workflow.
Which setup do I need?
| You use | Connects to sandbox? | How |
|---|---|---|
| Claude Code | Yes | One claude mcp add command |
| Claude Desktop or claude.ai | Yes | Custom connector with your key as a request header |
| ChatGPT (developer mode connectors) | Not yet | Its connector screen offers OAuth or no sign-in, not an API key. Use a Custom GPT Action |
| ChatGPT (Custom GPT Action) | Yes | The REST API with your key as a Bearer token |
| OpenAI Responses API and Agents SDK | Yes | MCP tool with an Authorization header |
| OpenAI Codex CLI | Yes | codex mcp add with a bearer token variable |
| Cursor | Yes | mcp.json with a header |
| VS Code (Copilot agent mode) | Yes | mcp.json with a key prompt |
| Gemini CLI, Zed, legacy Cascade and others | Should work | Any client that can send a header to a remote Streamable HTTP server |
Endpoint
- URL:
https://pennyforyours.com/mcp - Transport: Streamable HTTP, stateless (no session ID), JSON responses.
POSTonly;GETandDELETEreturn 405. - Auth:
Authorization: Bearer pfy_test_…on every request. Calls without a key get HTTP 401. OAuth sign-in is not offered yet. - Mode: sandbox only. Synthetic test members answer within a few minutes, and nothing is charged.
- Protocol: built on the official MCP TypeScript SDK, which speaks protocol revisions up to
2025-11-25. The current specification revision is2026-07-28, so your client must be able to use2025-11-25or an earlier revision.
Keep your key out of repositories: every snippet below reads it from an environment variable or a secure prompt, or has a placeholder for you to replace.
Claude Code
export PFY_API_KEY=pfy_test_... # your sandbox key
claude mcp add --transport http --scope user pennyforyours https://pennyforyours.com/mcp \
--header "Authorization: Bearer $PFY_API_KEY"--scope is local (the default: this project, just you), project (shared through .mcp.json) or user (all your projects). Your shell expands $PFY_API_KEY before Claude Code sees it, so the key is stored in ~/.claude.json. To share the server with a team without committing the key, use the project file, which Claude Code expands from the environment:
{
"mcpServers": {
"pennyforyours": {
"type": "http",
"url": "https://pennyforyours.com/mcp",
"headers": {
"Authorization": "Bearer ${PFY_API_KEY}"
}
}
}
}Then ask: “Run a five-answer sandbox test: which bakery name sounds friendlier, Brightwell or Corvane?” Claude calls ask_question, then get_results until the synthetic pool closes.
Claude Desktop and claude.ai
Custom connectors work on Free (one custom connector), Pro, Max, Team and Enterprise plans. The connection is made from Anthropic's servers, so it reaches the hosted endpoint, not a server on your own machine.
Free, Pro and Max:
- Open Customize → Connectors, select + Add, then Add custom connector.
- Name it “PennyForYours” and enter
https://pennyforyours.com/mcp. - Choose No sign in.
- Under Request headers, add
Authorizationwith the valueBearer pfy_test_…. Claude stores it encrypted and sends it with every request. - Add the connector, then switch it on in a chat.
Team and Enterprise: an Owner adds it under Organization settings → Connectors → Add → Custom → Web; members then connect from Customize → Connectors.
A local development server: use the mcp-remote bridge (MIT) in claude_desktop_config.json. The header value goes in env because some clients do not escape spaces in args:
{
"mcpServers": {
"pennyforyours": {
"command": "npx",
"args": [
"mcp-remote",
"https://pennyforyours.com/mcp",
"--header",
"Authorization:${PFY_AUTH_HEADER}"
],
"env": {
"PFY_AUTH_HEADER": "Bearer pfy_test_..."
}
}
}
}ChatGPT and OpenAI
In the ChatGPT app, through developer mode connectors: not yet. ChatGPT's connector screen supports OAuth, no authentication, or a mix of the two. It can't send a fixed API key, and our MCP server needs one. OAuth is on our list; tell us if it would let you use this connection.
In ChatGPT, through a Custom GPT Action: yes
Use the REST API instead of MCP:
- In the GPT editor, add an Action and paste in the OpenAPI document from
https://pennyforyours.com/v1/openapi.json. - Set Authentication to API Key, choose Bearer, and paste your sandbox key.
- In the GPT's instructions, tell it to call
GET /v1/question-types, thenPOST /v1/questionswith anIdempotency-Key, thenGET /v1/questions/{id}with the returned cursor until the state isclosed.
Actions time out after 45 seconds, so keep wait_seconds at 25 or less. Results can take minutes, so the GPT may need to check back rather than wait in one turn.
OpenAI Responses API
{
"model": "YOUR_MODEL",
"input": "Run a five-answer sandbox test: which bakery name sounds friendlier, Brightwell or Corvane?",
"tools": [
{
"type": "mcp",
"server_label": "pennyforyours",
"server_url": "https://pennyforyours.com/mcp",
"headers": {
"Authorization": "Bearer pfy_test_..."
},
"require_approval": "never"
}
]
}OpenAI Agents SDK (Python)
from agents.mcp import MCPServerStreamableHttp
server = MCPServerStreamableHttp(
name="pennyforyours",
params={
"url": "https://pennyforyours.com/mcp",
"headers": {"Authorization": f"Bearer {api_key}"},
"timeout": 30, # get_results can wait up to 25 seconds in one call
},
)OpenAI Codex CLI
# ~/.codex/config.toml
[mcp_servers.pennyforyours]
url = "https://pennyforyours.com/mcp"
bearer_token_env_var = "PFY_API_KEY"Or run codex mcp add pennyforyours --url https://pennyforyours.com/mcp --bearer-token-env-var PFY_API_KEY.
Cursor
In .cursor/mcp.json (this project) or ~/.cursor/mcp.json (everywhere). Cursor reads environment variables as ${env:NAME}.
{
"mcpServers": {
"pennyforyours": {
"url": "https://pennyforyours.com/mcp",
"headers": {
"Authorization": "Bearer ${env:PFY_API_KEY}"
}
}
}
}VS Code (Copilot agent mode)
Run MCP: Add Server. For new servers VS Code recommends its portable destinations: a workspace .mcp.json (the same mcpServers format as Claude Code) or your Copilot settings. The older .vscode/mcp.json is marked deprecated but still works, and it is the format where VS Code can prompt for the key once and store it securely:
{
"inputs": [
{
"type": "promptString",
"id": "pfy-api-key",
"description": "PennyForYours sandbox API key",
"password": true
}
],
"servers": {
"pennyforyours": {
"type": "http",
"url": "https://pennyforyours.com/mcp",
"headers": {
"Authorization": "Bearer ${input:pfy-api-key}"
}
}
}
}VS Code's documentation does not say the portable .mcp.json supports this key prompt, and servers that use ${input:…} are not passed to its Agent Host.
Other clients
- Gemini CLI:
gemini mcp add --transport http --header "Authorization: Bearer $PFY_API_KEY" pennyforyours https://pennyforyours.com/mcp. Insettings.json, a Streamable HTTP server goes underhttpUrl(urlmeans the older SSE transport). - Zed: add a remote server under
context_serverswithurlandheaders. - Devin Desktop, legacy Cascade agent: Devin's Cascade guide uses
serverUrlplusheadersin~/.config/devin/mcp_config.json(macOS and Linux), with${env:VAR}for the key. New tabs use Devin Local and its separate MCP setup. - Anything else: if it can send a header to a remote Streamable HTTP MCP server, it should work.
MCP Inspector and raw HTTP
npx @modelcontextprotocol/inspector --cli https://pennyforyours.com/mcp --transport http \
--method tools/list --header "Authorization: Bearer $PFY_API_KEY"Any HTTP client works. Send JSON-RPC with POST and accept both JSON and event streams:
curl https://pennyforyours.com/mcp \
-H "Authorization: Bearer $PFY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_question_types","arguments":{}}}'Human in the loop MCP server tools
Each tool mirrors a REST endpoint and needs the same scope. Errors come back as tool results with isError: true and a problem object (type, status, detail), so the model can read them and recover.
list_question_types List question types
Lists the question types you can ask now, with option rules, accepted assets and the launch example price per answer (in pence). Call this first to choose a type. Priced per answer by answer type. Launch example: a quick take (pick or rate plus one line of why, about 30 seconds) from 30p a person, plus 50p a question for screening and the summary. Free during early access: nothing is charged.
Hints: read-only
list_topics List topics
Lists the topic codes you can tag a question with (up to 3, in the topics field of get_quote and ask_question). Members choose which questions to answer from a board they can filter by topic, pay, time and answer type, so a good tag helps the right people find your question.
Hints: read-only
get_quote Get a price and time estimate
Prices a question before you ask it and estimates how long answers take. Returns quote_id (pass it to ask_question), offer_pence (what each person is paid), min_offer_pence, fee_per_answer_minor (our fee on top), price_per_answer_minor, request_fee_minor, launch_example_total_minor, charge_minor (0 during early access), eligible_pool and an ETA band. Nothing is created or charged. Priced per answer by answer type. Launch example: a quick take (pick or rate plus one line of why, about 30 seconds) from 30p a person, plus 50p a question for screening and the summary. Free during early access: nothing is charged. Pools of 5 to 50 people. Text or an image (PNG, JPEG or WebP); links and URL questions are refused.
Hints: read-only
| Argument | Type | Required | Notes |
|---|---|---|---|
type | string | Yes | Question type code from list_question_types: "quick_pick" (2–3 options), "quick_rate" (1–5 rating) or "quick_first_impression" (one line). |
prompt | string | Yes | The question, 3–280 characters, neutral wording. No links. |
options | string[] | No | For quick_pick: 2–3 distinct options, each up to 80 characters. Leave out for other types. |
pool | integer | Yes | How many verified people should answer (5–50). |
deadline_hours | integer | No | Close after this many hours even if the pool is not full (1–168). |
offer_pence | integer | No | Your offer per answer in pence. Every person who answers is paid it; our fee is added on top. Default: the launch example pay for the type (20p for a quick take). Never below the fair pay floor (get_quote shows min_offer_pence). |
required_tier | integer | No | Lowest member tier to ask: 1 everyday (default), 2 proven, 3 qualified (offers from £1). In-depth is always 2 or above. |
topics | string[] | No | Up to 3 topic codes from list_topics. Members choose which questions to answer from a board they filter by topic, pay and time, so tags help the right people find yours. |
expected_minutes | integer | No | Long answer types only (In-depth: 10–20). Minutes you expect each answer to take; sets the fair pay floor and the time members see. |
upload_id | string | No | An upload_id from an earlier ask_question image or POST /v1/uploads, if the question shows an image. |
ask_question Ask verified real people
Asks a pool of verified real people a quick question about a short text or an image, and returns the question with its state and a cursor. Then call get_results with that cursor and wait_seconds to collect answers as they arrive. To show an image, pass image.data as base64 (PNG, JPEG or WebP, up to 4 MB) or an upload_id from POST /v1/uploads. Image URLs and links are refused. Pass the same idempotency_key when retrying so the question is not asked twice. Answers come from verified real people (passkey-verified members, each answer signed with a receipt), not from an AI model. Sandbox mode: during early access only pfy_test_ keys are issued. Sandbox questions are answered by synthetic test members within a few minutes, every answer is marked synthetic, and nothing reaches real people. Priced per answer by answer type. Launch example: a quick take (pick or rate plus one line of why, about 30 seconds) from 30p a person, plus 50p a question for screening and the summary. Free during early access: nothing is charged. Not for regulated advice (medical, legal, financial), collecting personal data, or writing reviews.
Hints: idempotent
| Argument | Type | Required | Notes |
|---|---|---|---|
type | string | Yes | Question type code from list_question_types: "quick_pick" (2–3 options), "quick_rate" (1–5 rating) or "quick_first_impression" (one line). |
prompt | string | Yes | The question, 3–280 characters, neutral wording. No links. |
options | string[] | No | For quick_pick: 2–3 distinct options, each up to 80 characters. Leave out for other types. |
pool | integer | Yes | How many verified people should answer (5–50). |
deadline_hours | integer | No | Close after this many hours even if the pool is not full (1–168). |
offer_pence | integer | No | Your offer per answer in pence. Every person who answers is paid it; our fee is added on top. Default: the launch example pay for the type (20p for a quick take). Never below the fair pay floor (get_quote shows min_offer_pence). |
required_tier | integer | No | Lowest member tier to ask: 1 everyday (default), 2 proven, 3 qualified (offers from £1). In-depth is always 2 or above. |
topics | string[] | No | Up to 3 topic codes from list_topics. Members choose which questions to answer from a board they filter by topic, pay and time, so tags help the right people find yours. |
expected_minutes | integer | No | Long answer types only (In-depth: 10–20). Minutes you expect each answer to take; sets the fair pay floor and the time members see. |
idempotency_key | string | Yes | A unique string for this question (8–100 characters). Reuse it only when retrying the same request. |
quote_id | string | No | quote_id from get_quote, if you asked for one. |
upload_id | string | No | An image already uploaded with POST /v1/uploads. |
image | object | No | An image to show with the question. |
get_results Get results (long-poll)
Returns the question's state, accepted answers (choice or rating, the one line of why, receipt_id), counts per option, a summary once enough answers are in, and a cursor. To wait for news, pass the cursor from your last call and wait_seconds (up to 25): it returns as soon as an answer, state change or summary arrives, with changed=true, or when the wait runs out with changed=false. Keep calling until state is "closed", "cancelled" or "rejected". Answers held for a person to review are counted in held_for_review but not shown. Answers come from verified real people (passkey-verified members, each answer signed with a receipt), not from an AI model.
Hints: read-only
| Argument | Type | Required | Notes |
|---|---|---|---|
question_id | string | Yes | The id returned by ask_question. |
cursor | integer | No | cursor from your previous get_results or ask_question call. |
wait_seconds | integer | No | Wait up to this many seconds (0–25) for something newer than cursor. |
rate_answers Rate answers
Tells PennyForYours which answers helped: up or down per answer, at most one most_useful, and optionally which option you finally chose (final_choice, 1-based position). Ratings feed the answerers' Standing; ratings from AI tools are weighted separately from human askers. Rating again replaces your earlier rating.
Hints: idempotent
| Argument | Type | Required | Notes |
|---|---|---|---|
question_id | string | Yes | |
ratings | object[] | No | One entry per answer you want to rate. |
final_choice | integer | No | For quick_pick: the option position you went with. |
cancel_question Cancel a question
Cancels one of your questions. Only possible before the first answer arrives; after that the question runs to its pool or deadline. Returns the final snapshot.
Hints: destructive
| Argument | Type | Required | Notes |
|---|---|---|---|
question_id | string | Yes |
get_receipt Get an answer receipt
Returns the signed receipt for one answer on your question: a compact JWS (ES256) whose claims bind the question (qh), the exact image or text shown (ah), the answer (ans), the assignment (asg) and its status (st: accepted, held or void), plus the answerer's verification level and checks. Anyone can verify it with the public keys at /.well-known/jwks.json. Sandbox receipts (pfy_test_ keys) are signed with a separate sandbox key (kid sbx-, published at /v1/sandbox/jwks.json), say chk: ["synthetic"] and are never proof of a real person. Receipts are signed, not anchored on a blockchain.
Hints: read-only
| Argument | Type | Required | Notes |
|---|---|---|---|
answer_id | string | Yes | An answer id from get_results. |
verify_receipt Verify a receipt
Checks any PennyForYours receipt (a compact JWS) against the published public keys and returns valid, the signed claims and any mismatches. Sandbox receipts return valid: false and sandbox: true. Optionally pass values you already hold (the SHA-256 of the image you uploaded, a question hash, an answer hash or an assignment nonce) to confirm the receipt is about exactly that. Works for receipts shared with you by someone else.
Hints: read-only
| Argument | Type | Required | Notes |
|---|---|---|---|
jws | string | Yes | The compact JWS receipt. |
expected | object | No |
get_balance Get balance, usage and limits
Shows this key's mode (sandbox or live), what it has used this month, what that would cost at launch example prices, any caps and the rate limits. Priced per answer by answer type. Launch example: a quick take (pick or rate plus one line of why, about 30 seconds) from 30p a person, plus 50p a question for screening and the summary. Free during early access: nothing is charged.
Hints: read-only
upload_audio_clip Upload a sound clip
Uploads one sound clip for an audio question and returns its upload_id, sha256 (the ah value receipts will carry) and duration_ms. MP3, M4A, AAC, Ogg (Opus or Vorbis) or WAV, up to 4 MB and 90 seconds, as base64. The type is checked by content and tags are removed before hashing. Give a one-sentence description of the clip (members who cannot listen and screen-reader users rely on it), optionally a transcript, and confirm you own the audio or have permission to use it. A person listens to every clip before members hear it. Then call ask_audio_question with the upload_id.
| Argument | Type | Required | Notes |
|---|---|---|---|
data | string | Yes | Base64 audio bytes, or a data: URI (data:audio/mpeg;base64,...). Not a URL. |
mime_type | audio/mpeg | audio/aac | audio/mp4 | audio/ogg | audio/wav | Yes | audio/mpeg, audio/mp4, audio/aac, audio/ogg or audio/wav. |
description | string | Yes | One sentence saying what the clip is, for members who cannot listen. |
transcript | string | No | Words spoken or sung in the clip, if any. |
rights_confirmed | boolean | Yes | true: you own this audio or have permission to use it. |
ask_audio_question Ask verified real people about a sound clip
Asks a pool of verified real people about one sound clip (any image type: pick one, rate 1 to 5, first impression), or which of two clips sounds better (quick_pick with exactly two options and two clips, one per option, in order). Upload each clip first with upload_audio_clip. Members listen in their browser before answering; the time to listen is added to the time each answer takes, which raises the fair pay floor. A person listens to every clip before the question opens. Then call get_results as for ask_question. Answers come from verified real people (passkey-verified members, each answer signed with a receipt), not from an AI model. Sandbox mode: during early access only pfy_test_ keys are issued. Sandbox questions are answered by synthetic test members within a few minutes, every answer is marked synthetic, and nothing reaches real people.
Hints: idempotent
| Argument | Type | Required | Notes |
|---|---|---|---|
type | string | Yes | Question type code from list_question_types: "quick_pick" (2–3 options), "quick_rate" (1–5 rating) or "quick_first_impression" (one line). |
prompt | string | Yes | The question, 3–280 characters, neutral wording. No links. |
options | string[] | No | For quick_pick: 2–3 distinct options, each up to 80 characters. Leave out for other types. |
pool | integer | Yes | How many verified people should answer (5–50). |
deadline_hours | integer | No | Close after this many hours even if the pool is not full (1–168). |
offer_pence | integer | No | Your offer per answer in pence. Every person who answers is paid it; our fee is added on top. Default: the launch example pay for the type (20p for a quick take). Never below the fair pay floor (get_quote shows min_offer_pence). |
required_tier | integer | No | Lowest member tier to ask: 1 everyday (default), 2 proven, 3 qualified (offers from £1). In-depth is always 2 or above. |
topics | string[] | No | Up to 3 topic codes from list_topics. Members choose which questions to answer from a board they filter by topic, pay and time, so tags help the right people find yours. |
expected_minutes | integer | No | Long answer types only (In-depth: 10–20). Minutes you expect each answer to take; sets the fair pay floor and the time members see. |
clip_upload_ids | string[] | Yes | One upload_id from upload_audio_clip, or two for which sounds better (one per option, in option order). |
idempotency_key | string | Yes | A unique string for this question (8–100 characters). Reuse it only when retrying the same request. |
quote_id | string | No | quote_id from get_quote, if you asked for one. |
Transport details
- Stateless Streamable HTTP built on the official MCP TypeScript SDK. Every request stands alone, so there is no
Mcp-Session-Id;GETandDELETEreturn 405. get_resultslong-polls for up to 25 seconds inside a single call; nothing streams back mid-call.- Rate limits are per key and shared with REST: a tool call uses the same budget as its endpoint. See sandbox and limits.
- Optional header
X-Agent-Namerecords which agent asked. Members are told when a question came through an AI tool.
PennyForYours lets you ask verified real people quick questions and get structured answers back, each with a signed receipt.
Use it when a human judgement beats a model: first impressions of an image or text, which of 2–3 options people prefer, whether something reads clearly, or a 1–5 rating with a reason.
Typical flow: list_question_types → get_quote (optional) → ask_question → get_results with cursor and wait_seconds until state is "closed" → rate_answers. Use get_receipt and verify_receipt to prove an answer came from a verified person.
Ask neutral questions: do not lead, do not reveal which option you prefer, keep the prompt under 280 characters.
Every call is made under an operator API key whose owner is accountable for it. Members are told when a question came through an AI tool. Rules for agents: https://pennyforyours.com/agents/
Sandbox mode: during early access only pfy_test_ keys are issued. Sandbox questions are answered by synthetic test members within a few minutes, every answer is marked synthetic, and nothing reaches real people.
Priced per answer by answer type. Launch example: a quick take (pick or rate plus one line of why, about 30 seconds) from 30p a person, plus 50p a question for screening and the summary. Free during early access: nothing is charged.
Pools of 5 to 50 people. Text or an image (PNG, JPEG or WebP); links and URL questions are refused.
Not for regulated advice (medical, legal, financial), collecting personal data, or writing reviews.
Sources
Each client's steps were checked against its own documentation on 5 October 2026.
| Client | What it confirms |
|---|---|
| Claude Code | claude mcp add, --header, scopes; ${VAR} expansion in .mcp.json headers |
| Claude custom connectors | Customize > Connectors path; "No sign in"; request headers stored encrypted; Free plan limited to one custom connector |
| mcp-remote | --header with an env value, because some clients do not escape spaces in args; MIT licence |
| ChatGPT developer mode | OAuth, No Authentication and Mixed Authentication only |
| ChatGPT app authentication | cannot present custom API keys |
| GPT Actions | 45-second round trip for API calls |
| Responses API | MCP tool headers field |
| Agents SDK | MCPServerStreamableHttp params with headers and timeout |
| Codex CLI | url and bearer_token_env_var; codex mcp add --bearer-token-env-var |
| Cursor | url, headers and ${env:NAME} |
| VS Code | .vscode/mcp.json marked deprecated in favour of portable destinations; inputs with password: true |
| Gemini CLI | gemini mcp add --transport http --header; httpUrl in settings.json |
| Zed | context_servers with url and headers |
| Legacy Cascade in Devin Desktop | serverUrl and headers; ${env:VAR}. New Devin Local tabs use Devin CLI configuration |
| MCP specification | current revision 2026-07-28 |