Connect via MCP
Sandbox live. Create a free sandbox workspace at agentic-sandbox.pinkwallet.com (test credentials only; no money moves). Production is not yet available.
Pink Agentic AI Payments (by PinkWallet, early access) is the approval layer between AI agents and company money: plain-language rules, per-agent budgets and human approvals decide each payment before a single-use card or bank transfer is issued.
What the MCP server is
One Cloudflare Worker, Streamable HTTP, stateless, JSON responses (no SSE streaming needed for these tools). It lives at:
POST /mcpwithAuthorization: Bearer <agent key>, orPOST /mcp/<agent_key>— the key in the URL path, no header required.
Every workspace has its own agent keys, shaped pk_sandbox_agent_<workspace_id>_<agent_id>_<random> (admin keys: pk_sandbox_admin_<workspace_id>_<random>). You get both when you create a sandbox workspace — see Quickstart.
The 7 tools
These are the exact tool names, confirmed by calling tools/list against a running sandbox instance:
| Tool | Input | What it does |
|---|---|---|
pink.get_budget |
(none) | Monthly budget left, spent today, single-payment cap, vault balance, company daily ceiling. |
pink.list_payees |
(none) | Approved payees this workspace can pay. Paying anyone else triggers the "new payee" rule. |
pink.list_rules |
(none) | The policy rules that apply to this agent, in evaluation order. First match decides. |
pink.check_policy |
amount, purpose, + optional payee_id/payee_name, currency, reason, evidence, evidence_flags, local_hour |
Dry run. Returns would_allow / would_ask / would_block plus the full decision trace. Nothing is spent or held. |
pink.request_payment |
same as check_policy, plus optional idempotency_key |
The real call. Returns allowed (credential attached), pending_human (a hold_id), or blocked. |
pink.get_credential |
hold_id |
Poll a pending hold. Once a person approves, this returns the credential. Also works with any payment_id. |
pink.report_receipt |
payment_id, receipt (object, merchant/total/currency/reference/url/note, extra fields allowed) |
File the receipt after paying. Raises the agent's trust score by 1 (verified: 95 → 96). |
amount is a positive number, currency defaults to USD and must be one of USD EUR GBP HKD SGD JPY. evidence_flags accepts: po sow ticket scan statement brief renewal dupInvoice payeeChanged repeatCustomer deposit.
The server's own MCP instructions field (returned at connect time, verbatim from the server):
"You are connected to Pink Agentic AI Payments (sandbox) as the agent "Purchasing AI". You never hold money or card numbers. To pay for something: 1) optionally call pink.check_policy to see whether a payment like this would clear; 2) call pink.request_payment with the payee, amount, purpose and any evidence; 3) if the decision is "pending_human", poll pink.get_credential(hold_id) until a person approves or declines; 4) use the returned single-use credential; 5) call pink.report_receipt with the receipt. Use pink.list_payees to find approved payees and pink.get_budget to see what you have left."
Call sequence, with real responses
All calls below ran against $PINK_BASE/mcp/$PURCH where $PURCH is a Purchasing AI agent key from a freshly created "coffee" workspace.
1. Check the budget
curl -s -X POST "$PINK_BASE/mcp/$PURCH" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"pink.get_budget","arguments":{}}}'
Real response (structuredContent, unwrapped):
{
"agent": "Purchasing AI",
"status": "active",
"monthly_budget": 4000,
"spent_this_month": 2260,
"left_this_month": 1740,
"spent_today": 0,
"single_payment_cap": 500,
"vault": { "id": "v_ops", "name": "Operating", "balances": { "USD": 18420.55 } },
"company_daily_ceiling": 6000,
"company_spent_today": 0,
"policy_version": 1
}
2. Dry-run with check_policy
curl -s -X POST "$PINK_BASE/mcp/$PURCH" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"pink.check_policy","arguments":{"payee_id":"p_cc","amount":420,"purpose":"20 kg espresso beans"}}}'
Real response:
{
"decision": "would_allow",
"reason": "Small supply orders go through",
"rule": { "id": "r4", "name": "Small supply orders go through", "agents": ["a_purch","a_inv"], "payee": "approved", "amount": { "min": 0, "max": 500, "window": "tx" }, "action": "allow" },
"policy_version": 1,
"trace": [
"PASS · Agent registered · Purchasing AI",
"PASS · Agent active · not paused",
"PASS · Monthly budget · $2,680 of $4,000 after this",
"PASS · Daily ceiling, all agents · $420 of $6,000",
"PASS · Vault balance · Operating · $18,420.55 available",
"SKIP · Never: gift cards, cash-like, crypto · payee out of scope",
"SKIP · Between 11pm and 6am: ask the owner · inside business hours (06:00–23:00)",
"SKIP · Payee changed bank details: ask the owner first · no payee bank details changed in the last 7 days",
"SKIP · Payroll runs on schedule · different agent",
"PASS · Small supply orders go through · matched · allow"
]
}
decision is one of would_allow / would_ask / would_block — these are literally the strings; check_policy and request_payment do not share a decision vocabulary (allowed/pending_human/blocked, next section). Both are real fields from the same underlying rule engine — don't conflate them.
3. Request the payment — allowed
curl -s -X POST "$PINK_BASE/mcp/$PURCH" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"pink.request_payment","arguments":{"payee_id":"p_cc","amount":420,"purpose":"20 kg espresso beans","reason":"bin at 18%","idempotency_key":"docs-demo-1"}}}'
Real response:
{
"payment_id": "pay_ed04c795a07c",
"decision": "allowed",
"agent": "Purchasing AI",
"payee": "Counter Culture Coffee",
"payee_id": "p_cc",
"amount": 420,
"currency": "USD",
"purpose": "20 kg espresso beans",
"rule": "Small supply orders go through",
"policy_version": 1,
"created_at": "2026-09-30T21:06:39.283Z",
"credential": {
"type": "bank_transfer",
"sandbox": true,
"max_amount": 420,
"currency": "USD",
"locked_to": "Counter Culture Coffee",
"single_use": true,
"expires_at": "2026-09-30T21:21:39.283Z",
"transfer": { "rail": "ACH", "reference": "PWS-4E2DD4F5", "status": "submitted" }
}
}
If the payee is card-paid instead of bank, credential.type is "virtual_card" with a card: { pan, exp, cvv, name } block instead of transfer — real sandbox response: pan: "4111 1111 9718 9706", always a 4111 1111 test-range BIN, a sandbox test value, never a real card. A platform-refund payee (method: "platform") returns { type: "platform_refund", platform: { name, reference } }.
Calling request_payment again with the same idempotency_key returns the original payment instead of paying twice, with "replayed": true added to the response — confirmed by an automated test run against the sandbox, the same behavior we relied on above.
4. Request the payment — pending_human, then poll and approve
curl -s -X POST "$PINK_BASE/mcp/$PURCH" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"pink.request_payment","arguments":{"payee_id":"p_sysco","amount":890,"purpose":"weekly milk and syrups","reason":"standing order"}}}'
Real response:
{
"payment_id": "pay_c4821e4c4f85",
"decision": "pending_human",
"agent": "Purchasing AI",
"payee": "Sysco",
"amount": 890,
"currency": "USD",
"purpose": "weekly milk and syrups",
"rule": "Bigger supply orders: store manager checks",
"policy_version": 1,
"hold_id": "pay_c4821e4c4f85",
"approvers_needed": 1,
"approvals_so_far": 0,
"who": "Luis Ortega (Store manager)",
"expires_at": "2026-09-30T21:36:48.994Z",
"poll": "pink.get_credential(hold_id) · or GET /v1/payments/{id}"
}
hold_id and payment_id are the same string — get_credential and report_receipt both accept it. Holds expire 30 minutes after creation; polling one after expiry flips decision to "expired".
Polling before a person acts returns the same pending_human object. An admin then approves it (see Connect via REST for the admin REST call), and polling again returns:
{
"payment_id": "pay_c4821e4c4f85",
"decision": "approved",
"credential": { "type": "virtual_card", "sandbox": true, "max_amount": 890, "currency": "USD", "locked_to": "Sysco", "single_use": true, "expires_at": "2026-09-30T21:21:49.073Z", "card": { "pan": "4111 1111 7481 6092", "exp": "12/27", "cvv": "922", "name": "PINK SANDBOX" } },
"approved_by": ["Sandbox admin"]
}
Note: once approved, decision becomes "approved" (not "allowed" — that string is reserved for payments that never needed a human). A declined hold returns "decision": "declined" with credential: null and declined_by: [...].
5. Blocked
curl -s -X POST "$PINK_BASE/mcp/$PURCH" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"pink.request_payment","arguments":{"payee_name":"giftcards.com","amount":50,"purpose":"prizes"}}}'
Real response:
{
"payment_id": "pay_32816570b6e1",
"decision": "blocked",
"payee": "giftcards.com",
"payee_id": null,
"amount": 50,
"rule": "Never: gift cards, cash-like, crypto",
"credential": null,
"retry_after": null,
"why": "matched · block"
}
why is the field name returned on a block — it is not called reason at this layer (reason is the field name on check_policy's response, a different tool). retry_after is always null in this sandbox build — there is no documented automatic retry timer.
6. File the receipt
curl -s -X POST "$PINK_BASE/mcp/$PURCH" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"pink.report_receipt","arguments":{"payment_id":"pay_ed04c795a07c","receipt":{"merchant":"Counter Culture Coffee","total":420,"reference":"INV-1"}}}}'
Real response: { "ok": true, "payment_id": "pay_ed04c795a07c", "trust_score": 96 } — the agent's trust score (seeded at 95 in every template) rose by exactly 1.
Authentication
No OAuth in this sandbox build. Two equivalent forms, both tested:
Authorization: Bearer <agent_key> # POST /mcp
# — or —
POST /mcp/<agent_key> # key in the URL path, no header
A request with no valid key returns HTTP 401 with a JSON-RPC error body:
{"jsonrpc":"2.0","error":{"code":-32001,"message":"Unauthorized: use an agent key as Bearer token or in the URL /mcp/<agent_key>"},"id":null}
(Confirmed: WWW-Authenticate: Bearer realm="pink-agentic-sandbox" header is also present.)
Client setup
Verified against each client's own current documentation, and, where the tool runs headlessly, against the running sandbox above.
Claude Code
Tested on 2026-09-30 with claude mcp add --transport http: the server connects and lists all 7 tools.
claude mcp add --transport http pink "$PINK_BASE/mcp" \
--header "Authorization: Bearer $PINK_AGENT_KEY"
Short flags also work: claude mcp add -t http -H "Authorization: Bearer $PINK_AGENT_KEY" pink "$PINK_BASE/mcp". Source: code.claude.com/docs/en/mcp.
Claude Desktop / claude.ai (custom connectors)
Claude Desktop’s custom connectors (Settings → Connectors) are set up through an OAuth sign-in flow (or an advanced OAuth Client ID/Secret), and there is no documented field for a static API key or Authorization header; claude_desktop_config.json is documented for local (stdio) servers (Anthropic support: custom connectors).
This sandbox has no OAuth, so the documented route is the mcp-remote bridge: a local stdio process that speaks Streamable HTTP with your header to the real server.
{
"mcpServers": {
"pink": {
"command": "npx",
"args": ["mcp-remote", "https://agentic-sandbox.pinkwallet.com/mcp", "--header", "Authorization:${PINK_AUTH_HEADER}"],
"env": { "PINK_AUTH_HEADER": "Bearer <YOUR_SANDBOX_AGENT_KEY>" }
}
}
}
The colon-with-no-space and the env-var split are mcp-remote's own documented workaround for an arg-escaping bug on some platforms (source: mcp-remote README on GitHub).
Cursor
Config shape per Cursor's own docs (cursor.com/docs/context/mcp), which natively supports url + headers:
{
"mcpServers": {
"pink": {
"url": "https://agentic-sandbox.pinkwallet.com/mcp",
"headers": { "Authorization": "Bearer ${env:PINK_AGENT_KEY}" }
}
}
}
VS Code (GitHub Copilot agent mode)
Config shape per VS Code's own docs (code.visualstudio.com/docs/agents/reference/mcp-configuration):
{
"servers": {
"pink": { "type": "http", "url": "https://agentic-sandbox.pinkwallet.com/mcp", "headers": { "Authorization": "Bearer ${input:pink_agent_key}" } }
},
"inputs": [
{ "type": "promptString", "id": "pink_agent_key", "description": "Pink sandbox agent key", "password": true }
]
}
OpenAI Agents SDK (Python)
Not yet tested end-to-end by us. Config shape per the SDK's own docs (openai.github.io/openai-agents-python/mcp/), which takes a headers dict on MCPServerStreamableHttp:
from agents.mcp import MCPServerStreamableHttp
pink = MCPServerStreamableHttp(
name="Pink",
params={"url": "https://agentic-sandbox.pinkwallet.com/mcp", "headers": {"Authorization": f"Bearer {agent_key}"}},
)
LangChain / LangGraph (langchain-mcp-adapters)
Not yet tested end-to-end by us. Config shape per the adapters' own README on GitHub, which takes a headers dict on an "http"-transport entry:
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"pink": {"transport": "http", "url": "https://agentic-sandbox.pinkwallet.com/mcp", "headers": {"Authorization": f"Bearer {agent_key}"}}
})
tools = await client.get_tools()
MCP TypeScript SDK (the reference client)
This is the client used for every tools/call sample on this page:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const mcp = new Client({ name: 'my-agent', version: '0.0.1' });
await mcp.connect(new StreamableHTTPClientTransport(new URL(`${PINK_BASE}/mcp/${agentKey}`)));
const budget = JSON.parse((await mcp.callTool({ name: 'pink.get_budget', arguments: {} })).content[0].text);
Ran against the sandbox: all 19 assertions in the automated test suite passed.
Python mcp package
The official Python mcp package (Python ≥ 3.10) documents the client pattern (streamablehttp_client + ClientSession) in its README. We haven’t published a tested Python sample yet; the curl and TypeScript samples above are the tested reference.






