┌─────────────────────────────────────────────────────────────┐
│ AI Agent (Claude Desktop / GPT / LangChain) │
│ Uses MCP protocol to call Stellect tools │
└──────────────────────┬──────────────────────────────────────┘
│ stdio (MCP)
┌──────────────────────▼──────────────────────────────────────┐
│ @stellect/mcp-server │
│ Tools: list_services, call_paid_api, check_balance, │
│ get_spending_history │
└──────────┬──────────────────────────────┬───────────────────┘
│ │
┌──────────▼──────────┐ ┌──────────────▼───────────────────┐
│ @stellect/core │ │ @stellect/wallet-native │
│ - X402Client │ │ - Keystore (AES-256-GCM) │
│ - ServiceRegistry │ │ - Signer (isolated scope) │
│ - BudgetGuard │ │ - PolicyEngine │
└──────────┬──────────┘ └──────────────┬───────────────────┘
│ │
│ ┌─────────────────────────┤
│ │ │
┌──────────▼────▼──┐ ┌────────────────▼────────────────────┐
│ OpenZeppelin │ │ Stellar Network │
│ Facilitator │ │ - Soroban budget-policy contract │
│ (verify + settle) │ │ - USDC settlement │
└───────────────────┘ │ - Account balances │
└─────────────────────────────────────┘
Purpose: Define the contract that all wallet adapters must implement.
Files:
src/interface.ts— WalletProvider interfacesrc/types.ts— All shared types (WalletDescriptor, SignRequest, SignResult, SpendingPolicy, Balance, etc.)src/errors.ts— Custom error classes (PolicyRejectionError, KeystoreError, InsufficientBalanceError)src/index.ts— Re-exports everything
Key design: This package has ZERO runtime dependencies. It's pure TypeScript types and interfaces. It compiles to .d.ts files that other packages import.
Purpose: Stellar-native wallet implementation. Our primary adapter and main IP.
Files:
src/native-provider.ts— NativeStellarProvider implements WalletProvidersrc/keystore.ts— AES-256-GCM encrypted key storage at ~/.stellect/wallets/src/signer.ts— Isolated signing (decrypt → sign → wipe pattern)src/policy-engine.ts— Local + Soroban policy evaluationsrc/stellar-utils.ts— Helper functions (balance queries, account creation, friendbot)src/index.tssrc/__tests__/keystore.test.tssrc/__tests__/policy-engine.test.tssrc/__tests__/signer.test.tssrc/__tests__/native-provider.test.ts
Key design:
sign() flow:
1. policyEngine.evaluate(context) ← Check BEFORE touching keys
2. if rejected → return error ← No key material touched
3. keystore.decrypt(name) ← Key in memory (scoped)
4. keypair.sign(payload) ← Produce signature
5. [key goes out of scope → GC] ← Key wiped
6. policyEngine.recordSpend(context) ← Update spending tracker
7. return { signature, evaluation }
Storage format (~/.stellect/wallets/agent-name.json):
{
"version": 1,
"algorithm": "aes-256-gcm",
"salt": "hex...",
"iv": "hex...",
"tag": "hex...",
"encrypted": "hex...",
"metadata": {
"publicKey": "G...",
"network": "stellar:testnet"
},
"createdAt": "2026-04-02T..."
}Purpose: x402 payment engine and service registry.
Files:
src/x402-client.ts— X402Client class (request → 402 → pay → retry)src/service-registry.ts— ServiceRegistry (load bazaar JSON, filter, search)src/budget-guard.ts— BudgetGuard (Soroban contract interaction)src/facilitator.ts— FacilitatorClient (OpenZeppelin API wrapper)src/types.ts— X402PaymentRequired, ServiceEntry, etc.src/index.tssrc/__tests__/x402-client.test.tssrc/__tests__/service-registry.test.tssrc/__tests__/budget-guard.test.ts
x402 Payment Flow:
1. fetch(url) → HTTP response
2. if status !== 402 → return → Done (free endpoint)
3. parse payment instructions → { price, network, payTo, scheme }
4. find stellar:testnet accept option → Must match our network
5. wallet.sign(authEntry, context) → Signed Soroban auth entry
6. facilitator.settle(signature) → On-chain USDC transfer
7. fetch(url, { X-PAYMENT header }) → Retry with payment proof
8. return response → API data
Service Registry (bazaar) format — services.json:
{
"version": 1,
"services": [
{
"id": "weather-openweathermap",
"name": "Real-time Weather Data",
"description": "Current weather for any city worldwide via OpenWeatherMap",
"endpoint": "https://gateway.stellect.dev/api/weather",
"method": "GET",
"params": { "city": "string (city name)" },
"price": "$0.001",
"network": "stellar:testnet",
"payTo": "G...",
"category": "data",
"provider": "Stellect Demo"
}
]
}Purpose: MCP server that exposes Stellect tools to AI agents.
Files:
src/server.ts— McpServer setup + transportsrc/tools/list-services.ts— List available paid services from bazaarsrc/tools/call-paid-api.ts— Call a service, handle x402 payment automaticallysrc/tools/check-balance.ts— Query wallet USDC/XLM balancesrc/tools/get-spending-history.ts— Recent transactionssrc/index.tssrc/__tests__/tools.test.ts
MCP Tool Definitions:
| Tool | Input | Output | What it does |
|---|---|---|---|
list_services |
{ category?: string } |
JSON array of services | Reads service registry, returns available services with prices |
call_paid_api |
{ endpoint, method, params } |
API response JSON | Makes x402 request, handles 402+payment, returns data |
check_balance |
{} |
{ usdc, xlm } |
Queries Stellar for wallet balances |
get_spending_history |
{ limit?: number } |
Transaction array | Returns recent spending from local log |
Purpose: Express server with x402-paywalled endpoints that proxy to real external APIs.
Files:
src/server.ts— Express app + x402 middleware setupsrc/endpoints/weather.ts— Proxies to OpenWeatherMap APIsrc/endpoints/crypto-price.ts— Proxies to CoinGecko APIsrc/endpoints/ai-summary.ts— Simple text summarizationsrc/services.json— Bazaar registry filesrc/index.ts
Endpoint pricing:
| Endpoint | Method | Price | Real data source |
|---|---|---|---|
/api/weather?city=Istanbul |
GET | $0.001 | OpenWeatherMap API |
/api/crypto-price?symbol=XLM |
GET | $0.002 | CoinGecko API |
/api/ai-summary |
POST | $0.005 | Local summarization |
/api/services |
GET | FREE | Service registry JSON |
Purpose: On-chain spending policy enforcement.
Functions:
create_policy(owner, agent, max_per_tx, max_daily, allowed_services)→ Creates policycheck_spend(agent, amount, service)→ Returns bool (can agent spend?)record_spend(agent, amount)→ Updates daily_spent counterget_policy(agent)→ Returns current policy statedeactivate(owner, agent)→ Disables policy
Storage: One AgentPolicy struct per agent address in persistent storage. Daily counter resets after 86400 seconds (24h).
User: "What's the weather in Istanbul?"
│
▼
Claude Desktop → calls list_services() via MCP
│
▼
MCP Server → reads services.json → returns service list
│
▼
Claude picks weather service → calls call_paid_api("/api/weather?city=Istanbul")
│
▼
MCP Server → X402Client.request("http://localhost:4021/api/weather?city=Istanbul")
│
▼
Demo API Server → x402 middleware returns HTTP 402
│ Body: { accepts: [{ scheme: "exact", price: "$0.001", network: "stellar:testnet", payTo: "G..." }] }
│
▼
X402Client → parses 402 → extracts payment instructions
│
▼
X402Client → BudgetGuard.checkSpend(agent, "$0.001", payTo)
│
▼
BudgetGuard → calls Soroban contract check_spend() → returns true
│
▼
X402Client → wallet.sign(walletName, { type: "auth_entry", payload: authEntryXdr, context })
│
▼
NativeStellarProvider:
1. PolicyEngine.evaluate() → allowed ✓
2. Keystore.decrypt("agent-default") → secret key (in scoped memory)
3. Keypair.sign(authEntry) → signature
4. [secret key out of scope → wiped]
5. PolicyEngine.recordSpend()
│
▼
X402Client → FacilitatorClient.settle(signature, authEntry)
│
▼
OpenZeppelin Facilitator:
1. Verifies signature
2. Submits USDC transfer on Stellar testnet
3. Returns { paymentHeader, txHash }
│ ~5 seconds
│
▼
X402Client → fetch(url, { headers: { "X-PAYMENT": paymentHeader } })
│
▼
Demo API Server → middleware verifies payment → passes to handler
│
▼
Weather handler → fetches from OpenWeatherMap → returns real data
│ { city: "Istanbul", temperature: 22, condition: "sunny", ... }
│
▼
MCP Server → returns data to Claude
│
▼
Claude: "Istanbul'da 22°C ve güneşli. Hafif bir ceket yeterli olacaktır."
-
Key isolation: Private keys are AES-256-GCM encrypted at rest. Decryption happens inside a scoped block in the sign() method. The decrypted key never leaves that scope, is never logged, and is never returned.
-
Policy-before-decrypt: The policy engine runs BEFORE any key material is touched. If the policy rejects (budget exceeded, service not allowed), the keystore is never accessed.
-
On-chain enforcement: Soroban contract provides a second layer of budget checking. Even if the local policy engine is bypassed, the contract will reject over-budget transactions.
-
Passphrase protection: The encryption passphrase comes from an environment variable, never hardcoded. In production, this would be a hardware security module or secure enclave.
-
Audit trail: Every sign() call is logged to a local append-only JSON log with timestamp, service, amount, and tx hash. No key material in logs.