Protocol7 min read

Spending Mandates vs OAuth Scopes: Why AI Agents Need Different Authorization

OAuth 2.0 is the most successful authorization protocol ever built. It is also completely wrong for AI agent delegation. Not because it has bugs — it does not — but because it was designed to solve a different problem.

OAuth answers the question: can this application act on behalf of this human? Spending mandates answer a different question: how far down a delegation chain can an agent spend, and on what? Those two questions share some vocabulary but almost no mechanics.

The three things OAuth cannot do

1. Attenuation

In OAuth, a token grants the scope it carries. An app with read:calendar can delegate that token to a sub-process, and the sub-process has full read:calendar access. There is no standard way to say “you can only read events in the next 7 days”.

Spending mandates are attenuating by design. When an orchestrator receives a mandate authorizing $500 in flight bookings, it can only issue downstream mandates for up to $500. A sub-agent that gets $300 cannot re-issue at $400. The math is enforced cryptographically — not by a policy engine you have to configure.

2. Chain depth limits

A modern AI workflow might look like: Human → Orchestrator → Flight Agent → Seat Selector → Price Verifier. OAuth has no concept of delegation depth. If Flight Agent delegates to Seat Selector using a standard access token, Seat Selector can delegate it again, indefinitely.

Aroha bounds delegation depth with a maxDelegationDepth constraint — set it to 2, and the chain terminates there. Seat Selector cannot issue sub-mandates even if it tries; the attempt is rejected at chain verification. The issuer at the top decides how far trust propagates. (Depth lives on task mandates and is enforced by the delegation chain verifier; it composes with the spending limits shown above.)

3. Spend limits as a first-class primitive

OAuth scopes describe what you can do. They have no mechanism for how much. There is no standard scope that means “you can call the Stripe API but spend no more than $200”.

Spending mandates make monetary limits a first-class field — spendLimit: 200, currency: "USD" — that flows through the whole chain. Every agent in the chain can verify the limit without calling back to a central server.

What spending mandates look like in practice

from aroha.mandate import (
    issue_intent_mandate, attenuate_to_payment, verify_mandate, SpendingConstraints
)

# Human approves: orchestrator may spend up to $500, for 1 hour
mandate = issue_intent_mandate(
    grantor_did=human_did,
    grantee_did=orchestrator_did,
    constraints=SpendingConstraints(spend_limit_usd=500),
    private_key=human_key,
    ttl_seconds=3600,
)

# Orchestrator delegates to the flight agent — the limit can only narrow
sub = attenuate_to_payment(
    mandate,
    grantee_did=flight_agent_did,
    narrowed=SpendingConstraints(spend_limit_usd=300),   # can't exceed 500
    grantor_private_key=orchestrator_key,
)

# The flight agent verifies before spending
valid, m, reason = verify_mandate(sub.token, human_public_key)
# valid is True
# m.constraints.spend_limit_usd == 300
The key invariant: attenuation is monotone. Each step in the chain can only reduce the scope, spend limit, or permitted capabilities. No sub-agent can grant itself more than what it received. This is enforced by the cryptographic signature chain, not by policy configuration.
Authority can only shrink: $500 granted, narrowed to $300; widening to $800 throws, a tampered token fails verification

That diagram isn't aspirational, by the way — narrowing succeeds, widening throws, and a tampered token fails verification are the real behaviour of attenuate_to_payment and verify_mandate in the published packages. See the mandates guide to run it.

When to still use OAuth

OAuth is still the right tool when a human is directly granting your agent access to their Google Calendar, Slack workspace, or GitHub repos. That is exactly what OAuth was designed for — user-to-app authorization.

The pattern we recommend: use OAuth to get the initial access token from the user, then wrap any downstream agent delegation in a spending mandate. The mandate layer handles everything that happens within your agent network; OAuth handles everything that happens between your network and third-party services.