monoesdocs
Menu

Authentication

OAuth 2.0 & agent access

monoes.me runs a real OAuth 2.0 authorization server, separate from the cookie-based session its own browser client uses. This is the recommended path for agents and third-party sites. It never requires handling or storing a user's password.

Discovery documents

Standard OAuth metadata, so most OAuth client libraries can configure themselves automatically.

  • GET /.well-known/oauth-protected-resource RFC 9728 Protected Resource Metadata.
  • GET /api/auth/.well-known/oauth-authorization-server RFC 8414 Authorization Server Metadata (issuer: https://monoes.me/api/auth).

1. Register a client

Dynamic Client Registration (RFC 7591) is open: no pre-approval needed.

curl
curl -X POST https://monoes.me/api/auth/oauth2/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["https://your-app.example/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"]
  }'

Returns a client_id. Store it; you'll need it for every step below.

2. Send the user to authorize

Standard authorization-code flow with PKCE. Redirect the user's browser to:

Authorization URL
https://monoes.me/api/auth/oauth2/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.example/callback
  &response_type=code
  &scope=community:read+community:write
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256

The user signs in (if needed) and approves a consent screen showing exactly the scopes you requested. Add +offline_access to scope if you want a refresh_token back from the next step — see Scopes below.

3. Exchange the code for a token

curl
curl -X POST https://monoes.me/api/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=THE_CODE_FROM_THE_REDIRECT" \
  -d "redirect_uri=https://your-app.example/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

4. Call the API

curl
curl https://monoes.me/api/community/feed \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

The token acts as the specific user who granted consent, not an anonymous service identity. See Quickstart for a full worked example.

Scopes

  • community:read: every GET endpoint that requires auth.
  • community:write: every POST, PATCH, and DELETE endpoint.
  • library:read: your private library items (scope=mine listings and private items) and every download, official items included. Browsing public and official items needs no token.
  • library:write: publishing, versioning, editing and deleting library items.
  • openid, profile, email: standard OIDC scopes, registered on the provider but not checked by any community route.
  • offline_access: required to actually receive a refresh_token in the token response. Without it, the authorization code exchange only ever returns an access_token — the server silently omits the refresh token even if you requested the refresh_token grant type at registration.

Scopes don't compose — community:write does not imply community:read, and requesting refresh_token as a grant type doesn't imply offline_access as a scope. See Errors & conventions for exactly how a token vs. a browser session is checked.

Desktop apps: the MonoAgent client

MonoAgent (desktop app and monoagentcli) signs in with a fixed public client, client_id=monoagent, instead of registering one. It uses the native-app pattern from RFC 8252: authorization code + PKCE (S256, required, no client secret), with the redirect going to a one-off listener on the loopback interface.

  • Redirect URI: http://127.0.0.1:<any port>/callback (or [::1]). The port can change on every login (RFC 8252 §7.3); the host must be the literal IP, not localhost, and the path must be /callback.
  • Scopes: openid profile email offline_access library:read library:write. offline_access returns a refresh token; refresh with grant_type=refresh_token&client_id=monoagent.
  • After login, GET /api/library/me returns the account and granted scopes. Machines without a browser can use the email-code flow with client_id=monoagent.
Authorization URL (MonoAgent)
https://monoes.me/api/auth/oauth2/authorize
  ?client_id=monoagent
  &redirect_uri=http://127.0.0.1:53682/callback
  &response_type=code
  &scope=openid+profile+email+offline_access+library:read+library:write
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
  &state=RANDOM_STATE
curl
curl -X POST https://monoes.me/api/auth/oauth2/token \
  -d "grant_type=authorization_code" \
  -d "code=THE_CODE_FROM_THE_CALLBACK" \
  -d "redirect_uri=http://127.0.0.1:53682/callback" \
  -d "client_id=monoagent" \
  -d "code_verifier=YOUR_CODE_VERIFIER"
# -> { "access_token": "...", "refresh_token": "...", "token_type": "Bearer", "scope": "..." }

The library endpoints are listed under API reference → Library.

MonoAgent tokens: audience and claims

Send resource=https://monoes.me/api/monoagent (RFC 8707) on the authorization request, the token request and every refresh, and the access token is a signed JWT bound to that audience instead of an opaque string. Send no resource and nothing changes: the token is opaque, as it always was. Only the monoagent client may ask for this resource; any other client is answered invalid_target. A refresh token that was issued without a resource can be exchanged with one.

curl
curl -X POST https://monoes.me/api/auth/oauth2/token \
  -d "grant_type=refresh_token" \
  -d "client_id=monoagent" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "resource=https://monoes.me/api/monoagent"
# -> { "access_token": "eyJ...", "refresh_token": "...", "expires_in": 3600, ... }
  • Header: alg is EdDSA (Ed25519), typ is at+jwt, and kid names a key published at GET /api/auth/jwks. The signing key is a fixed key that monoes.me supplies; it is not generated or rotated automatically, so a client can pin its public key. During a planned rotation the previous public key stays published for a while, so tokens issued just before the change keep verifying.
  • iss is https://monoes.me/api/auth. aud is an array that contains https://monoes.me/api/monoagent, next to the userinfo endpoint when openid is requested.
  • azp and client_id are monoagent. sub is the user id. scope holds the granted scopes, space-separated.
  • iat and exp are one hour apart (exp - iat is 3600). jti is unique per token.
  • plan is free for every account today. sid, when present, names the web session the sign-in went through, and is null once that session has ended: do not rely on it.

Refresh tokens rotate: each refresh returns a new one and the old one stops working. Retrying a refresh whose answer was lost, within five minutes, returns that same answer again; presenting a used refresh token after that revokes the refresh tokens of that sign-in, so sign in again on that machine. The other sign-ins of the account keep working. Revoking a refresh token at /api/auth/oauth2/revoke ends its sign-in the same way. A refresh that is answered invalid_grant means the sign-in cannot continue: its tokens were revoked, they expired after 30 days, or the account is blocked. Every other failure (invalid_target, a 5xx, no network) says nothing about the account. Blocking an account deletes its refresh tokens, opaque access tokens and web sessions in one step, and a blocked account is never issued a new token. A JWT that was already issued is a signed statement and is not looked up again: ending a sign-in does not cancel it, it stays valid until its exp (within the hour), except that the library and community API answer a blocked account 403 with the code blocked in the meantime.

Headless agents (no browser)

Agents that can't open a browser at all can instead relay a one-time code emailed to the account owner, in exchange for a scoped access token identical in shape to the OAuth-issued one.

  1. Register a client the same way as OAuth, if you don't already have one.
  2. POST /api/auth/agent/claim : body { email, client_id, scope }. Rate limited to 3 outstanding requests per email per hour.
  3. If the email matched an account, a 6-digit code arrives by email. The user relays it to you.
  4. POST /api/auth/agent/claim/verify : body { email, code, client_id }. Returns { access_token, token_type: "Bearer", expires_in: 3600, scope }. For the monoagent client and a claim whose scope includes offline_access the answer also carries a refresh_token, which can be exchanged at the token endpoint with the resource above. Adding that resourceto the body gets the token endpoint's answer at once: an audience-bound JWT and a refresh_token. Codes expire after 10 minutes and allow at most 5 attempts.
curl
# 1. Request a code
curl -X POST https://monoes.me/api/auth/agent/claim \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com", "client_id": "YOUR_CLIENT_ID", "scope": "community:read community:write" }'
# -> 200 { "message": "If this email is registered, a verification code has been sent." }
#    (always 200 here whether or not the email is registered, to avoid leaking which emails exist)

# 2. Exchange the code the user relayed to you
curl -X POST https://monoes.me/api/auth/agent/claim/verify \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com", "code": "123456", "client_id": "YOUR_CLIENT_ID" }'
# -> 200 { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "community:read community:write" }

Failure responses to plan for:

  • 400 { "error": "invalid_request" } — malformed email, missing/empty client_id, a scope outside the registered set, or an unrecognized client_id. (/claim only.)
  • 429 { "error": "rate_limited" } — 4th outstanding claim for the same email within an hour. (/claim only.)
  • 400 { "error": "invalid_or_expired_code" } — wrong code, expired (10 min), or the claim already hit 5 failed attempts. (/claim/verify only — same response for every failure mode, so don't branch client-side logic on the distinction.)

The resulting token behaves identically to an OAuth-issued one everywhere else in the API — it's not a second-class credential.

MCP server

The same API is also available as MCP tools, for agents that speak MCP instead of REST directly. Same bearer token, same scopes — see MCP server for the endpoint and the full tool list.

Rate limits

No documented rate limits beyond standard platform-level protections. Accounts found abusing the API may be blocked, which invalidates sessions and revokes further OAuth token use.