Agno · AI agents · API key security

Agno AI agent API key: scoping tool calls in multi-agent teams

Agno (formerly Phidata) is designed for speed and simplicity: a few lines of Python spin up a multi-modal, memory-equipped agent or a coordinated team of agents. That low friction to creating capable agent systems is exactly what makes vendor API key management non-trivial — tools are shared across team members, agents run in parallel, and there's no built-in mechanism to say "this session's total spend on Stripe is capped at $200." This page covers the specific risks of Agno multi-agent systems and the vault-key pattern that adds the missing spending guardrails.

TL;DR

Agno teams share tools across member agents. If any member calls a payment tool with a full-access API key and that member gets confused or receives bad context, it can exhaust your vendor budget without the team leader being aware. A vault key proxy adds the per-session dimension: issue a vault key at session start, give each team member its own sub-cap, and get a per-call audit log with agent identity attached. The 429 from an exceeded cap surfaces as a tool exception — your team leader can handle it without the session crashing.

How Agno agents call vendor APIs

In Agno, tools are Python functions passed to the Agent constructor. The agent decides when to call them. A billing agent built on Agno might look like:

from agno.agent import Agent
from agno.models.anthropic import Claude
import stripe, os

def charge_customer(customer_id: str, amount_cents: int) -> str:
    """Charge a customer via Stripe and return the payment intent ID."""
    stripe.api_key = os.environ["STRIPE_SECRET_KEY"]  # full-access key
    intent = stripe.PaymentIntent.create(
        amount=amount_cents,
        currency="usd",
        customer=customer_id,
    )
    return f"Payment intent created: {intent.id}"

billing_agent = Agent(
    model=Claude(id="claude-sonnet-4-6"),
    tools=[charge_customer],
    description="Processes billing for the current customer cohort",
)

This is standard Agno code. The problem is the API key: STRIPE_SECRET_KEY is a full-access key with no per-session cap. The agent decides when to call charge_customer — you have no pre-execution enforcement of how many times it can be called or how much total spend the session is authorized to create.

Three gaps Agno's native tooling doesn't fill for vendor spend control

GapWhat happens in practiceAgno's answer
No per-session spend cap A billing agent is given ambiguous context about which customers to charge and calls charge_customer 40 times instead of 4. Agno faithfully executes every tool call the LLM requests. The cap on damage is your Stripe account limit. None. Tool call history in the agent's session state shows what happened, but doesn't prevent it.
No per-session revoke You notice the billing agent is misbehaving. Stopping the Python process kills the agent session, but tool calls that were in-flight may have already reached Stripe. Rotating the Stripe key breaks every other agent process using that key. Agent session termination stops new tool calls but cannot undo in-flight vendor calls or revoke credentials from them.
No per-tool-call audit with agent identity Agno's session history shows tool inputs and outputs, but doesn't parse dollar amounts from Stripe responses or attribute individual Stripe charges to a specific agent session ID in a queryable audit log. Session messages and tool call history. No vendor cost parsing, no per-session charge aggregation.

The team risk: how Agno's multi-agent architecture amplifies credential exposure

Agno's Team class lets you create a supervisor agent that coordinates multiple member agents. Team members can have their own tools, or share tools with the team. In either case, the underlying API key is the same shared credential:

from agno.team import Team

billing_agent = Agent(tools=[charge_customer], ...)
notification_agent = Agent(tools=[send_twilio_sms, charge_customer], ...)

billing_team = Team(
    members=[billing_agent, notification_agent],
    ...
)

If the supervisor sends work to both members simultaneously (Agno teams support parallel execution), both agents can call vendor APIs concurrently with no shared spending cap. A supervisor that misroutes work to notification_agent when it meant to check a balance, and notification_agent interprets the task as a billing action, can create Stripe charges with no circuit breaker.

Scoping vault keys per session in Agno

Issue the vault key before the agent or team session starts, and close it into the tool function via a wrapper:

import httpx, uuid
from agno.agent import Agent
from agno.models.anthropic import Claude
import stripe, os

def make_charge_tool(vault_key: str, session_id: str):
    """Factory returning a charge_customer tool bound to this session's vault key."""
    def charge_customer(customer_id: str, amount_cents: int) -> str:
        """Charge a customer via Stripe. Returns payment intent ID."""
        stripe.api_key = vault_key
        stripe.api_base = "https://proxy.keybrake.com/stripe"
        intent = stripe.PaymentIntent.create(
            amount=amount_cents,
            currency="usd",
            customer=customer_id,
            idempotency_key=f"{session_id}-{customer_id}-{amount_cents}",
        )
        return f"Payment intent created: {intent.id}"
    return charge_customer

def run_billing_session(customer_ids: list, budget_usd: float = 200.0):
    session_id = str(uuid.uuid4())

    # Issue vault key before spinning up the agent
    r = httpx.post(
        "https://proxy.keybrake.com/vault/keys",
        headers={"Authorization": f"Bearer {os.environ['KEYBRAKE_API_KEY']}"},
        json={
            "vendor": "stripe",
            "daily_usd_cap": budget_usd,
            "allowed_endpoints": ["POST /v1/payment_intents", "GET /v1/customers/*"],
            "expires_in": "1h",
            "agent_run_label": f"agno-billing/{session_id}",
        },
    )
    vault_key = r.json()["vault_key"]

    # Bind vault key into the tool — STRIPE_SECRET_KEY never set in env
    charge_tool = make_charge_tool(vault_key, session_id)

    billing_agent = Agent(
        model=Claude(id="claude-sonnet-4-6"),
        tools=[charge_tool],
        description="Processes billing for the provided customer list",
    )
    billing_agent.run(f"Charge these customers $29.99: {customer_ids}")

The vault key is issued once per session and bound into the tool via closure. The agent never sees STRIPE_SECRET_KEY — it only has access to the scoped vault key. If the agent calls charge_customer more than the cap allows, the proxy returns a 429, which surfaces as an exception in the tool function and an error message to the LLM — at which point the agent stops trying.

Per-member vault keys for Agno teams

For Agno teams where you want per-member spending isolation, issue a vault key for each team member with its own sub-cap:

billing_vault_key = issue_vault_key(session_id, budget=150.0, label="billing-agent")
notification_vault_key = issue_vault_key(session_id, budget=50.0, label="notification-agent")

billing_agent = Agent(tools=[make_charge_tool(billing_vault_key, session_id)], ...)
notification_agent = Agent(tools=[
    make_sms_tool(notification_vault_key, session_id),
    make_charge_tool(billing_vault_key, session_id),  # shared billing key, shared cap
], ...)

The billing agent has a $150 cap. The notification agent's SMS calls are capped at $50 independently. If the notification agent tries to make Stripe charges, it uses the billing vault key's cap — giving you a shared $150 ceiling on Stripe charges regardless of which agent makes them.

How Keybrake fits

Keybrake is the proxy layer between your Agno agents and Stripe, Twilio, or Resend. You issue vault keys before agent sessions start, bind them into tool functions via closures, and the real vendor secrets never enter your agent environment. Each session has an independent cap; each team member can have its own sub-cap. The audit log records every call with the session label you set — queryable by agent identity, session ID, and vendor.

Get early access

Related questions

Does the vault key approach work with Agno's built-in memory and storage?

Yes — but don't store the vault key string in Agno's persistent memory (e.g. in a PgMemory or SqliteMemory storage backend). Vault keys are short-lived session credentials; storing them in long-term memory means a new session resuming from memory could accidentally use an expired key. Issue a fresh vault key at the start of each session, even when resuming from persistent memory context. The session history and agent context in memory is fine to persist — just not the vault key itself.

Can I use Agno's structured output mode with the vault key pattern?

Yes — structured output and vault key scoping are orthogonal. Agno's response_model parameter shapes the LLM's final output format; the vault key controls what the tool functions do when called. You can use both simultaneously: an agent that returns a BillingResult Pydantic model and makes Stripe calls via a vault-key-bound tool function. The structured output doesn't affect how the tool functions are executed.

How do I handle vault key expiry for long-running Agno sessions?

For sessions expected to run longer than the vault key TTL, either (1) set a longer TTL at issue time that covers the expected session duration, or (2) implement key refresh logic in the tool function: check the vault key's expires_at before each call, and re-issue if it's within 60 seconds of expiry. Option 1 is simpler; option 2 is more robust for sessions with unpredictable runtime. In either case, keep the TTL as short as is practical — a 30-day vault key is closer to a permanent credential than a session-scoped one.

Further reading