Skip to main content
There are two distinct ways a policy can stop an operation:
  • Denial — the policy blocked it outright. The agent is not permitted to do this at all.
  • Pending approval — the operation is allowed in principle but exceeds a threshold that requires the owner to sign off first. See Policy Engine for how to configure approval thresholds.
This guide covers the denial case. When a transfer or contract call violates a policy, the Cobo Agentic Wallet service returns a structured 403 response. The SDK raises a PolicyDeniedError carrying machine-readable guidance that enables agents to self-correct and retry without human intervention.

Error hierarchy

ExceptionHTTPWhen
APIErrorvariesBase class for all errors
AuthenticationError401Missing or invalid API key
NotFoundError404Resource not found
ServerError5xxServer-side error (auto-retried)
PolicyDeniedError403Policy violation — contains structured PolicyDenial

The PolicyDenial structure

@dataclass(frozen=True)
class PolicyDenial:
    code: str                   # e.g. "TRANSFER_LIMIT_EXCEEDED"
    reason: str                 # human-readable explanation
    details: dict[str, Any]     # machine-readable constraint values
    suggestion: str | None      # actionable fix for the agent
    raw_response: dict[str, Any]
Example denial response:
{
  "code": "TRANSFER_LIMIT_EXCEEDED",
  "reason": "Transfer amount exceeds per-transaction limit",
  "details": { "limit_value": 100, "requested_amount": 500 },
  "suggestion": "Retry with amount <= 100."
}

Denial codes

CodeCauseKey details fields
TRANSFER_LIMIT_EXCEEDEDPer-tx amount caplimit_value, requested_amount
CHAIN_RESTRICTEDChain not in allowed listallowed_chains, requested_chain
CONTRACT_NOT_WHITELISTEDContract not whitelistedallowed_contracts, requested_contract
PARAMETER_OUT_OF_BOUNDSContract call parameter out of rangeparam_name, op, limit_value
BUSINESS_HOURS_VIOLATIONOperation outside allowed hours
DELEGATION_EXPIREDDelegation TTL has passedexpires_at
WALLET_FROZENWallet or delegation is frozen
INSUFFICIENT_BALANCEWallet balance too lowbalance, requested_amount
INSUFFICIENT_PERMISSIONDelegation lacks required permissionrequired_permission
POLICY_DENIEDGeneric rule block

Basic self-correction

from cobo_agentic_wallet.errors import PolicyDeniedError

try:
    await agent_client.transfer_tokens(
        wallet_uuid=WALLET_UUID,
        chain_id="SETH",
        dst_addr="0xRecipient...",
        token_id="SETH_USDC",
        amount="500",
    )
except PolicyDeniedError as exc:
    denial = exc.denial
    print(f"Denied: {denial.code}{denial.reason}")
    print(f"Suggestion: {denial.suggestion}")

    # Use structured details to derive the corrected amount
    if "limit_value" in denial.details:
        corrected_amount = str(denial.details["limit_value"])
        await agent_client.transfer_tokens(
            wallet_uuid=WALLET_UUID,
            chain_id="SETH",
            dst_addr="0xRecipient...",
            token_id="SETH_USDC",
            amount=corrected_amount,
        )

Framework-specific handling

Each framework returns denials as tool output strings (not exceptions), so the LLM’s reasoning loop continues naturally:
# LangChain catches PolicyDeniedError inside the tool handler and
# returns formatted text. The agent sees it as a normal tool result.
toolkit = CoboAgentWalletToolkit(client=client)
# No extra code needed — denial text arrives as tool output:
# "Policy Denied [TRANSFER_LIMIT_EXCEEDED]: Amount exceeds per-tx limit
#  limit_value: 100
#  Suggestion: Retry with amount <= 100."

Production patterns

Cap retry attempts to prevent infinite loops:
MAX_RETRIES = 3
amount = "500"

for attempt in range(MAX_RETRIES + 1):
    try:
        await agent_client.transfer_tokens(
            wallet_uuid=WALLET_UUID, chain_id="SETH",
            dst_addr="0x...", token_id="SETH_USDC", amount=amount,
        )
        break
    except PolicyDeniedError as exc:
        if attempt == MAX_RETRIES:
            raise
        amount = str(exc.denial.details.get("limit_value", amount))
Cumulative limits (daily, monthly) can’t be fixed by adjusting parameters. Escalate to the owner instead:
try:
    await agent_client.transfer_tokens(
        wallet_uuid=WALLET_UUID,
        chain_id="SETH",
        dst_addr="0x...",
        token_id="SETH_USDC",
        amount=amount,
    )
except PolicyDeniedError as exc:
    if exc.denial.code in ("DAILY_CUMULATIVE_EXCEEDED", "MONTHLY_CUMULATIVE_EXCEEDED"):
        await notify_owner(
            f"Agent hit cumulative limit: {exc.denial.reason}\n"
            f"Remaining: {exc.denial.details.get('remaining', '?')}"
        )
        return {"status": "escalated"}

    # Per-tx limits can still be self-corrected.
    amount = str(exc.denial.details.get("limit_value", amount))
If details fields are missing, parse the suggestion text:
import re
from decimal import Decimal

def derive_retry_amount(denial, attempted: str) -> str | None:
    for key in ("limit_value", "max_allowed_amount"):
        if denial.details.get(key) is not None:
            v = str(denial.details[key])
            if Decimal(v) < Decimal(attempted):
                return v
    match = re.search(r"<=\s*([0-9]+(?:\.[0-9]+)?)", denial.suggestion or "")
    if match:
        return match.group(1)
    return None