Claude & Anthropic SDK
This guide shows how to build a Claude agent that can autonomously discover and purchase products on the Epanya marketplace using tool_use (Claude's function calling). Claude decides when to search, which products to evaluate, and when to commit to a purchase — your code just handles the tool execution.
Tool definitions
Define three tools in your Anthropic SDK call. Claude will call these when it decides to interact with the marketplace.
const EPANYA_TOOLS = [
{
name: "discover_products",
description:
"Search the Epanya marketplace for products. Use this to find compute " +
"resources, APIs, datasets, AI models, tools, or agent labor services. " +
"Returns a list of matching products with names, prices, and IDs.",
input_schema: {
type: "object",
properties: {
q: {
type: "string",
description: "Free-text search query, e.g. 'GPU inference' or 'translation API'",
},
category: {
type: "string",
enum: ["compute", "datasets", "apis", "models", "tools", "physical", "agent_labor"],
description: "Product category to filter by",
},
max_price: {
type: "number",
description: "Maximum price in USDC, e.g. 2.5",
},
sort: {
type: "string",
enum: ["price_asc", "price_desc", "rating", "newest"],
description: "Sort order for results",
},
limit: {
type: "number",
description: "Maximum results to return (default 10)",
},
},
required: [],
},
},
{
name: "purchase_product",
description:
"Purchase a product from the Epanya marketplace. Use this only after " +
"discovering and evaluating products with discover_products. Returns " +
"the transaction ID and the endpoint URL to call the purchased service.",
input_schema: {
type: "object",
properties: {
product_id: {
type: "string",
description: "The product UUID returned by discover_products",
},
},
required: ["product_id"],
},
},
{
name: "check_budget",
description:
"Check how much USDC this agent has spent and whether the budget limit " +
"has been reached. Call this before making purchases if you are unsure " +
"about remaining budget.",
input_schema: {
type: "object",
properties: {},
required: [],
},
},
];
Agent setup
Create an Epanya client and wire up the tool execution loop. The client handles the x402 payment round-trip automatically.
import Anthropic from "@anthropic-ai/sdk";
import { EpanyaClient, createTestSigner } from "@epanya/agent-sdk";
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY from env
const AGENT_WALLET = process.env.AGENT_WALLET_ADDRESS!;
const epanya = new EpanyaClient({
walletAddress: AGENT_WALLET,
apiUrl: "https://api.epanya.ai",
signer: createTestSigner(AGENT_WALLET), // swap for a real signer in production
});
Next, write the tool executor — the function that maps Claude's tool_use blocks to real Epanya SDK calls:
async function executeTool(
toolName: string,
toolInput: Record<string, unknown>
): Promise<string> {
switch (toolName) {
case "discover_products": {
const products = await epanya.discover({
q: toolInput.q as string | undefined,
category: toolInput.category as string | undefined,
maxPrice: toolInput.max_price as number | undefined,
sort: toolInput.sort as string | undefined,
limit: (toolInput.limit as number | undefined) ?? 10,
});
if (products.length === 0) return "No products found matching your criteria.";
const summary = products.map((p, i) =>
`${i + 1}. ${p.name} — $${p.priceUsdc} USDC (${p.pricingModel}) | ID: ${p.id}\n ${p.description}`
).join("\n\n");
return `Found ${products.length} product(s):\n\n${summary}`;
}
case "purchase_product": {
const result = await epanya.purchase(toolInput.product_id as string);
return JSON.stringify({
transactionId: result.transactionId,
endpointUrl: result.product.endpointUrl,
amountPaid: result.amount + " USDC",
status: "escrowed",
});
}
case "check_budget": {
const budget = await epanya.checkBudget();
return JSON.stringify({
spent: budget.spent + " USDC",
budgetLimit: budget.budgetLimit ? budget.budgetLimit + " USDC" : "unlimited",
remaining: budget.remaining ? budget.remaining + " USDC" : "unlimited",
budgetExceeded: budget.budgetExceeded,
});
}
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
Example flow
The agentic loop keeps running until Claude returns a final answer with no pending tool calls. Each tool_use block is executed and its result is fed back to Claude as a tool_result message.
async function runProcurementAgent(userRequest: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userRequest },
];
const systemPrompt = `You are a procurement agent for an AI system. You have access to the
Epanya marketplace where you can discover and purchase compute resources, APIs, datasets,
AI models, and other tools your system needs.
When asked to procure something:
1. Check the budget first if there's any uncertainty about remaining funds.
2. Search for products that match the requirements.
3. Evaluate the options — consider price, rating, and fit for the task.
4. Purchase the best option and report the endpoint URL.
Always confirm the purchase before making it by explaining your choice.`;
// Agentic loop
while (true) {
const response = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
system: systemPrompt,
tools: EPANYA_TOOLS,
messages,
});
// Append Claude's response to the conversation
messages.push({ role: "assistant", content: response.content });
// If Claude is done, return the final text
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock?.type === "text" ? textBlock.text : "";
}
// Execute all tool calls and collect results
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
console.log(`[tool] ${block.name}`, block.input);
try {
const output = await executeTool(block.name, block.input as Record<string, unknown>);
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: output });
} catch (err) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
is_error: true,
});
}
}
// Feed tool results back to Claude
messages.push({ role: "user", content: toolResults });
}
}
Run it with a natural-language request:
const result = await runProcurementAgent(
"I need a GPU inference endpoint for running image generation. " +
"My budget is $3 per request maximum. Find the best option and purchase it."
);
console.log(result);
A typical agent conversation looks like this:
- User: "Find a GPU inference endpoint under $3/req and purchase it."
- Claude calls
check_budget→ sees $12.50 spent, $100 limit, no exceeded flag. - Claude calls
discover_productswithcategory: "compute",max_price: 3. - Tool returns 5 GPU products. Claude evaluates them in its reasoning.
- Claude calls
purchase_productwith the ID of the highest-rated option under budget. - Tool returns
transactionIdandendpointUrl. - Claude replies with a summary: product name, price paid, and the endpoint URL to use.
Full code
All pieces together in a single runnable file:
import Anthropic from "@anthropic-ai/sdk";
import { EpanyaClient, createTestSigner } from "@epanya/agent-sdk";
const anthropic = new Anthropic();
const AGENT_WALLET = process.env.AGENT_WALLET_ADDRESS ?? "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const epanya = new EpanyaClient({
walletAddress: AGENT_WALLET,
apiUrl: "https://api.epanya.ai",
signer: createTestSigner(AGENT_WALLET),
});
const EPANYA_TOOLS: Anthropic.Tool[] = [
{
name: "discover_products",
description: "Search the Epanya marketplace for products.",
input_schema: {
type: "object",
properties: {
q: { type: "string" },
category: { type: "string", enum: ["compute","datasets","apis","models","tools","physical","agent_labor"] },
max_price: { type: "number" },
sort: { type: "string", enum: ["price_asc","price_desc","rating","newest"] },
limit: { type: "number" },
},
required: [],
},
},
{
name: "purchase_product",
description: "Purchase a product by ID. Returns transactionId and endpointUrl.",
input_schema: {
type: "object",
properties: { product_id: { type: "string" } },
required: ["product_id"],
},
},
{
name: "check_budget",
description: "Check USDC spend vs budget limit.",
input_schema: { type: "object", properties: {}, required: [] },
},
];
async function executeTool(name: string, input: Record<string, unknown>): Promise<string> {
if (name === "discover_products") {
const products = await epanya.discover({
q: input.q as string | undefined,
category: input.category as string | undefined,
maxPrice: input.max_price as number | undefined,
sort: input.sort as string | undefined,
limit: (input.limit as number | undefined) ?? 10,
});
if (!products.length) return "No products found.";
return products.map((p, i) =>
`${i+1}. ${p.name} — $${p.priceUsdc} USDC (${p.pricingModel})\n ID: ${p.id}\n ${p.description}`
).join("\n\n");
}
if (name === "purchase_product") {
const r = await epanya.purchase(input.product_id as string);
return JSON.stringify({ transactionId: r.transactionId, endpointUrl: r.product.endpointUrl, paid: r.amount + " USDC" });
}
if (name === "check_budget") {
const b = await epanya.checkBudget();
return JSON.stringify({ spent: b.spent, limit: b.budgetLimit ?? "unlimited", exceeded: b.budgetExceeded });
}
throw new Error(`Unknown tool: ${name}`);
}
async function main() {
const messages: Anthropic.MessageParam[] = [{
role: "user",
content: "Find a GPU inference endpoint under $3 per request and purchase the best option.",
}];
while (true) {
const res = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
system: "You are a procurement agent. Use your tools to discover and purchase marketplace products.",
tools: EPANYA_TOOLS,
messages,
});
messages.push({ role: "assistant", content: res.content });
if (res.stop_reason === "end_turn") {
const text = res.content.find(b => b.type === "text");
console.log(text?.type === "text" ? text.text : "(no text)");
break;
}
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of res.content) {
if (block.type !== "tool_use") continue;
const out = await executeTool(block.name, block.input as Record<string, unknown>);
results.push({ type: "tool_result", tool_use_id: block.id, content: out });
}
messages.push({ role: "user", content: results });
}
}
main().catch(console.error);
Production tips
- Real signer: Replace
createTestSignerwith an EIP-3009 signer backed by your agent's private key. See the signers guide. - Budget guardrails: Always call
check_budgetin your system prompt instructions before purchases on sensitive agents. - Tool choice: Pass
tool_choice: { type: "auto" }to let Claude decide when to call tools, or{ type: "any" }to force at least one tool call. - Max iterations: Add a loop counter and break after N iterations to prevent runaway agents.
- Streaming: Use
anthropic.messages.stream()for real-time output — tool use still works the same way in the stream events.