Skip to main content
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), dynamic client registration (RFC 7591), protected-resource metadata (RFC 9728), and token revocation (RFC 7009). Most users never touch these endpoints directly — their host (e.g. Hermes) drives the flow. See 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:
{
  "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

ScopeGrants
wallet:readRead agent identity, wallets, spending limits, and payment history.
wallet:transferSend USDC via pay_direct / payments.create, within the on-chain caps.
x402:payPay 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

1

Register (once)

Public PKCE clients self-register. Redirect URIs must be https or loopback http (127.0.0.1 / localhost) per RFC 8252. No client secret is issued — PKCE is the proof-of-possession.
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", ... }
2

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.
3

Exchange the code for tokens

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
{
  "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/*:
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?”.
4

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).
curl -X POST https://app.tallyforagents.com/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=tly_ort_… \
  -d client_id=tly_client_…
5

Disconnect / switch accounts

Revoking either token tears down the entire session (access + refresh):
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.

Token reference

PrefixTokenLifetimeNotes
tly_oat_Access token1 hourBearer credential for /v1/*.
tly_ort_Refresh token30 daysSingle-use; rotates on refresh.
tly_oac_Authorization code60 secondsSingle-use; PKCE-bound.
tly_client_Client idPublic; identifies the client.
Only SHA-256 hashes of tokens and codes are stored server-side — the same model as 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.