MCP server that lets AI agents discover, pay for, and execute SKILL.md endpoints via the x402 protocol
@402md/mcp
MCP server that transforms SKILL.md files into executable tools for AI agents. Point to any skill — URL, local file, or marketplace name — and the server parses it, auto-pays via x402, and returns the result.
Table of Contents
- Quick Start
- Networks
- Wallet Setup
- How Payments Work
- Claude Desktop Configuration
- Environment Variables
- Wallet File
- Budget & Spending Limits
- Tools
- Modes
- Skill Resolution
- Architecture
- Examples
- Development
- License
Quick Start
npx @402md/mcp
Or install globally:
npm install -g @402md/mcp
402md-mcp
The server starts in read-only mode by default. You can browse and inspect skills immediately. To execute paid endpoints, configure a wallet (see Wallet Setup).
Networks
The server supports four networks across two blockchain ecosystems:
Stellar
| Network | ID | Use Case | USDC Contract |
|---------|-----|----------|---------------|
| Stellar Mainnet | stellar | Production payments with real USDC | Native Stellar USDC (Centre) |
| Stellar Testnet | stellar-testnet | Development & testing with free testnet USDC | Testnet USDC |
Stellar is the default and preferred network. It offers sub-second finality, near-zero fees (~0.00001 XLM per tx), and native USDC support.
- Testnet faucet: Use Stellar Laboratory to create and fund testnet accounts.
- Mainnet: Fund your account via any Stellar DEX, exchange, or on-ramp that supports USDC on Stellar.
EVM (Base)
| Network | ID | Use Case | USDC Contract |
|---------|-----|----------|---------------|
| Base Mainnet | base | Production payments on Base L2 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Base Sepolia | base-sepolia | Development & testing on Base testnet | Sepolia USDC |
Base is an Ethereum L2 with low gas fees and fast confirmations.
- Sepolia faucet: Get testnet ETH from Alchemy Faucet or Coinbase Faucet (needed for gas). Then bridge or mint testnet USDC.
- Mainnet: Bridge USDC to Base from Ethereum, or buy directly on Base via Coinbase or any supported on-ramp.
Choosing a Network
- Just getting started? Use
stellar-testnet— no real money, instant setup withcreate_wallet. - Testing EVM skills? Use
base-sepolia— free testnet, good for EVM-specific endpoints. - Production? Use
stellar(lower fees) orbasedepending on what the skill accepts.
The server automatically selects the best compatible network when calling a skill. If a skill supports multiple networks and your wallet has both keys configured, Stellar is preferred.
Wallet Setup
There are three ways to configure a wallet, listed by priority (highest first):
Option 1: Environment Variables (recommended for production)
# Stellar
export STELLAR_SECRET="SCZANGBA5YHTNYVVV3C7CAZMCLXPILHSE6PGYV2FHHUQ5DGQJWRZ4GXT"
export NETWORK="stellar-testnet"
# EVM (Base)
export EVM_PRIVATE_KEY="0x4c0883a69102937d6231471b5dbb6204fe512961708279f23efb3c0c90..."
export NETWORK="base-sepolia"
# Both (FULL mode)
export STELLAR_SECRET="S..."
export EVM_PRIVATE_KEY="0x..."
export NETWORK="stellar" # default network when both are available
Option 2: create_wallet Tool (recommended for development)
If no wallet is configured, ask your AI agent to use the create_wallet tool:
"Create a new wallet on stellar-testnet"
This generates a keypair and saves it to ~/.402md/wallet.json. The server reloads automatically.
Important: create_wallet refuses to run if a wallet is already configured. To replace an existing wallet, delete ~/.402md/wallet.json manually first.
Option 3: Wallet File (manual)
Create ~/.402md/wallet.json manually:
{
"stellarSecret": "SCZANGBA5YHTNYVVV3C7CAZMCLXPILHSE6PGYV2FHHUQ5DGQJWRZ4GXT",
"evmPrivateKey": "0x4c0883a69102937d6231471b5dbb6204fe512961708279f23efb3c0c90...",
"network": "stellar-testnet",
"createdAt": "2026-01-15T10:30:00.000Z"
}
The file is created with 0o600 permissions (owner read/write only). The directory ~/.402md/ is created with 0o700.
Generating Keys Manually
Stellar:
# Using stellar-sdk in Node.js
node -e "const { Keypair } = require('@stellar/stellar-sdk'); const kp = Keypair.random(); console.log('Secret:', kp.secret()); console.log('Public:', kp.publicKey())"
EVM:
# Using viem in Node.js
node -e "const { generatePrivateKey, privateKeyToAccount } = require('viem/accounts'); const pk = generatePrivateKey(); const acc = privateKeyToAccount(pk); console.log('Private Key:', pk); console.log('Address:', acc.address)"
# Or using openssl
openssl rand -hex 32 | sed 's/^/0x/'
How Payments Work
The payment flow is handled automatically by the x402 protocol:
Agent calls use_skill("my-skill", "/api/generate")
│
├─ 1. Resolve skill → parse SKILL.md manifest
├─ 2. Validate manifest (schema, required fields)
├─ 3. Check budget limits (per-call & daily)
├─ 4. Select compatible network (skill networks ∩ wallet networks)
├─ 5. Create PaymentClient for that network
├─ 6. client.fetch(url) → x402 auto-payment:
│ a. First request returns 402 Payment Required
│ b. Client signs USDC payment (on-chain)
│ c. Retries request with payment proof header
│ d. Server verifies payment, returns response
├─ 7. Record spending (amount, skill, endpoint, network)
└─ 8. Return response to agent
The agent never sees the payment mechanics — it just calls use_skill and gets a result. All USDC amounts use 6 decimal places (e.g., "0.050000").
What Happens If the Endpoint Fails?
If payment succeeds but the endpoint returns an error (4xx/5xx), the spending is still recorded (the payment was already made on-chain) but the tool returns isError: true with a clear message:
Endpoint returned 500. Payment was sent but the request failed.
{"error": "Internal server error"}
This lets the agent (or user) know to contact the skill provider.
Claude Desktop Configuration
Add to your claude_desktop_config.json:
Stellar Testnet (getting started)
{
"mcpServers": {
"402md": {
"command": "npx",
"args": ["-y", "@402md/mcp"],
"env": {
"STELLAR_SECRET": "SCZANGBA5YHTNYVVV3C7CAZMCLXPILHSE6PGYV2FHHUQ5DGQJWRZ4GXT",
"NETWORK": "stellar-testnet",
"MAX_PER_CALL": "0.10",
"MAX_PER_DAY": "5.00"
}
}
}
}
Base Sepolia (EVM testing)
{
"mcpServers": {
"402md": {
"command": "npx",
"args": ["-y", "@402md/mcp"],
"env": {
"EVM_PRIVATE_KEY": "0x4c0883a69102937d623147...",
"NETWORK": "base-sepolia",
"MAX_PER_CALL": "0.10",
"MAX_PER_DAY": "5.00"
}
}
}
}
Production (both networks)
{
"mcpServers": {
"402md": {
"command": "npx",
"args": ["-y", "@402md/mcp"],
"env": {
"STELLAR_SECRET": "S...",
"EVM_PRIVATE_KEY": "0x...",
"NETWORK": "stellar",
"MAX_PER_CALL": "1.00",
"MAX_PER_DAY": "50.00"
}
}
}
}
Read-only (no wallet)
{
"mcpServers": {
"402md": {
"command": "npx",
"args": ["-y", "@402md/mcp"]
}
}
}
Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| STELLAR_SECRET | — | Stellar secret key (starts with S). Enables Stellar payments. |
| EVM_PRIVATE_KEY | — | EVM private key (hex, starts with 0x). Enables Base payments. |
| NETWORK | stellar | Default network: stellar, stellar-testnet, base, base-sepolia |
| MAX_PER_CALL | 0.10 | Maximum USDC allowed per individual skill call |
| MAX_PER_DAY | 20.00 | Maximum USDC allowed per calendar day |
| REGISTRY_URL | https://api.402.md | 402.md marketplace API base URL |
Environment variables always take priority over the wallet file (~/.402md/wallet.json).
Wallet File
Located at ~/.402md/wallet.json. Created automatically by create_wallet or manually.
{
"stellarSecret": "S...",
"evmPrivateKey": "0x...",
"network": "stellar-testnet",
"createdAt": "2026-01-15T10:30:00.000Z"
}
- Permissions:
0o600(read/write owner only) - Directory:
~/.402md/with0o700 - Merge behavior:
saveWalletConfigmerges new fields with existing data, so adding an EVM key won't erase an existing Stellar key - Priority: env vars > wallet file > defaults
Budget & Spending Limits
The server enforces two spending limits:
| Limit | Default | Env Variable | Description |
|-------|---------|--------------|-------------|
| Per-call | 0.10 USDC | MAX_PER_CALL | Maximum for a single skill invocation |
| Per-day | 20.00 USDC | MAX_PER_DAY | Maximum total across all calls in a calendar day |
Budget is checked before each payment. If a call would exceed either limit, the request is rejected with an error (no payment is made).
Use spending_summary to check current spending and configured limits:
{
"spentToday": "1.2500",
"spentSession": "0.3000",
"limits": {
"maxPerCall": "1.00",
"maxPerDay": "50.00"
},
"recentPayments": [
{
"skillName": "image-gen",
"endpoint": "/api/generate",
"amount": "0.15",
"network": "stellar-testnet",
"timestamp": "2026-01-15T14:30:00.000Z"
}
]
}
Tools
use_skill
Execute a paid SKILL.md endpoint. Resolves the skill, validates the manifest, auto-pays via x402, and returns the result.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| skill | string | Yes | Skill source: URL, local file path, or marketplace name |
| endpoint | string | No | Endpoint path (defaults to first endpoint in manifest) |
| method | string | No | HTTP method: GET, POST, PUT, DELETE, PATCH (defaults to spec) |
| body | string | No | Request body as JSON string |
| headers | object | No | Additional request headers |
Requires: configured wallet (STELLAR_ONLY, EVM_ONLY, or FULL mode).
Validation: the manifest is validated before any payment is attempted. If the SKILL.md is invalid, the tool returns an error without spending.
Examples:
# By marketplace name
use_skill({ skill: "image-gen", body: '{"prompt": "a sunset"}' })
# By URL
use_skill({ skill: "https://example.com/SKILL.md", endpoint: "/api/v2/generate" })
# By local file
use_skill({ skill: "./skills/my-skill/SKILL.md", method: "POST", body: '{"input": "hello"}' })
# With custom headers
use_skill({ skill: "translate", headers: { "X-Target-Lang": "pt-BR" }, body: '{"text": "hello"}' })
read_skill
Read and parse a SKILL.md without executing it. Returns full manifest details, endpoints, pricing, and validation results. Works in any mode (including READ_ONLY).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| skill | string | Yes | Skill source: URL, local file path, or marketplace name |
Returns: name, description, version, author, base URL, endpoints (with pricing and schemas), payment info (networks, asset, payTo), tags, category, validation result, and current wallet mode.
check_balance
Check the USDC balance and address for the configured wallet.
No parameters.
Returns:
{
"address": "GCXZ...",
"balance": "45.250000 USDC",
"network": "stellar-testnet",
"mode": "STELLAR_ONLY"
}
spending_summary
View spending summary: amounts spent today and in the current session, budget limits, and the last 10 payments.
No parameters.
Returns: see Budget & Spending Limits for output format.
create_wallet
Generate a new wallet keypair and save it to ~/.402md/wallet.json.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| network | string | Yes | stellar, stellar-testnet, base, or base-sepolia |
Network-to-wallet mapping:
stellarorstellar-testnet→ generates a Stellar keypair (Keypair.random())baseorbase-sepolia→ generates an EVM keypair (generatePrivateKey())
Safety:
- Refuses to run if a wallet is already configured (any mode except
READ_ONLY). - To replace an existing wallet, delete
~/.402md/wallet.jsonfirst. - New keys are merged with existing config — adding an EVM wallet won't erase a Stellar key.
Returns:
{
"type": "stellar",
"publicKey": "GCXZ...",
"network": "stellar-testnet",
"savedTo": "/Users/you/.402md/wallet.json",
"note": "Fund this address on the Stellar testnet faucet before use."
}
search_skills
Search the 402.md marketplace for available skills.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| query | string | Yes | Search keywords |
| maxPrice | string | No | Max price per call in USDC (e.g., "0.50") |
| category | string | No | Filter by category |
Returns: up to 20 results with name, description, min price, and supported networks.
Modes
The server operates in one of four modes based on which keys are available:
| Mode | Condition | Available Tools |
|------|-----------|-----------------|
| READ_ONLY | No keys configured | read_skill, search_skills, create_wallet |
| STELLAR_ONLY | STELLAR_SECRET set | All tools — pays via Stellar |
| EVM_ONLY | EVM_PRIVATE_KEY set | All tools — pays via Base |
| FULL | Both keys set | All tools — pays via best available network |
In FULL mode, when a skill supports both Stellar and EVM networks, Stellar is preferred (lower fees and faster finality).
Skill Resolution
The skill parameter in use_skill and read_skill accepts three formats:
| Format | Example | How It Resolves |
|--------|---------|-----------------|
| URL | https://example.com/SKILL.md | Fetches directly via HTTP |
| Local file | ./skills/my-skill/SKILL.md | Reads from filesystem |
| Marketplace name | image-gen | Queries {REGISTRY_URL}/api/v1/discovery/skills/{name}/skill-md |
Local file detection: any source starting with /, ./, ../, or ending with .md is treated as a file path.
Architecture
┌──────────────────────────────────────────────────────┐
│ AI Agent (Claude, etc.) │
│ Calls MCP tools: use_skill, check_balance, etc. │
└──────────────┬───────────────────────────────────────┘
│ stdio (MCP protocol)
┌──────────────▼───────────────────────────────────────┐
│ @402md/mcp Server │
│ │
│ ┌─────────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ Use Tools │ │ Wallet │ │ Registry │ │
│ │ use_skill │ │ check_bal │ │ search_skills │ │
│ │ read_skill │ │ spending │ │ │ │
│ │ │ │ create_wal │ │ │ │
│ └──────┬──────┘ └─────┬──────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌─────▼──────┐ ┌───────▼───────┐ │
│ │ resolveSkill│ │ Spending │ │ fetch() │ │
│ │ validateSkil│ │ Tracker │ │ → Registry API│ │
│ │ selectNetwrk│ │ (budget) │ │ │ │
│ └──────┬──────┘ └────────────┘ └───────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────────┐ │
│ │ ClientCache → PaymentClient (@402md/x402) │ │
│ │ ├─ StellarClient (@stellar/stellar-sdk) │ │
│ │ └─ EvmClient (viem) │ │
│ └──────┬──────────────────────────────────────────┘ │
└─────────┼────────────────────────────────────────────┘
│ x402 payment protocol
┌─────────▼────────────────────────────────────────────┐
│ Skill Provider (HTTP server) │
│ Returns 402 → receives payment proof → returns data │
└──────────────────────────────────────────────────────┘
Key patterns:
- Lazy client loading:
ClientCachecreatesPaymentClientinstances on first use per network - Budget enforcement:
SpendingTrackerwrapsBudgetTrackerfrom@402md/x402— checked before, recorded after - Smart network selection: intersects skill's supported networks with wallet capabilities, prefers Stellar
- Config reload:
create_wallettriggersconfig.reload()so new keys take effect immediately - Optional deps:
@stellar/stellar-sdkandviemare optional — only loaded when the corresponding network is used
Examples
End-to-end: first-time setup to skill execution
User: "Search for image generation skills"
→ Agent calls search_skills({ query: "image generation" })
→ Returns list of skills with pricing
User: "Create a wallet so we can use one"
→ Agent calls create_wallet({ network: "stellar-testnet" })
→ Returns public key + "fund on testnet faucet"
User: "Use the image-gen skill to create a sunset"
→ Agent calls use_skill({ skill: "image-gen", body: '{"prompt":"a sunset over mountains"}' })
→ Server resolves skill → validates → checks budget → pays → returns image URL
User: "How much have we spent?"
→ Agent calls spending_summary()
→ Returns { spentToday: "0.0500", spentSession: "0.0500", limits: { maxPerCall: "0.10", maxPerDay: "20.00" }, ... }
Using a local SKILL.md during development
User: "Test my local skill"
→ Agent calls read_skill({ skill: "./my-skill/SKILL.md" })
→ Returns parsed manifest with validation errors/warnings
→ Agent calls use_skill({ skill: "./my-skill/SKILL.md", endpoint: "/api/test" })
→ Executes against your local server
Budget rejection
→ Agent calls use_skill for a skill priced at 5.00 USDC
→ MAX_PER_CALL is 0.10
→ Error: "Exceeds per-call limit (0.10 USDC)"
→ No payment is made
Development
Prerequisites
- Node.js >= 18
- npm
Setup
git clone https://github.com/402md/mcp.git
cd mcp
npm install
Scripts
| Command | Description |
|---------|-------------|
| npm run build | Build with tsup (ESM, shebang included) |
| npm run dev | Build in watch mode |
| npm run typecheck | Run TypeScript type checking (tsc --noEmit) |
| npm run lint | Lint source files with ESLint |
| npm run lint:fix | Lint and auto-fix |
| npm run format | Format with Prettier |
| npm run format:check | Check formatting without writing |
| npm test | Run tests with Vitest |
| npm run test:watch | Run tests in watch mode |
Project Structure
mcp/
├── src/
│ ├── index.ts # CLI entry point (stdio transport)
│ ├── server.ts # MCP server setup, tool registration
│ ├── config.ts # Env vars + wallet file → McpConfig
│ ├── types.ts # McpConfig, SpendingRecord, WalletFileConfig
│ ├── clients.ts # ClientCache + network selection logic
│ ├── resolve.ts # Skill resolution (URL / file / registry)
│ ├── spending.ts # SpendingTracker (budget enforcement)
│ ├── wallet-store.ts # Read/write ~/.402md/wallet.json
│ └── tools/
│ ├── use.ts # use_skill, read_skill
│ ├── wallet.ts # check_balance, spending_summary, create_wallet
│ └── registry.ts # search_skills
├── __tests__/
│ ├── config.test.ts
│ ├── spending.test.ts
│ ├── resolve.test.ts
│ └── wallet-store.test.ts
├── tsup.config.ts # Build config (ESM, shebang, sourcemaps)
├── tsconfig.json
├── eslint.config.mjs
└── .prettierrc
Code Conventions
- No semicolons, single quotes, no trailing commas (Prettier)
- ESM only (
"type": "module",.jsextensions in imports) - Strict TypeScript (
strict: true) - Tools are thin handlers — business logic lives in modules (
spending.ts,clients.ts,resolve.ts) @stellar/stellar-sdkandviemare optional deps, dynamically imported only when the matching network is used
Adding a New Tool
- Create or edit a file in
src/tools/ - Export a
register*Tools(server, config, spending, clientCache)function - Call it from
src/server.ts - Add tests in
__tests__/ - Run
npm run typecheck && npm run lint && npm test
Running Locally
# Build and run
npm run build
node dist/index.js
# Or in dev mode (rebuilds on change)
npm run dev
# In another terminal, test with MCP inspector
npx @modelcontextprotocol/inspector node dist/index.js
To test with environment variables:
STELLAR_SECRET="S..." NETWORK="stellar-testnet" node dist/index.js
License
MIT