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 /mcp with Authorization: Bearer <agent key>, or
  • POST /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.