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-resourceRFC 9728 Protected Resource Metadata.GET /api/auth/.well-known/oauth-authorization-serverRFC 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 -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:
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=S256The 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 -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 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.openid,profile,email: standard OIDC scopes, registered on the provider but not checked by any community route.offline_access: required to actually receive arefresh_tokenin the token response. Without it, the authorization code exchange only ever returns anaccess_token— the server silently omits the refresh token even if you requested therefresh_tokengrant 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.
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.
- Register a client the same way as OAuth, if you don't already have one.
POST /api/auth/agent/claim: body{ email, client_id, scope }. Rate limited to 3 outstanding requests per email per hour.- If the email matched an account, a 6-digit code arrives by email. The user relays it to you.
POST /api/auth/agent/claim/verify: body{ email, code, client_id }. Returns{ access_token, token_type: "Bearer", expires_in: 3600, scope }. Codes expire after 10 minutes and allow at most 5 attempts.
# 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/emptyclient_id, a scope outside the registered set, or an unrecognizedclient_id. (/claimonly.)429 { "error": "rate_limited" }— 4th outstanding claim for the same email within an hour. (/claimonly.)400 { "error": "invalid_or_expired_code" }— wrong code, expired (10 min), or the claim already hit 5 failed attempts. (/claim/verifyonly — 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.