PinMarketDocs

PinMarket MCP server

Let an AI agent buy gift cards, mobile top-ups and eSIMs with USDC on Solana, using its own wallet.

Connect

Claude Code

claude mcp add --transport http pinmarket https://pinmarket.fun/mcp

Cursor / 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 code
  1. search_products (or list_filters first) finds a product_id.
  2. get_product lists the denominations. Pick a package_id.
  3. buy_gift_card with your wallet's public key. You get order_token and transaction_base64, an unsigned Solana transaction. The server has already checked that the wallet holds enough USDC and SOL.
  4. Sign transaction_base64 with that wallet's key. Then either broadcast it yourself, or pass the signed base64 to submit_signed_transaction and PinMarket broadcasts it.
  5. wait_for_code waits for the payment to confirm and the card to be bought, then returns the code. If it returns done: 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 signature

JavaScript / 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

Security notes

Support

support@pinmarket.fun. Include the order_id.