Security and limits

What a personal access token is

A token looks like fpat_ plus 43 characters plus a 6-character checksum — 54 characters total. It is not an app JWT: it only works on the /v1/mcp endpoint and nowhere else — Fealthy's GraphQL API, WebSocket, and chat all refuse it. With no token, or an invalid one, the server answers 401 with the header WWW-Authenticate: Bearer realm="fealthy", error="invalid_token".

Scope — your data only

A token is bound to one user, and that's checked on the server for every single call. Reaching for someone else's record (for example, substituting a foreign uid) returns the exact same not_found error as a request for a record that doesn't exist — the server never confirms, by the mere fact of its answer, that a foreign record even exists.

Premium-only access

MCP is available only on Fealthy's Premium plan. Demo accounts are always blocked, regardless of any other setting. On a Free plan, a call returns a JSON-RPC forbidden error with reason plan_restricted — and deliberately on HTTP 200: the reference MCP client only looks for a JSON-RPC error inside a 2xx response, so a non-2xx status here would read to the agent as an opaque transport failure rather than an explanation.

Token access levels

A token is minted at one of two levels:

  • Write — all 70 tools;
  • Read — only list_*/get_*, 29 tools; calling a write tool is rejected, and tools/list for a token at this level won't show anything but reads.

Rate limit

50 operations per 60 seconds, per user account, shared across all of that user's tokens — a second token doesn't add budget. A batch call with N items (for example, create_transaction with several rows at once) counts as N operations; but create_transfer and create_exchange, even though each creates two transactions, count as one operation. Remaining budget rides back in the _meta["ua.fealthy/rateLimit"] field of every response. Exceeding the limit returns a rate_limited error with a retryAfterSeconds field — exactly how long to wait before retrying.

uid, never a numeric id

Every record is identified only by uid (a UUID string); numeric ids don't exist in this API. The server generates the uid of a new record itself — a create call that supplies its own uid is rejected with validation_error.

Global (system) records

Built-in categories and merchants are shared across all users and read-only: trying to delete one returns forbidden with reason global_read_only. Renaming a system category or changing its icon is still possible — it creates a personal override just for you: other users and the shared record itself stay unchanged, and the category's English name never changes. Hiding a system category (the same "eye" toggle as in the app's settings) is also possible, and reversible.

Error shape

A tool error comes back not as an HTTP error but inside an ordinary, successful JSON-RPC response: isError: true and a code in structuredContent.error.code. Real HTTP errors are reserved for authentication, request method, and body format (401, 405, and the like).

CodeWhen it happens
unauthorizedThe token is missing or invalid
forbiddenThe action is not allowed; the reason is in the reason field (global_read_only, plan_restricted, demo_user, write_access_required for a Read token, feature_disabled)
not_foundThe uid doesn't exist, or belongs to another user
validation_errorInvalid call parameters; the field name is in error.field
conflictThe action conflicts with the record's current state
rate_limitedThe rate limit was exceeded; the wait time is in retryAfterSeconds
internalAn internal server error; retrying the call later may help

Timezone and language

Timezone resolves in this order: your Fealthy profile setting → else the app-timezone request header → else Europe/Kyiv. A date or time you send with no explicit offset is interpreted in that timezone; every date the server returns always carries an offset.

The response language is your Fealthy profile's interface language: the server directly instructs the agent to answer in it.

What the server tells the agent

Along with the tool list, the server sends the agent explicit instructions — rules it's expected to follow:

  1. Records have only a uid; numeric ids don't exist — never invent or ask for one.
  2. Look for an existing record first via list_*/get_* and use it — create only what's actually missing.
  3. The server generates the uid of a new record; supplying your own is an error.
  4. Before any delete_* call or other irreversible change, tell you exactly what will be deleted or changed and wait for your explicit confirmation.
  5. Global records can be read and referenced, but never changed or deleted.
  6. Dates with no offset are in your timezone; every date in a response carries an offset.
  7. Answer in your Fealthy profile's language.
  8. Don't exceed the rate limit, and don't retry immediately after a rate_limited error — wait for retryAfterSeconds.
  9. Don't repeat, unchanged, a call that failed with validation_error or forbidden.
  10. Remember its own token's level (Write or Read) and never offer an action that level doesn't allow.

Revoking a token

There's no self-service revocation in the app yet — ask the Fealthy team to revoke the token. A revoked token stops working within 60 seconds at the latest (the lifetime of the internal token-verification cache).

Where your data goes

The MCP endpoint hands your own data directly to whichever client you connected — one HTTP call, no intermediary services. Fealthy logs the fact of the call itself (which tool, whether it succeeded, what it cost against the rate limit, how long it took) — not the call's arguments and not the content of the response. What the client or agent does with the data afterward is outside Fealthy's control: choose a client you trust, and guard the token as carefully as a password.