> For the complete documentation index, see [llms.txt](https://docs.t-rex.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.t-rex.network/guides/identity/22.-add-and-remove-keys-on-an-onchainid.md).

# 22. Add and remove keys on an ONCHAINID

The keys of an ONCHAINID decide who controls it and who acts for it. The Identity Owner adds a key to enroll a new device, a passkey, an operator or a signing service, and removes a key when a device is lost or a person leaves. Both are management operations: they change who holds authority over the identity, and only a `MANAGEMENT` key can make them.

## At a glance

|                   |                                                                                                                                                                                |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Triggered by**  | Identity Owner                                                                                                                                                                 |
| **Actors**        | Identity Owner, Platform                                                                                                                                                       |
| **Contracts**     | ONCHAINID, `ERC734Validator`                                                                                                                                                   |
| **Preconditions** | The identity exists (see [02](/guides/identity/02.-deploy-an-onchainid.md)); the owner controls a `MANAGEMENT` key of the identity; for a new signer, its signer data is known |
| **Result**        | The identity's key set reflects the change: a signer holds a new purpose, or no longer holds one                                                                               |

## Actors and roles

| Actor          | Role in this flow                                            | What they hold                         |
| -------------- | ------------------------------------------------------------ | -------------------------------------- |
| Identity Owner | Decides the change and authorizes it.                        | A `MANAGEMENT` key of the identity     |
| Platform       | Encodes the signer, relays the call and verifies the result. | Whatever keys the owner has authorized |

## Sequence

```mermaid
sequenceDiagram
    participant Platform
    participant Owner as Identity Owner
    participant Identity as ONCHAINID
    participant Validator as ERC734Validator

    Platform->>Identity: getKey(keyHash), getKeysByPurpose(purpose) (read)
    Platform->>Owner: change prepared, submit
    alt add a key or a purpose
        Owner->>Identity: addKeyWithData(keyHash, purpose, keyType, signerData, clientData)
        Identity->>Identity: caller holds MANAGEMENT, hash matches signer data
        Identity->>Validator: addKey
        Validator->>Validator: signer and purpose valid, not already held
        Validator-->>Platform: KeyAdded
    else remove a purpose
        Owner->>Identity: removeKey(keyHash, purpose)
        Identity->>Identity: caller holds MANAGEMENT
        Identity->>Validator: removeKey
        Validator->>Validator: key registered, not the last manager, purpose held
        Validator-->>Platform: KeyRemoved
    end
```

## Step by step

### Step 1. Prepare the change

**Who:** Platform, on behalf of the Identity Owner

**Call:** Off-chain, using read calls

**Before the call:** The owner knows which signer gains or loses which purpose.

**What happens:** For a new signer, the Platform builds its signer data: the 20 bytes of an address for an externally owned account or a smart-contract wallet, or the address of a verifier followed by the public key for a passkey or another scheme. It computes `keyHash` as `keccak256(signerData)`. It reads `Identity.getKey(keyHash)` to see whether the signer is already registered and with which purposes, and `Identity.getKeysByPurpose(purpose)` to see who already holds the purpose. Before removing a `MANAGEMENT` purpose, it reads `Identity.getKeysByPurpose(1)` and confirms that another manager remains.

**What changes:** Nothing on-chain.

### Step 2. Add a key, or a purpose to a key

**Who:** Identity Owner (`MANAGEMENT` key)

**Call:** `Identity.addKeyWithData(keyHash, purpose, keyType, signerData, clientData)` for a signer that is not registered yet. `Identity.addKey(keyHash, purpose, keyType)` to give another purpose to a signer that is already registered.

**Before the call:** Step 1 is done. The call is made by a `MANAGEMENT` key directly on the identity, or by the identity itself in a user operation signed by a `MANAGEMENT` key.

**What the contract checks:**

1. The caller holds `MANAGEMENT` on the identity, or is the identity itself. Otherwise the call reverts with `SenderDoesNotHaveManagementKey`.
2. For `addKeyWithData`: `keyHash` equals `keccak256(signerData)`. For `addKey`: the key is already registered. Otherwise `InvalidSignerData`.
3. The signer data is at least 20 bytes and within the maximum length, and the client data is within its maximum. Otherwise `InvalidSignerLength` or `ClientDataTooLong`.
4. The purpose is one of the six. Otherwise `InvalidPurpose`.
5. The signer is not the key registry module itself. Otherwise `ModuleCannotBeKey`.
6. For a signer already registered: `keyType` is the type it was registered with. Otherwise `KeyTypeMismatch`.
7. The signer does not already hold this purpose. Otherwise `KeyAlreadyRegistered`.

**What changes:** The registry records the signer if it is new, and adds the purpose to it. It emits `KeyAdded(identity, keyHash, purpose, keyType, signerData, clientData, caller)`. From the next call, the signer has the authority of that purpose.

### Step 3. Remove a purpose from a key

**Who:** Identity Owner (`MANAGEMENT` key)

**Call:** `Identity.removeKey(keyHash, purpose)`

**Before the call:** Step 1 is done. To strip a signer entirely, the owner removes each of its purposes.

**What the contract checks:**

1. The caller holds `MANAGEMENT` on the identity, or is the identity itself. Otherwise `SenderDoesNotHaveManagementKey`.
2. The key is registered. Otherwise `KeyNotRegistered`.
3. When the purpose is `MANAGEMENT` and the key is a signer: another `MANAGEMENT` signer remains. Otherwise `CannotRemoveLastManagementKey`.
4. The key holds the purpose. Otherwise `KeyDoesNotHavePurpose`.

**What changes:** The purpose is removed from the key. When it was the key's last purpose, the key's record is deleted. The registry emits `KeyRemoved(identity, keyHash, purpose, caller)`. From the next call, the signer no longer has that authority. Requests it had queued in the execution queue can no longer be approved.

## State after the flow

* `Identity.keyHasPurpose(keyHash, purpose)` reflects the change.
* `Identity.getKeysByPurpose(purpose)` includes or no longer includes the key.
* `Identity.getKey(keyHash)` returns the key's purposes, type and signer data, or nothing if the key was deleted.
* The identity always has at least one `MANAGEMENT` key that can sign.
* The identity's claims, linked wallets, positions and eligibility are unchanged.

## Failure modes

| Symptom                                                   | Cause                                                                                                                 | Fix                                                                          |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Reverts with `SenderDoesNotHaveManagementKey`             | The caller holds `ACTION` or another purpose, not `MANAGEMENT` (check 1)                                              | Use a `MANAGEMENT` key                                                       |
| A user operation that adds a key is rejected              | It was signed by an `ACTION` key. Calls to the identity itself are management operations                              | Sign with a `MANAGEMENT` key                                                 |
| Reverts with `InvalidSignerData`                          | `keyHash` is not the hash of `signerData`, or `addKey` was used for a signer that is not registered (step 2, check 2) | Recompute the hash. Use `addKeyWithData` for a new signer                    |
| Reverts with `InvalidSignerLength`                        | The signer data is shorter than 20 bytes or too long (step 2, check 3)                                                | Encode an address as its 20 bytes. Encode other signers as verifier then key |
| Reverts with `InvalidPurpose`                             | The purpose is not between 1 and 6 (step 2, check 4)                                                                  | Use a defined purpose                                                        |
| Reverts with `KeyTypeMismatch`                            | The type given differs from the one the key was registered with (step 2, check 6)                                     | Pass the registered type, read with `getKey`                                 |
| Reverts with `KeyAlreadyRegistered`                       | The signer already holds this purpose (step 2, check 7)                                                               | Nothing to do                                                                |
| Reverts with `KeyNotRegistered`                           | The key is not on the identity (step 3, check 2)                                                                      | Check the hash. For an address, it is the hash of the 20 address bytes       |
| Reverts with `CannotRemoveLastManagementKey`              | The key is the identity's last manager that can sign (step 3, check 3)                                                | Add another `MANAGEMENT` key first                                           |
| Reverts with `KeyDoesNotHavePurpose`                      | The key does not hold the purpose (step 3, check 4)                                                                   | Read `getKeyPurposes(keyHash)`                                               |
| A passkey was added and cannot call the identity directly | A direct call is authorized from the caller's address, and a passkey has none                                         | A passkey acts through user operations                                       |

## Common misunderstandings

> **Wrong:** "Adding a key links a wallet."
>
> **Right:** Keys and links are separate. A key gives a signer authority over the identity. A link attributes an address, and what it holds, to the identity. An address can be a key and not a linked wallet, or the reverse.

> **Wrong:** "An `ACTION` key can add another `ACTION` key."
>
> **Right:** Every change to the key set is a management operation. An `ACTION` key acts toward other contracts, and has no route to the identity's own keys, through a user operation, a direct call or the execution queue.

> **Wrong:** "Removing a key removes the signer."
>
> **Right:** `removeKey` removes one purpose. A signer that holds `MANAGEMENT` and `ACTION` keeps the other after one call. The record is deleted when the last purpose goes.

> **Wrong:** "The key hash of an address is the hash of its ABI encoding."
>
> **Right:** A key hash is `keccak256` of the signer data. For an address, the signer data is the 20 address bytes, not the 32-byte ABI encoding that earlier versions hashed.

## Integration notes

* Enroll a second `MANAGEMENT` signer early, on another device. An identity with one manager and no guardians is lost with that key.
* Use `addKeyWithData` for every new signer. `addKey` only adds a purpose to a signer the identity already knows.
* Give each key the least purpose it needs: `ACTION` for daily use, `CLAIM_SIGNER` for a signing service, `PROPOSER` for an automated system.
* When a device is lost, remove each purpose of its key. Then review requests pending in the execution queue.
* Index `KeyAdded` and `KeyRemoved` on the `ERC734Validator`, filtered by identity. They carry the caller, so every change is attributable.
* A Claim Issuer rotates its signing keys with this flow. Removing `CLAIM_SIGNER` from a key invalidates every claim that key signed, at once.

## Related flows

* [02 Deploy an ONCHAINID](/guides/identity/02.-deploy-an-onchainid.md) : sets the first keys
* [21 Recover access to an ONCHAINID](/guides/identity/21.-recover-access-to-an-onchainid.md) : when no `MANAGEMENT` key is left
* [07 Link a wallet to an ONCHAINID](/guides/identity/07.-link-a-wallet-to-an-onchainid.md) : links are not keys
* [19 Revoke a claim](/guides/claim-issuer/19.-revoke-a-claim.md) : the effect of rotating a Claim Issuer's signing key


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.t-rex.network/guides/identity/22.-add-and-remove-keys-on-an-onchainid.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
