> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tallyforagents.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth 2.1

> Connect an MCP host (or any OAuth 2.1 client) to a Tally workspace with the authorization-code + PKCE flow — no static API key in config.

Tally runs a standard **OAuth 2.1 + PKCE** authorization server so MCP hosts and
other clients can connect to a workspace without a user pasting a long-lived
`tly_…` API key. It implements authorization-server metadata
([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)), dynamic client
registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)),
protected-resource metadata ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)),
and token revocation ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)).

Most users never touch these endpoints directly — their host (e.g. Hermes)
drives the flow. See [MCP server → Authentication](/sdk/mcp-server#authentication)
for the user-facing setup. This page documents the protocol for anyone wiring a
new host or debugging.

## Discovery

```
GET https://app.tallyforagents.com/.well-known/oauth-authorization-server
GET https://app.tallyforagents.com/.well-known/oauth-protected-resource
```

The authorization-server document advertises every endpoint and capability:

```json theme={null}
{
  "issuer": "https://app.tallyforagents.com",
  "authorization_endpoint": "https://app.tallyforagents.com/oauth/authorize",
  "token_endpoint": "https://app.tallyforagents.com/oauth/token",
  "registration_endpoint": "https://app.tallyforagents.com/oauth/register",
  "revocation_endpoint": "https://app.tallyforagents.com/oauth/revoke",
  "scopes_supported": ["wallet:read", "wallet:transfer", "x402:pay"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"]
}
```

## Scopes

| Scope             | Grants                                                                    |
| ----------------- | ------------------------------------------------------------------------- |
| `wallet:read`     | Read agent identity, wallets, spending limits, and payment history.       |
| `wallet:transfer` | Send USDC via `pay_direct` / `payments.create`, within the on-chain caps. |
| `x402:pay`        | Pay x402-paywalled HTTP services.                                         |

A token is issued the intersection of the requested scopes and what the client
registered. Moving money (`POST /v1/payments`) requires `wallet:transfer`;
read endpoints work with any granted scope.

## Flow

<Steps>
  <Step title="Register (once)">
    Public PKCE clients self-register. Redirect URIs must be `https` or loopback
    `http` (`127.0.0.1` / `localhost`) per [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252).
    No client secret is issued — PKCE is the proof-of-possession.

    ```bash theme={null}
    curl -X POST https://app.tallyforagents.com/oauth/register \
      -H 'content-type: application/json' \
      -d '{
        "client_name": "Hermes",
        "redirect_uris": ["http://127.0.0.1:8976/callback"],
        "scope": "wallet:read wallet:transfer x402:pay"
      }'
    # → { "client_id": "tly_client_…", "token_endpoint_auth_method": "none", ... }
    ```
  </Step>

  <Step title="Authorize">
    Send the user to the authorization endpoint with a PKCE challenge. They sign in
    to Tally and pick the **workspace**, **mode**, and **agent** to authorize.

    ```
    GET /oauth/authorize
      ?response_type=code
      &client_id=tly_client_…
      &redirect_uri=http://127.0.0.1:8976/callback
      &code_challenge=<BASE64URL(SHA256(verifier))>
      &code_challenge_method=S256
      &scope=wallet:read%20wallet:transfer%20x402:pay
      &state=<opaque>
      &agent_id=hermes        # optional: pre-select the agent
    ```

    On approval Tally redirects to `redirect_uri?code=tly_oac_…&state=…`. On denial
    it returns `?error=access_denied`.
  </Step>

  <Step title="Exchange the code for tokens">
    ```bash theme={null}
    curl -X POST https://app.tallyforagents.com/oauth/token \
      -d grant_type=authorization_code \
      -d code=tly_oac_… \
      -d code_verifier=<the original PKCE verifier> \
      -d client_id=tly_client_… \
      -d redirect_uri=http://127.0.0.1:8976/callback
    ```

    ```json theme={null}
    {
      "access_token": "tly_oat_…",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "tly_ort_…",
      "scope": "wallet:read wallet:transfer x402:pay"
    }
    ```

    Use the access token as a normal bearer credential against `/v1/*`:

    ```bash theme={null}
    curl https://app.tallyforagents.com/v1/me \
      -H "Authorization: Bearer tly_oat_…"
    # → {
    #     "auth_type": "oauth",
    #     "account_slug": "acme", "account_name": "Acme", "mode": "test",
    #     "scopes": ["wallet:read", "wallet:transfer", "x402:pay"],
    #     "agent_id": "hermes",
    #     "expires_at": "2026-05-29T18:00:00.000Z",
    #     "wallets": [{ "address": "0x…", "max_per_tx_usdc": "10", "daily_cap_usdc": "100" }]
    #   }
    ```

    `/v1/me` is the source for a host's `whoami` — everything it returns is
    non-secret (no token), so it's safe to display or log. It answers "which
    workspace / agent / wallet am I connected as, with what scopes, and when does
    this connection expire?".
  </Step>

  <Step title="Refresh">
    Access tokens last one hour. Exchange the refresh token for a new pair before it
    expires — refresh tokens **rotate** on every use, and reusing an already-rotated
    refresh token revokes the whole session (token-theft defense).

    ```bash theme={null}
    curl -X POST https://app.tallyforagents.com/oauth/token \
      -d grant_type=refresh_token \
      -d refresh_token=tly_ort_… \
      -d client_id=tly_client_…
    ```
  </Step>

  <Step title="Disconnect / switch accounts">
    Revoking either token tears down the entire session (access + refresh):

    ```bash theme={null}
    curl -X POST https://app.tallyforagents.com/oauth/revoke \
      -d token=tly_ort_…
    ```

    To switch workspaces, revoke and re-run the authorize flow, choosing a different
    workspace on the consent screen.
  </Step>
</Steps>

## Token reference

| Prefix        | Token              | Lifetime   | Notes                           |
| ------------- | ------------------ | ---------- | ------------------------------- |
| `tly_oat_`    | Access token       | 1 hour     | Bearer credential for `/v1/*`.  |
| `tly_ort_`    | Refresh token      | 30 days    | Single-use; rotates on refresh. |
| `tly_oac_`    | Authorization code | 60 seconds | Single-use; PKCE-bound.         |
| `tly_client_` | Client id          | —          | Public; identifies the client.  |

Only SHA-256 hashes of tokens and codes are stored server-side — the same
model as [API keys](/api-keys).

## Security notes

* **PKCE is mandatory** (`S256` only; `plain` is rejected). There are no
  confidential clients and no client secrets.
* **Redirect URIs** must be `https` or loopback `http` (`127.0.0.1` / `localhost`).
  `https` URIs are exact-matched against the registration; loopback URIs match on
  host + path and accept **any port** (RFC 8252 §7.3), so CLI clients can bind an
  ephemeral port at request time.
* **Authorization codes are single-use** and expire in 60 seconds.
* **Refresh-token rotation** with reuse detection: a replayed refresh token
  revokes the whole grant family.
* **Account-scoped**: a token only ever sees the workspace + mode the user chose
  on the consent screen. Spending stays bounded by each agent's on-chain
  permission caps regardless of scopes.
* The token endpoints are rate-limited and never cache responses.
