PinMarket MCP server
Let an AI agent buy gift cards, mobile top-ups and eSIMs with USDC on Solana, using its own wallet.
- Endpoint:
https://pinmarket.fun/mcp - Transport: MCP Streamable HTTP. Send JSON-RPC 2.0 with
POST; responses are plainapplication/json. Stateless: no sessions, no event stream (GETreturns 405, as the spec allows). - Protocol versions:
2025-06-18,2025-03-26,2024-11-05 - Auth: none. Searching and quoting are public. Paying needs only a Solana wallet with USDC.
- Custody: your private key never leaves your agent. The server returns an unsigned transaction and only ever broadcasts one that is exactly the transaction it issued.
- Network: Solana mainnet. Currency: USDC (
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v), 6 decimals. Listed prices already include every fee.
Connect
Claude Code
claude mcp add --transport http pinmarket https://pinmarket.fun/mcpCursor / VS Code / other clients that accept a URL
{ "mcpServers": { "pinmarket": { "url": "https://pinmarket.fun/mcp" } } }Claude Desktop (add as a custom connector with the URL above, or bridge it with mcp-remote)
{ "mcpServers": { "pinmarket": { "command": "npx", "args": ["-y", "mcp-remote", "https://pinmarket.fun/mcp"] } } }Your own agent can call it with plain HTTP, see Raw JSON-RPC.
The purchase flow
search_products ─▶ get_product ─▶ buy_gift_card ─▶ (agent signs) ─▶ submit_signed_transaction ─▶ wait_for_code
find a card read prices order + unsigned tx your wallet optional broadcast helper returns the codesearch_products(orlist_filtersfirst) finds aproduct_id.get_productlists the denominations. Pick apackage_id.buy_gift_cardwith your wallet's public key. You getorder_tokenandtransaction_base64, an unsigned Solana transaction. The server has already checked that the wallet holds enough USDC and SOL.- Sign
transaction_base64with that wallet's key. Then either broadcast it yourself, or pass the signed base64 tosubmit_signed_transactionand PinMarket broadcasts it. wait_for_codewaits for the payment to confirm and the card to be bought, then returns thecode. If it returnsdone: false, call it again.
Nothing is charged until the signed transaction lands on-chain. The order_token is a secret: it is the only handle to the order and its code. Anyone holding it can read the code.
Timing: the quote is valid for 15 minutes. The transaction's blockhash lasts about 60 seconds, so sign promptly; if it expires, refresh_transaction gives you a fresh transaction for the same order and price.
Tools
All tools return both structuredContent (JSON) and a text copy of the same JSON. Errors have isError: true and { "error": { "code", "message", ... } }.
search_products
Search the catalogue. Every word of query must match the brand, name, country or category.
| Argument | Type | Notes |
|---|---|---|
query |
string, optional | e.g. "steam brazil", "netflix" |
country |
string, optional | ISO code, e.g. US, TR |
category |
string, optional | id from list_filters, e.g. games |
limit |
integer 1–25, optional | default 10 |
offset |
integer, optional | pagination |
Returns { total, offset, next_offset, products: [{ product_id, name, brand, country_code, country, currency, categories, from_price_usdc }] }.
list_filters
No arguments. Returns every countries: [{ code, name, products }] and categories: [{ id, products }].
get_product
| Argument | Type |
|---|---|
product_id |
string, required |
Returns { product_id, name, country, currency, in_stock, requires_phone_number, terms, packages: [{ package_id, value, currency, price_usdc }], purchasable }. value is the card's face value in currency; price_usdc is what you pay.
get_shop_status
No arguments. { open, reason? }. The shop closes automatically when it is restocking.
buy_gift_card
| Argument | Type | Notes |
|---|---|---|
product_id |
string, required | from search_products |
package_id |
string, required | from get_product |
payer_wallet |
string, required | public key (base58) of the wallet that will sign and pay |
phone_number |
string | only for top-up products (requires_phone_number), international format |
Returns:
{
"order_id": "3f9c2a71-…",
"order_token": "…secret…",
"status": "awaiting_payment",
"product": "Steam Brazil", "value": "55",
"amount_usdc": "11.020000",
"quote_expires_at": "2026-10-07T12:15:00.000Z",
"network": "solana-mainnet",
"transaction_base64": "AQAAAAAA…",
"message_base64": "AQABB…",
"recent_blockhash": "…", "last_valid_block_height": 123456789,
"valid_for_seconds": 60,
"next_step": "…"
}transaction_base64 is a legacy wire-format transaction with an empty signature slot. It contains three instructions: compute-unit limit, compute-unit price (about 600 lamports of priority fee), and one SPL transferChecked of exactly amount_usdc USDC from your token account to PinMarket's, carrying a unique read-only reference account that identifies the order. You are the only signer and the fee payer.
refresh_transaction
| Argument | Type |
|---|---|
order_token |
string, required |
A fresh unsigned transaction (new blockhash) for the same unpaid order. Same amount, same order. Only the newest transaction can be submitted through submit_signed_transaction.
submit_signed_transaction
Optional: use it when your signer cannot broadcast.
| Argument | Type |
|---|---|
order_token |
string, required |
signed_transaction_base64 |
string, required: the same transaction bytes with your signature filled in |
The server accepts the transaction only if its message is byte-for-byte the one it issued and the payer's signature verifies. Anything else is rejected (invalid_transaction). Returns { signature, explorer }.
get_order
| Argument | Type |
|---|---|
order_token |
string, required |
Current state, immediately, with the code if delivered.
wait_for_code
| Argument | Type | Notes |
|---|---|---|
order_token |
string, required | |
timeout_seconds |
integer 1–45 | default 30 |
Waits for the payment to confirm on-chain and the card to be delivered. Returns the order state; when status is delivered it includes:
{
"status": "delivered", "done": true,
"code": "A7QK-9TZP-3WLM-X2VD",
"fields": [{ "key": "code", "value": "A7QK-9TZP-3WLM-X2VD" }],
"redemption_instructions": "Go to store.steampowered.com …",
"raw": "code: A7QK-…"
}Order status values: awaiting_payment → paid → fulfilling → delivered. Side states: awaiting_balance and failed (delivery delayed: contact support, do not pay again) and expired (the quote ran out before payment).
Signing the transaction
You only need to put a valid ed25519 signature over the message into the signature slot. The wire format is:
[ 0x01 ][ 64-byte signature ][ message … ] message_base64 = the bytes after the signatureJavaScript / TypeScript (@solana/web3.js)
import { Keypair, VersionedTransaction, Connection } from "@solana/web3.js";
const wallet = Keypair.fromSecretKey(/* your agent's key */);
const tx = VersionedTransaction.deserialize(Buffer.from(transaction_base64, "base64"));
tx.sign([wallet]);
// A) broadcast yourself
const sig = await new Connection(RPC_URL).sendRawTransaction(tx.serialize());
// B) or let PinMarket broadcast: pass the signed bytes to submit_signed_transaction
const signed_transaction_base64 = Buffer.from(tx.serialize()).toString("base64");Python (solders)
import base64
from solders.keypair import Keypair
from solders.transaction import VersionedTransaction
wallet = Keypair.from_bytes(SECRET_KEY_BYTES)
tx = VersionedTransaction.from_bytes(base64.b64decode(transaction_base64))
signed = VersionedTransaction(tx.message, [wallet])
signed_transaction_base64 = base64.b64encode(bytes(signed)).decode()Wallets that only sign messages (MPC, HSM, custody APIs)
Sign the raw bytes of message_base64 with ed25519, then build the transaction yourself: 0x01 + signature (64 bytes) + message. Base64-encode it and send it via submit_signed_transaction.
Before you sign, check
An agent should not sign blindly. Decoding the transaction shows: the fee payer is your wallet; the only value-moving instruction is one SPL transferChecked of USDC (mint EPjFWdd5…Dt1v, 6 decimals) whose amount equals amount_usdc. If anything differs, do not sign.
Raw JSON-RPC
# initialise (optional for a stateless server, but clients normally do it)
curl -s https://pinmarket.fun/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}'
# list the tools
curl -s https://pinmarket.fun/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# search
curl -s https://pinmarket.fun/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_products","arguments":{"query":"steam brazil","limit":3}}}'
# buy (returns the unsigned transaction)
curl -s https://pinmarket.fun/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"buy_gift_card","arguments":{"product_id":"steam-brazil","package_id":"…","payer_wallet":"YourWalletPubkey"}}}'Notifications (messages without an id) get an empty 202. Batches (arrays of up to 20 messages) are supported.
Errors
error.code |
Meaning | What to do |
|---|---|---|
invalid_arguments |
An argument is missing or malformed | Fix the argument named in message |
shop_paused |
The shop is restocking or in maintenance | Wait; check get_shop_status |
product_not_found, package_unavailable |
Unknown or out-of-stock item | Search again |
phone_required |
Top-up product needs phone_number |
Add it |
invalid_wallet |
payer_wallet is not a valid public key |
Fix it |
insufficient_usdc |
No single token account holds enough USDC | Fund the wallet; have and need are returned |
insufficient_sol |
Not enough SOL (~0.00001) for the network fee | Add a little SOL |
too_many_open_orders |
3 unpaid orders for this wallet | Pay or wait for them to expire |
rate_limited |
Too many requests | Back off for a minute |
invalid_transaction |
submit_signed_transaction got something other than the issued transaction |
reason says why: not_signed, message_mismatch, bad_signature |
blockhash_expired |
The transaction waited too long | refresh_transaction, sign again |
order_not_found |
Wrong order_token |
Use the one from buy_gift_card |
order_expired, not_payable |
The 15-minute quote ended, or the order is already paid | Create a new order, unless you already paid |
no_transaction, no_payer |
The order has no issued transaction | Call refresh_transaction, or create a new order |
insufficient_funds |
The wallet lost the USDC or SOL between building and sending | Top up, then refresh_transaction |
broadcast_failed |
Solana rejected the transaction (detail has the reason) |
Retry, or broadcast it yourself |
shop_unavailable, internal_error |
A problem on our side | Retry shortly; if it persists, write to support |
rpc_unavailable |
A Solana RPC call failed | Retry shortly |
Limits and behaviour
- Quotes last 15 minutes; transactions about 60 seconds.
- Rate limits are per IP: reads 120/min, orders 12/hour, refreshes 60/hour, submits 40/hour.
wait_for_codewaits up to 45 seconds per call. - Only fixed-denomination products are supported. Custom-amount products show
purchasable: false. - No email is needed or sent for agent orders. The code is returned by
wait_for_code/get_orderonly. Keep theorder_token; if you lose it, you can still see the purchase by signing in with the paying wallet at https://pinmarket.fun/#/codes. - Payment detection is automatic: it finds your transfer by its unique reference account, so it does not matter whether you broadcast it yourself or via
submit_signed_transaction. - If you pay the right amount but delivery is delayed, the order moves to
awaiting_balanceorfailedand stays on record. Contact support@pinmarket.fun with theorder_id. Do not pay twice.
Security notes
- The server never asks for, receives, or stores a private key.
submit_signed_transactionis not a generic relay: it refuses any transaction that is not exactly the one issued for that order, so it cannot be used to broadcast anything else.- Treat
order_tokenlike a password. It is not recoverable from the order id. - The reference account is read-only and unique per order; it can only ever credit this order.
Support
support@pinmarket.fun. Include the order_id.