# Access: accounts, Personal Alias and memories

Use `https://api.decoys.me` for HTTP requests. The [MCP quickstart](/docs/memory-quickstart.md) provides six memory tools and level-based access. Inspect `tools/list` for exact inputs; enable optional graph tools with `DECOY_MEMORY_GRAPH=true`.

Choose the account, Personal Alias or memories the task needs, then one level: **Read**, **Read & write**, or **Full access**. Explain that choice briefly to the person. Combine already-agreed access in one approval. Personal Info continues to use its documented actions and `datatypes`.

## What a level means

| Level | Account and its own inbox | Personal Alias | Memories |
| --- | --- | --- | --- |
| `read` | Details, passwords/custom fields, TOTP, passkey sign-in and reading mail | Read mail | Read |
| `read_write` | Read plus editing the account, sending/replying and forwarding | Read plus sending/replying | Read and edit |
| `full` | Read & write plus deleting the account/mail and burning its address | Read & write plus deleting mail; no burning, renaming or rerouting the alias | Read, edit and delete |

**Read includes credential access.** Do not describe it as metadata-only. Retrieve only what the agreed task needs. An account's inbox comes with its level; its Personal Alias does not, even if the alias is used as that account's login. Account grants also bring memories about those accounts.

To read a granted password, call the bridge's `credential_get` with the site's `domain` or the account `id`. Over HTTP, `GET /api/agent/sealed-secrets` returns each covered secret sealed to your agent key; decrypt it locally.

Saving memories is a separate permission, Save memories (below). No level includes it.

Grants are live sets: the server evaluates which items match at the time of access. A category or topic grant is not a frozen list of today's memories. The owner may narrow the request before approving. Read the final grant, not your original request.

## Exact request rows

Put these rows in `authorization_details` for `POST /oauth/par` or `POST /api/connect`. They are alternatives to select for the agreed task, not a default bundle:

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"account","level":"read"}
```

No selector means **all accounts**. Use `account_id` for a known account, or `domain` as a request-time hint resolved by the owner's phone. Do not list private accounts before access or invent a generic account picker.

When the task names a site but you have no account ID, send its `domain`:

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"account","domain":"github.com","level":"read"}
```

The owner's phone matches the domain to their saved account for that site and grants that account. If the phone finds no account for the domain, the row grants nothing. It never widens to every account.

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"account","account_id":"<account UUID>","level":"read_write"}
```

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"message","namespace_id":"<namespace UUID>","level":"read"}
```

For every Personal Alias use `personal_aliases: true` instead of `namespace_id`. Claiming a new Personal Alias requires every Personal Alias at `read_write` or `full`; Create accounts alone does not permit it.

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"memory","level":"read","datatypes":["food","travel"]}
```

For topics, replace `datatypes` with `about: ["topic:<entity ID>"]`. Omit both for every memory. Send separate rows for categories and topics, never both selectors on one row. Memory grants imply no token scopes. Personal Info is not a memory permission.

To save memories, request **Save memories**:

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"memory","actions":["create"]}
```

It takes no level, categories or topics. Every connected agent reads the same memory store, so saving needs this permission. It opens no one else's memories. A connection that only saves and recalls what the person tells it needs this one row.

Send `level` OR `actions`, not both. Save memories is the only memory row with `actions`. Account levels cover every field, so do not add `datatypes` to an account level. `created_by_self` takes no level. Older action-based requests remain supported, but new integrations should lead with levels.

## Create accounts and implicit access

To create new accounts and their addresses, request:

```json
{"type":"https://decoys.me/oauth/types/unlock","kind":"account","actions":["create"]}
```

What this connection creates is implicitly its own: it can read, reveal, edit and delete its accounts and addresses, and read, correct and delete its saved memories. Password verification requires an independent read through an already-authorized content/key path; a write receipt alone confirms storage, not recovered content. Do not broaden access for verification. This does not include passkey sign-in. Saving a memory needs Save memories; fixing or deleting one it saved does not. Do not request separate created-only account, inbox or memory approvals.

Create accounts derives `account.read account.write account.unlock decoy.read decoy.write message.read message.unlock`. The resource gate still limits this authority to its creations. Password reveal is authorized for its created accounts. Verify an independent read through an already-authorized content/key path before claiming recovered content; do not broaden access for verification. Capability scopes and key availability still apply to each operation.

A scoped connection has no whole-vault key. Create the account **with its address** through `POST /api/agent/decoys`, using a non-sensitive `service_name` and optional `service_url`/`label`. Persist `account_id`, the address ID and `decoy_email`. Creation in Decoy is not registration at the external service.

Account reads (`GET /api/agent/accounts` and `/api/agent/accounts/{id}`) include **`metadata_seal`**: a base64 envelope sealed to this connection's key, opening to `{ serviceName, serviceURL }`. It is null when this connection has no seal, and never contains another connection's seal. It uses the envelope framing documented in OpenAPI; it is not the account's `encrypted_data` vault record. The service name is submitted to the server for sealing; never put secrets or memories in it.

Save a password through `POST /api/agent/accounts/{id}/credential` with `{"encrypted_password": "..."}`: base64 of a headerless envelope to `public_key` from `GET /api/agent/accounts/public-key` (SPKI DER base64), `[2-byte BE key length][RSA-OAEP-SHA256 wrapped AES-256 key][12-byte IV][AES-256-GCM ciphertext][16-byte tag]`, whose plaintext is the password's UTF-8 bytes. No SealedBlob header, no JSON wrapper: `sealPassword()` in [agent-memory.mjs](https://www.decoys.me/examples/agent-memory.mjs) does exactly this. The server can't read it to check it, so a wrong format is stored and the owner's phone can't open it. A second call replaces the stored password. Follow the [storage guide](/agent-storage.md) for the full initial-password request and its verification steps. Account permission does not create a missing vault-record key. Username, notes, tags, category, login URL and custom/structured fields remain in that sealed record; this no-vault-key path cannot write those fields. Do not re-pair with broader access to bypass that limitation. Memories use their own supported store.

The MCP bridge handles this for `account_create`, `account_update`, `account_get` and `account_list` in its advanced catalog. A connection without the vault key renames an account through its address: `GET /api/agent/accounts/{id}` gives `decoy_id`, then `PATCH /api/agent/decoys/{decoy_id}` with `{"service_name":"..."}`. On its own account it can also `PATCH /api/agent/accounts/{id}` with `two_factor_enabled`, `agent_label`, `status` (and `expected_rev`); username, notes, URLs and custom fields need the vault key. Default `decoy_list` and advanced `decoy_get` return locally decrypted `service_name` / `service_url`, or null without a matching key. Check `tools/list` rather than calling a tool absent from your runtime.

## Verify and extend the connection

For API keys, other hidden values, security answers and typed contact/address/card fields, use the [field catalog and recipes](/agent-fields.md). A field available in the app is not proof the current agent has its encrypted write/read path. Preserve the original account and distinguish an acknowledged write from a readable field visible to the owner.

The token response and approved connect poll return granted `authorization_details`, including rows marked `implicit`. `GET /api/agent/policies` returns approved permissions under `policies` and a separate top-level **`implicit`** array. Inspect both, the token scopes, expiry and each row's selectors. Level rows also return expanded `actions`; these response fields do not mean request rows should mix level and actions.

Aggregate `reach` describes the combined request. An intentionally broad Personal Info row can make it `account_wide` without widening a specific account row. Keep one approval and verify row by row. An unrequested broad account or Personal Alias row is a reason to stop and correct the request.

For new work, `POST /api/agent/grant-request` accepts a level on the existing connection:

```json
{"kind":"account","resource_id":"<account UUID>","level":"read_write","reason":"Manage the service settings you requested"}
```

```json
{"kind":"memory","level":"read","datatypes":["travel"],"reason":"Use your saved travel preferences for this trip"}
```

```json
{"kind":"memory","action":"create","reason":"Remember what you tell me"}
```

Ask for Save memories when `POST /api/agent/context/memories` (or `add_memory`) returns `403 INSUFFICIENT_GRANT`. Its `remediation` names this exact call.

For every Personal Alias, use `{kind:"message", select:"personal_aliases", level:"read"}`; for one use `namespace_id`. A normal request returns pending status, `request_id`, `expires_in: 600`, the level and any `requested_scopes`. A created-by-self request returns `200` with `status: "implicit"` and no pending approval. Avoid making that request in the first place. Duplicate pending requests return `409 GRANT_ALREADY_REQUESTED`.

`authorization_details` alone supplies exactly its implied scopes, even none for memory-only access. Do not attach a broad scope string by habit. For scope-only requests: `account.read` alone selects no accounts; `account.write`/`decoy.write` selects Create accounts; `account.unlock`/`account.passkey_assert` selects all accounts at Read; message scopes select account inboxes, not passwords or Personal Alias. Use explicit rows instead.

Revocation/narrowing stops future API access. It cannot erase keys, plaintext or envelopes already received. Do not promise retroactive erasure. Approved reconnect carries active grants and creation provenance; a brand-new pairing does not.
