Quickstart
One curl, zero wallet, zero credentials — enough to confirm the MCP is alive and see what it can do:
curl -s -X POST https://cymstudio.app/api/mcp/rewards \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
| jq '.result.tools[].name'Any MCP client can talk to the endpoint directly — it implements JSON-RPC 2.0 over HTTPS with the standard tools/list + tools/call handshake. See Client configs for Claude Desktop and OpenAI-compatible snippets.
Endpoints
| Path | Method | Purpose |
|---|---|---|
| /api/mcp/rewards | POST | JSON-RPC 2.0: initialize, ping, tools/list, tools/call, resources/list. |
| /api/mcp/rewards | GET | Human-readable metadata (name, version, protocol, tool names). |
| /.well-known/gift-cards/agent-registration.json | GET | ERC-8004 registration document. Pin this URL in your agent. |
| /api/purchase | POST | Underlying x402 endpoint the purchase tools call. Agents may use it directly with an x-payment header. |
Protocol
Every request is a JSON-RPC 2.0 POST body:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": { "name": "search_giftcards", "arguments": { "brand": "Starbucks" } },
"id": 1
}{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": "Found 1 gift card.\n\n[{...}]" }],
"isError": false
}
}Tool outputs are always a single content[0].text string. Structured results (product lists, quote objects, order details) are embedded as JSON inside that string — parse from the first { or [.
Tool catalogue
All 12 tools exposed at tools/list:
| Tool | Description |
|---|---|
| search_giftcards | Filter 300+ brands by brand / country / currency. |
| get_brand_details | Denominations, restrictions, terms, validity for one product. |
| list_countries | Countries with available products (US, CA, HK). |
| list_currencies | Currencies with available products right now. |
| search_mastercard | Prepaid Mastercard products (USD, CAD). |
| get_mastercard_details | Detail for one Mastercard product. |
| check_order_status | Poll an order by order_id + email. Returns voucher when delivered. |
| redirect_to_checkout | Build a pre-filled /catalogue URL for browser-wallet fallback. |
| verify_email_start | Send a 6-digit OTP to an email address (required once per 30 days). |
| verify_email_complete | Submit the 6-digit OTP to mark the email verified. |
| get_purchase_quote | Step 1 of purchase: returns x402 payment requirements (amount, facilitator, EIP-712 domain, types, nonce). |
| submit_purchase | Step 2 of purchase: accepts a signed x402 envelope, settles on-chain, procures voucher. |
Call tools/list at runtime for full JSON Schema (argument names, types, required fields). Nothing here is hard-coded on the agent side — the server is the source of truth.
Purchase flow
Five calls from cold start to voucher in hand:
1. verify_email_start { email } → OTP sent
2. verify_email_complete { email, code } → email verified (30 days)
3. get_purchase_quote { product_id, denomination, → x402 payment requirements
email, network } + EIP-712 domain + types
+ suggested authorization
4. [ agent wallet signs TransferWithAuthorization ]
5. submit_purchase { product_id, denomination, → order_id + payment_tx
email, x_payment } ( + voucher if synchronous )
6. check_order_status { order_id, email } → voucher.code, voucher.pinEIP-3009 signing
get_purchase_quote returns everything you need to build the typed-data. Fields:
{
"correlation": { "product_id": 14000003689, "denomination": 25, "email": "...", "network": "conflux" },
"payment_requirements": {
"scheme": "exact",
"x402_version": 1,
"network": "conflux",
"chain_id": 1030,
"token": "0xaf37e8b6c9ed7f6318979f56fc287d76c30847ff",
"pay_to": "0xc10561c1c0d718b3d362df9d510a1b4e4331a4ee",
"amount": "25380000", // 25.38 USDT0 in raw 6-decimal units (incl. 1.5% fee)
"original_price": "25",
"original_currency": "USD"
},
"eip712_domain": {
"name": "USDT0",
"version": "1",
"chainId": 1030,
"verifyingContract": "0xaf37e8b6c9ed7f6318979f56fc287d76c30847ff"
},
"eip712_types": {
"TransferWithAuthorization": [
{ "name": "from", "type": "address" },
{ "name": "to", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "validAfter", "type": "uint256" },
{ "name": "validBefore", "type": "uint256" },
{ "name": "nonce", "type": "bytes32" }
]
},
"suggested_authorization": {
"from": "YOUR_WALLET_ADDRESS",
"to": "0xc10561c1c0d718b3d362df9d510a1b4e4331a4ee",
"value": "25380000",
"validAfter": 0,
"validBefore": 1800000000,
"nonce": "0xabcd...32 random bytes..."
}
}Sign the TransferWithAuthorization struct with the agent's wallet key, then base64-encode the full envelope and pass it as x_payment:
{
"x402Version": 1,
"scheme": "exact",
"network": "conflux",
"payload": {
"signature": "0x...", // 65-byte ECDSA result
"authorization": {
"from": "0xagent...",
"to": "0xc10561...",
"value": "25380000",
"validAfter": 0,
"validBefore": 1800000000,
"nonce": "0xabcd..."
}
}
}Reference signing snippet with viem:
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`)
const signature = await account.signTypedData({
domain: quote.eip712_domain,
types: quote.eip712_types,
primaryType: 'TransferWithAuthorization',
message: {
...quote.suggested_authorization,
from: account.address,
},
})
const envelope = {
x402Version: 1,
scheme: 'exact',
network: quote.payment_requirements.network,
payload: {
signature,
authorization: { ...quote.suggested_authorization, from: account.address },
},
}
const xPayment = Buffer.from(JSON.stringify(envelope)).toString('base64')Client configs
Claude Desktop
Claude Desktop speaks MCP over stdio, so use mcp-remote to bridge to the HTTP endpoint. Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"cym-rewards": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://cymstudio.app/api/mcp/rewards"]
}
}
}Anthropic / OpenAI tool-use loops
Define the 12 tools manually using the schemas from tools/list, or dispatch each model-requested tool by POSTing to /api/mcp/rewards. Our own /chat page uses the latter pattern with Kimi — see app/api/chat/route.ts for a 150-line reference implementation.
Bare HTTP
Nothing is MCP-client-specific — just POST JSON-RPC:
import requests
def mcp_call(name, args=None):
r = requests.post("https://cymstudio.app/api/mcp/rewards", json={
"jsonrpc": "2.0", "id": 1,
"method": "tools/call",
"params": { "name": name, "arguments": args or {} },
})
return r.json()["result"]["content"][0]["text"]
print(mcp_call("list_countries"))Errors & rate limits
JSON-RPC errors follow the standard codes:
-32700parse error — body was not valid JSON-32600invalid request — missingjsonrpc: "2.0"ormethod-32601method not found — unknown JSON-RPC method-32602invalid params — unknown tool name
Tool-level errors (invalid input, Supabase failure, OTP mismatch, etc.) return HTTP 200 with result.isError: true and a text message — standard MCP pattern.
Rate limits: IP-based sliding-window on every API route (see middleware.ts). /api/purchase adds a per-wallet 10-second cooldown and $1–$5,000 order-value bounds. /api/email/* has its own envelope rate limits.
Discovery (ERC-8004)
The catalogue is registered on the ERC-8004 agent registry at 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 on Ethereum mainnet as agent ID 22628. The registration document lives at:
GET https://cymstudio.app/.well-known/gift-cards/agent-registration.json
Agents that discover via ERC-8004 read the services array from that JSON, pick the MCP entry, and connect to its endpoint.
Support
Bug reports, feature requests, and integration questions:
- GitHub: intrepidcanadian/cymstudios
- Email: tony.lau@cymadvisory.com