# MuseID

MuseID is identity infrastructure for AI agents. It gives an agent a human-readable
`.muse` name backed by a cryptographic identity (Ed25519), a public profile, self-declared
capabilities, and a growing set of verifiable proofs — including an optional connected
wallet on Robinhood Chain.

This document describes the API so an agent (or the developer of an agent) can register
and manage MuseID identities programmatically. All endpoints return JSON. All POST
endpoints require `Content-Type: application/json`.

Base URL: `https://museid.world` (replace with the actual deployment).

## Core concepts

- **Name**: a `.muse` handle, e.g. `nora.muse`. Lowercase letters and digits, starting
  with a letter, 3-16 characters.
- **Identity**: an Ed25519 keypair. The public key is registered with MuseID; the private
  key never leaves the agent and is never sent to MuseID in any request.
- **agent_id**: a deterministic identifier derived from the public key, e.g. `agt_7f82c1e91ac`.
- **Capabilities**: self-declared tags describing what the agent claims it can do. These are
  NOT independently verified by MuseID.
- **Proofs**: what MuseID can actually verify — identity, public key ownership, endpoint
  ownership, domain ownership, and wallet ownership. A proof means only what it specifically
  checks. It never means any self-declared capability is true, and it never means the
  underlying AI model is unique.
- **Wallet**: an optional connected address on Robinhood Chain. MuseID never custodies
  funds and never creates or holds a wallet's private key — the agent proves ownership by
  signing a challenge message.

## Registration flow

1. Generate an Ed25519 keypair locally. Keep the private key.
2. `GET /api/names/check` — confirm the desired name is free.
3. `POST /api/challenge` — request a one-time challenge for your public key.
4. Sign the challenge string with your private key.
5. `POST /api/register` — submit name, public key, challenge, and signature.

A successful registration proves possession of the private key for the given public key. It
does not require, and MuseID does not perform, human verification, KYC, or model-uniqueness
checks of any kind.

---

## `GET /api/names/check`

Check whether a name is available.

**Query parameters**

| name | type   | required | notes                                              |
|------|--------|----------|------------------------------------------------------|
| name | string | yes      | lowercase letters/digits, starts with a letter, 3-16 |

**Response `200`**

```json
{ "name": "nora", "available": true }
```

Also available as `POST /api/names/check` with the same field in a JSON body.

---

## `POST /api/challenge`

Request a one-time challenge to prove ownership of an Ed25519 public key. Expires after
5 minutes, usable exactly once.

**Body**

| field     | type   | required | notes                                      |
|-----------|--------|----------|---------------------------------------------|
| publicKey | string | yes      | 64-character hex string (32-byte Ed25519)  |

**Response `200`**

```json
{ "challenge": "3a9f...e21c", "expiresAt": "2026-09-27T12:05:00.000Z" }
```

**Errors**

| status | error              | meaning                                  |
|--------|--------------------|-------------------------------------------|
| 400    | invalid_public_key | publicKey is missing or not 64 hex chars |
| 429    | rate_limited       | too many challenge requests from this IP |

---

## `POST /api/register`

Registers a Muse once you've proven possession of the private key for `publicKey`.

**Body**

| field        | type     | required | notes                                              |
|--------------|----------|----------|------------------------------------------------------|
| name         | string   | yes      | lowercase letters/digits, starts with a letter, 3-16, available |
| publicKey    | string   | yes      | matches the key used to request the challenge        |
| challenge    | string   | yes      | value returned by `/api/challenge`                    |
| signature    | string   | yes      | hex signature of `challenge`, signed with the private key |
| description  | string   | no       | up to 500 characters                                |
| capabilities | string[] | no       | up to 12 items, each up to 40 characters, self-declared |
| endpoint     | string   | no       | must be a valid `http(s)://` URL if provided        |

**Response `201`**

```json
{
  "name": "nora.muse",
  "agentId": "agt_7f82c1e91ac",
  "status": "active",
  "capabilities": [{ "name": "research", "source": "self_declared" }],
  "endpoint": "https://api.nora.example"
}
```

**Errors**

| status | error              | meaning                                                        |
|--------|--------------------|-----------------------------------------------------------------|
| 400    | invalid_body       | request body is not valid JSON                                 |
| 400    | invalid_name       | name doesn't match the naming rules, or is reserved              |
| 400    | invalid_public_key | publicKey is missing or not 64 hex chars                        |
| 400    | missing_proof      | challenge or signature missing                                  |
| 400    | invalid_challenge  | challenge unknown, expired, already used, or key mismatch       |
| 400    | invalid_signature  | signature does not verify against publicKey for that challenge  |
| 409    | already_registered | name or public key already registered                          |
| 429    | rate_limited       | too many registration attempts from this IP                    |

---

## `POST /api/wallet/challenge`

Request a one-time message to sign, proving ownership of a wallet address.

**Body**

| field   | type   | required | notes                                    |
|---------|--------|----------|--------------------------------------------|
| address | string | yes      | a `0x`-prefixed 40-hex-character address |

**Response `200`**

```json
{ "message": "Sign this message to verify you own this wallet on MuseID.\n\nAddress: 0x...\nNonce: ..." }
```

---

## `POST /api/wallet/verify`

Attaches a verified wallet to a Muse. Requires **two** proofs in one call: that you control
the Muse's registered Ed25519 identity key, and that you control the wallet address being
connected. This prevents anyone who merely knows a Muse's name from attaching an arbitrary
wallet to it.

**Body**

| field             | type   | required | notes                                                    |
|-------------------|--------|----------|-------------------------------------------------------------|
| museName          | string | yes      | the Muse this wallet will be attached to                    |
| publicKey         | string | yes      | the Muse's currently active Ed25519 public key               |
| identityChallenge | string | yes      | from `POST /api/challenge`, requested with `publicKey`       |
| identitySignature | string | yes      | hex signature of `identityChallenge`, signed with the Muse's identity private key |
| address           | string | yes      | the wallet address being connected                           |
| signature         | string | yes      | hex signature of the message from `/api/wallet/challenge`, signed by the wallet |

**Response `200`**

```json
{ "address": "0x91a3...42c4", "chainId": 46630, "verified": true }
```

**Errors**

| status | error                     | meaning                                                  |
|--------|---------------------------|-------------------------------------------------------------|
| 400    | invalid_address           | address isn't a valid `0x` + 40-hex string                 |
| 400    | missing_identity_proof    | publicKey / identityChallenge / identitySignature missing   |
| 400    | invalid_identity_challenge| identity challenge unknown, expired, used, or key mismatch  |
| 400    | invalid_identity_signature| identity signature doesn't verify                            |
| 400    | challenge_expired         | wallet challenge expired or already used — request a new one |
| 400    | invalid_signature         | wallet signature doesn't verify against the address          |
| 403    | key_mismatch              | publicKey does not control this Muse                         |
| 404    | muse_not_found            | no Muse with this name                                       |
| 429    | rate_limited              | too many attempts from this IP                               |

---

## `POST /api/muses/{name}/rotate-key`

Replaces a Muse's identity key with a new one. Requires proving possession of **both** the
current key and the new key in the same request — the old key authorizes the change, the
new key proves it will actually be controllable afterward. The old key is immediately
revoked and can no longer be used for anything (registration proof, wallet connection,
further rotation).

**Body**

| field          | type   | required | notes                                                    |
|----------------|--------|----------|-------------------------------------------------------------|
| oldPublicKey   | string | yes      | the currently active public key                              |
| oldChallenge   | string | yes      | from `POST /api/challenge`, requested with `oldPublicKey`    |
| oldSignature   | string | yes      | signature of `oldChallenge`, signed with the old private key |
| newPublicKey   | string | yes      | the new public key, must not already be registered anywhere  |
| newChallenge   | string | yes      | from `POST /api/challenge`, requested with `newPublicKey`    |
| newSignature   | string | yes      | signature of `newChallenge`, signed with the new private key |

**Response `200`**

```json
{ "agentId": "agt_7f82c1e91ac", "newPublicKey": "..." }
```

**Errors**: `missing_fields`, `invalid_new_key`, `same_key`, `not_found`, `key_mismatch`,
`invalid_old_challenge`, `invalid_old_signature`, `invalid_new_challenge`,
`invalid_new_signature`, `key_already_used`, `rotation_failed`, `rate_limited`.

Note: this proves you still hold the current key. It cannot help if the key has already
been lost — there is no recovery path for a lost key today.

---

## `POST /api/muses/{name}/verify-endpoint`

Checks the Muse's declared endpoint for a manifest file at
`{endpoint}/.well-known/muse.json`. The manifest must be reachable over HTTP(S) and return
JSON containing this Muse's `agentId`:

```json
{ "agentId": "agt_7f82c1e91ac" }
```

No body required — the endpoint URL is read from the Muse's registered data.

**Response `200`**: `{ "verified": true }`
**Response `400`**: `{ "verified": false, "message": "..." }` — reason included, e.g. manifest
unreachable, wrong status code, or `agentId` mismatch.

---

## `POST /api/muses/{name}/verify-domain`

Checks for a DNS TXT record proving control of a domain.

**Body**

| field  | type   | required | notes             |
|--------|--------|----------|--------------------|
| domain | string | yes      | e.g. `example.com` |

Add a TXT record on the domain with the exact value:

muse-verify=agt_7f82c1e91ac
(using this Muse's own `agentId`.)

**Response `200`**: `{ "verified": true }`
**Response `400`**: `{ "verified": false, "message": "..." }` — includes the exact expected
TXT value if no match was found.

---

## `GET /api/muses`

Search the registry.

**Query parameters**

| name       | type   | required | notes                                  |
|------------|--------|----------|--------------------------------------------|
| q          | string | no       | substring match on name                  |
| capability | string | no       | exact match on a capability name (lowercase) |

**Response `200`** — array of Muse objects, each shaped like:

```json
{
  "name": "nora",
  "displayName": "NORA",
  "description": "...",
  "category": "RESEARCH",
  "status": "active",
  "identity": "agt_7f82c1e91ac",
  "publicKey": "7f82...",
  "createdDaysAgo": 18,
  "endpoint": "https://api.nora.example",
  "links": ["Website", "GitHub", "X"],
  "capabilities": [{ "name": "Research", "source": "self_declared" }],
  "proofs": [
    { "type": "identity", "verified": true },
    { "type": "public_key", "verified": true },
    { "type": "endpoint", "verified": false },
    { "type": "domain", "verified": false },
    { "type": "wallet", "verified": false }
  ],
  "activity": [{ "type": "heartbeat", "label": "Identity heartbeat", "timestamp": "4m ago" }],
  "observedActions": 142,
  "wallet": null,
  "numericId": 7,
  "network": "Robinhood Chain"
}
```

---

## `GET /api/muses/{name}`

Resolve a single Muse. Same object shape as one item in `/api/muses`.
`404` with `{ "error": "not_found" }` if unregistered.

## `GET /api/muses/{name}/capabilities`

Returns only the `capabilities` array.

## `GET /api/muses/{name}/proofs`

Returns only the `proofs` array — always includes all five proof types (`identity`,
`public_key`, `endpoint`, `domain`, `wallet`), each with its current `verified` status.
A type with no corresponding check yet shows `verified: false`, not an absent entry.

## `GET /api/muses/{name}/activity`

Returns only the `activity` array — observed events, not self-declared information.

---

## Minimal client example (Node.js, `@noble/ed25519`)

```js
import * as ed from "@noble/ed25519";

const toHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
const BASE = "https://museid.world";

const privateKey = crypto.getRandomValues(new Uint8Array(32));
const publicKey = toHex(await ed.getPublicKeyAsync(privateKey));

const { challenge } = await fetch(`${BASE}/api/challenge`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ publicKey }),
}).then((r) => r.json());

const signature = toHex(await ed.signAsync(new TextEncoder().encode(challenge), privateKey));

const result = await fetch(`${BASE}/api/register`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "nora",
    publicKey,
    challenge,
    signature,
    capabilities: ["research", "analysis"],
  }),
}).then((r) => r.json());
```

Keep `privateKey` yourself — MuseID never receives it and cannot recover it. **There is
currently no key recovery mechanism.** Losing the private key means permanently losing the
ability to manage this Muse: no wallet connection, no key rotation, nothing. Back it up.

## What MuseID does not do

MuseID does not perform KYC, biometric verification, or any check of human uniqueness.
MuseID does not create or custody a crypto wallet — wallet ownership is proven by signature,
and MuseID never holds a wallet private key. MuseID does not independently verify
self-declared capabilities, description, or links unless a specific proof type says
otherwise. An operator may register more than one Muse; MuseID proves that a registered
identity controls a name, not that an underlying AI model is unique.
