@moneda/services exports getAgentTools(userId, email?) — a ready-to-bind tool surface for LLM agents that need to read Moneda data. It packages all 29 read operations the MCP server exposes as provider-agnostic descriptors you wrap in your AI SDK’s tool factory (Anthropic, OpenAI, Gemini, Mastra, Vercel AI SDK, etc.).
Use this when you’re building an in-process agent (support chatbot, internal assistant, background automation). It’s roughly 7–10× cheaper per query than hitting the MCP server over HTTP — the MCP copy stays for external third-party agents that need OAuth + scoped external access.
This is an SDK-level API, not an HTTP endpoint. Import it from @moneda/services in a Node process that already has a Moneda user ID — typically your support chatbot service or an internal agent worker.
Why is it cheaper than MCP?
| Lever | MCP-over-HTTP | Agent Tools |
|---|
| Tool schema size | 37 multi-paragraph descriptions (~18k tokens) re-sent every turn | 29 terse descriptions (~1.1k tokens), can be prompt-cached |
| Response encoding | JSON (verbose) | TOON (~40% smaller) |
| Response shape | Full service shape with UI coaching fields | Lite projection — drops IDs, raw addresses, derived counts, UI hints |
| Transport | HTTP round-trip per call | In-process function call |
Rough per-query cost on Claude Sonnet 4.5:
| Path | Cost |
|---|
| MCP-over-HTTP | ~$0.34 |
| Agent Tools (no caching) | ~$0.03–0.05 |
Agent Tools (with cache_control on prefix) | ~$0.01–0.02 |
Quick start
import { tool } from "ai"; // or mastra, openai, @anthropic-ai/sdk, etc.
import { getAgentTools } from "@moneda/services";
// Build the descriptor set for an authenticated user.
const descriptors = getAgentTools(currentUser.id);
// Wrap each descriptor in your AI SDK's tool() factory.
const tools = Object.fromEntries(
Object.entries(descriptors).map(([name, d]) => [name, tool(d)]),
);
// Pass to your agent loop.
const response = await generateText({
model: anthropic("claude-sonnet-4-5"),
tools,
messages,
});
What’s in the box
Every descriptor has { description, parameters, execute }:
description — terse (~30–50 tokens). Your caller is free to prepend agent-coaching on top.
parameters — a Zod schema with tight enums so the agent’s guess-space is narrow and tool calls validate more often.
execute(args) — returns a Promise<string>. The string is TOON-encoded, with the lite projection + pruneEmpty already applied.
Account & balance:
get_balance — { currency?: "USD"|"EUR"|"CHF"|"CAD" }
get_wallet — {}
get_transactions — rich filters, see below
get_spending_by_type_group — { period: "7d"|"30d"|"90d", currency? }
get_virtual_accounts — {}
get_my_accounts — {}
Rates & products:
get_exchange_rate — { from, to }
get_apy_rates — { currency? }
Rewards:
get_points_balance — {}
get_points_activity — {}
get_referral_code — {} (returns the user’s referral code + referral count)
get_referees — {} (lists users the caller has successfully referred)
Contacts & security:
get_contacts — { search?, limit?, offset? }
get_recovery_contacts — {}
list_recovery_emails — { accountId?: string } — ZK email-based account recovery
get_recovery_email — { id }
get_recovery_config — { accountId? } — accepted emails, total weight, threshold, fullySet flag
get_recovery_status — { accountId? } — most recent recovery snapshot; emails anonymized as a***@domain
get_passkeys — {}
get_settings — {}
Notifications:
list_notifications — { cursor?, limit?, filter?: "all" | "unread" }
get_unread_notification_count — {} (cheaper than list_notifications when you only need the count)
Knowledge base (fallback — prefer embedding the FAQ in the system prompt directly):
search_knowledge — { query, category?, limit? }
get_faq_category — { slug }
get_faq_item — { id }
Sub-accounts (vaults):
list_sub_accounts — { includeClosed?: boolean }
get_sub_account — { id }
get_sub_account_balance — { id, currency?: "USDC"|"EURC"|"CHFAU"|"CADD" }
get_total_wealth — {} (aggregates main account + every live vault)
AGENT_TOOL_NAMES is exported as a const array for allowlisting, analytics, or admin UIs that want to enumerate capabilities without building the full descriptor set.
get_transactions — lite by default, detail: true to opt into full
Returns the compact shape by default — hash, direction, amount, currency, status, date, trimmed counterparty. On a single-hash drill-down (e.g. “why did this transaction fail?”), pass detail: true along with transactionHash to get the full row (typeGroup, category, note, reference, raw counterparty routing, etc.):
// Common case — compact list
await tools.get_transactions.execute({ limit: 10, currency: "USD" });
// Drill-down — full row
await tools.get_transactions.execute({
transactionHash: "0xabc...",
detail: true,
});
Response shape — lite projections
Each tool’s execute runs the service output through a hand-picked field allowlist (project* in @moneda/services) and then through pruneEmpty before TOON-encoding.
get_wallet drops network, supportedTokens, warning, tip (~76% smaller)
get_virtual_accounts drops minimumTransfer, firstPartyPayments, thirdPartyPayments, capabilityInfo (~78% smaller)
get_balance drops source, notice, snapshotTimestamp; collapses snapshot metadata to an optional stale field on the fallback path (~22% live, ~65% on snapshot)
get_transactions drops typeGroup, type, category, note, reference, exchanged*, received*, raw counterparty routing (~27% list, ~48% single row)
get_my_accounts, get_contacts, get_recovery_contacts, get_passkeys, get_points_activity, get_referees, get_referral_code — see lite.ts for the exact allowlist.
Pair with prompt caching for the real win
The agent-tools descriptor set + your system prompt are cacheable. Wire cache_control on the prefix once and every subsequent conversation turn pays ~10% of the input cost on that portion. For a support chatbot with 3–5 turns, that typically compounds to another 3–5× savings on top of the lite projection.
// Anthropic Messages API example
messages: [
{ role: "system", content: systemPrompt },
],
tools, // descriptor set, 29 entries
// Add cache_control on the last system block + last tool definition to
// snapshot the prefix. The SDK handles refresh.
See packages/services/src/agent-tools.ts for the descriptor factory and packages/services/src/lite.ts for the projections.
When to use MCP instead
Use the MCP server when the agent runs in a third-party client (Claude Desktop, ChatGPT over MCP, Cursor) and needs OAuth + scope-gated external access with human-in-the-loop approval for writes.
Use Agent Tools (this SDK) when the agent runs in your own process, you already have the authenticated userId, and you’re paying the tokens yourself. The support chatbot is the canonical use case.