# [ERC8122] AIエージェントを登録して探せる仕組み

> 例えば利用者がDeFi向けのAIエージェントを探し、候補一覧でAlphaという名前を見つけたとします。同じ名前の候補が別のregistryにもあるため、名前だけで接続先を開くと、意図しないagentへつながるおそれがあります。

- URL: https://cardene.dev/articles/3ec73d2a8a63bb7e4911
- 公開日: 2026-09-22
- 更新日: 2026-09-22
- トピック: Blockchain
- タグ: ethereum, erc, smartcontract, ai

## はじめに

初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

AIエージェントを選ぶ時は、名前だけでなく、誰が公開し、どこへ接続するものかを確認します。
ERC8122は、エージェントの登録先、所有者、説明、接続先をオンチェーンへ記録し、番号から探せるようにする仕組みです。

## 概要

例えば利用者がDeFi向けのAIエージェントを探し、候補一覧で`Alpha`という名前を見つけたとします。
同じ名前の候補が別のregistryにもあるため、名前だけで接続先を開くと、意図しないagentへつながるおそれがあります。

そこで利用者は、まず一覧の説明から候補群を選び、registryの場所とagent IDを組にして同じagentかを特定します。
次に現在の所有者を確認し、最後に通信方式と接続先を読んで接続します。

registryは、この確認に使うエージェントの一覧、所有者、説明、接続先を記録するコントラクトです。
ERC8122は、候補の名前、同一性、公開者、接続先を1つの値で済ませず、判断ごとに読む情報を分けます。

ここでいうregistryは、すべてのエージェントを集めた中央ディレクトリではありません。
誰でも用途に合わせたregistryをデプロイできるため、同じagent IDが複数のregistryに存在します。
そのため、クライアントが保存する識別子はagent IDだけでは足りず、registryの場所と組にします。

所有者と接続先も同じ意味ではありません。
所有者は登録物を移転できるアドレスであり、serviceは利用者が通信を開始するための情報です。
serviceのURIを読めても、そのURIの運営者が現在の所有者であるとは限らないため、クライアントは所有者照会とmetadataを別々に確認します。

![registryの場所とagent IDから所有者と接続先を読む](/images/qiita-6720bb7f-ca07-4b77-841c-5752c3bd719e.webp)

#### 世界で一意なagent

`agentRegistry`は、どのチェーンのどのregistryコントラクトかを表す場所です。
`agentId`は、そのregistryが発行する番号であり、両方が一致した時だけ同じエージェントを指します。

ERC7930はチェーン種別、チェーン参照、コントラクトアドレスを1つのバイト列に入れます。
クライアントはこの値から接続先を特定し、番号だけを別チェーンの登録物へ結び付けません。

:::message
**ERC7930**については以下の記事を参考にしてください。
:::

[\[ERC7930\] 複数チェーンのアドレスを1つの形式で扱う仕組みを理解しよう！](/articles/e280966bf2619d25d4c8)

#### 一人だけの所有者

各agent IDには常に1人の所有者がいます。
ERC6909を土台にしつつ、ERC8122は送付量を必ず1にして、送付時には現在の所有者も更新します。

:::message
**ERC6909**については以下の記事を参考にしてください。
:::

[\[ERC6909\] ERC1155を使いやすくアップデートした仕組みを理解しよう！](/articles/edbc280b177255d1d981)

#### 表示用の名前

エージェント名や番号、registryの場所などを人が読みやすい表示用に並べる時は、`[alias.]agentId@registry`形式を使います。
このERC8127形式では、別名は候補を見分けるために使い、正式な参照には番号とregistryまで使います。

## 動機

エージェント間通信ができても、どの候補を探し、誰が公開したものかを決める仕組みは別に必要です。
ERC8122は発見に必要な情報をオンチェーンに置き、用途ごとに異なるregistryをデプロイできます。

通信プロトコルだけを用意しても、利用者が最初に解く「**どの相手へ接続するか**」という問題は残ります。
名前やサービスURIを各サービスのデータベースだけに置くと、クライアントごとに検索方法が分かれ、同じ名前の候補を同一の登録物として扱う根拠も失われます。

ERC8122は、登録物に番号を割り当て、所有者とmetadataをその番号へ結び付けます。
この対応関係をチェーン上で確認できるため、クライアントは外部の検索画面を候補の発見に使いながら、最終的な識別と所有者確認をregistryへ戻せます。

オンチェーンに置くのは、長い説明文や検索インデックスをすべて保存するためではありません。
名前、説明、通信方式、URIなど、候補を絞り込み接続先を解決するための最小情報を、agent IDと同じ登録物として参照できるようにします。

![用途ごとに独立したregistryを配置する](/images/qiita-4719f1da-c694-438d-b85f-27b43573b593.webp)

#### 発見情報をオンチェーンに置く理由

名前と説明は候補を絞るために使い、通信方式と接続先は利用方法を決めるために使います。
これらをagent IDと結び付けて保存するため、クライアントは同じ形式で候補の情報を取得できます。

外部データベースは検索性や更新性に向いていますが、そこにしか登録情報がないと、データベースの運営者が変わった時に候補の正本を判断できません。
ERC8122では、外部検索が返したregistry所在地とagent IDをチェーン上で再照会し、番号の存在、所有者、metadataの現在値を確認できます。
ただし、オンチェーンにあるURIの内容や接続先の安全性まで保証する規格ではありません。

#### collectionごとにregistryを分ける理由

DeFi戦略用、脆弱性調査用、固定供給のコレクションは、それぞれ別のregistryとして配置できます。
クライアントは表示名だけでなく、どのregistryに載っている候補かも選択条件にします。

registryを分けると、用途ごとに登録者、メタデータの意味、更新権限、採用するagentの範囲を定義できます。
一方で、registryが違えば同じagent IDでも別の登録物です。
利用者は「**名前が一致したから同じ**」と判断せず、信頼するregistryの一覧や運営条件を先に決めます。

## 仕様

ERC8122のregistryはERC6909を拡張し、agentごとのmetadataにはERC8048を実装します。
registry自身の案内情報を持たせる時はERC8049も実装できます。

### agent registryと単一所有権

registryは`agentId`ごとに現在の所有者を保存し、`ownerOf`で返します。
存在しない番号の照会、または1以外の送付量はrevertします。

![送付量を1に限定して所有者を更新する](/images/qiita-17bd41dc-2171-4f10-bb44-7bcf8b4cb83a.webp)

### registry metadata

registry全体の`name`、`description`、`image`は、個別エージェントではなく一覧を説明する値です。
ERC8049を使う実装はこれらを設定でき、`symbol`、`banner_image`、`featured_image`、`external_link`も追加できます。

### agent metadata

agent IDごとの`name`、`ens_name`、`image`、`description`、`service_type`、`service`、`agent_account`はERC8048のキーと値として保存します。
`setMetadata`は所有者、承認済み利用者、operator以外が呼ぶとrevertします。

![agent単位とregistry単位のmetadataを分けて保存する](/images/qiita-e3e74771-94be-41e9-928d-42145d1e4041.webp)

| キー | クライアントが読み取る内容 |
| --- | --- |
| `name` | agentの表示名です。 |
| `description` | agentが何をするかを説明する文章です。 |
| `service_type` | 接続に使う通信方式です。 |
| `service` | 主なオフチェーンサービスのURIです。 |
| `agent_account` | agentがトランザクションに使うアカウントです。 |

値は`bytes`で保存し、仕様で型を指定していないキーの値はUTF-8文字列を符号化します。
URI中の`{id}`はagent IDへ置き換えられ、追加の接続先はERC8119の番号付きキーで対応付けられます。

### `IERC8122`

```js
interface IERC8122 {
    struct MetadataEntry { string key; bytes value; }
    event Registered(uint256 indexed agentId, address indexed owner, string service_type, string service, address agent_account);
    function register(address owner, string calldata service_type, string calldata service, address agent_account) external returns (uint256 agentId);
    function register(address owner, MetadataEntry[] calldata metadata) external returns (uint256 agentId);
    function registerBatch(address[] calldata owners, MetadataEntry[][] calldata metadata) external returns (uint256[] memory agentIds);
    function ownerOf(uint256 agentId) external view returns (address);
}
```

#### `MetadataEntry`

```js
struct MetadataEntry { string key; bytes value; }
```

`MetadataEntry`は、登録する情報の種類と値を1組にする構造体です。
呼び出し側が作り、registryがagent IDごとのERC8048保存値として記録します。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `key` | 保存する情報の種類です。 |
| `value` | キーに対応する`bytes`値です。 |

#### `Registered`

```js
event Registered(uint256 indexed agentId, address indexed owner, string service_type, string service, address agent_account);
```

`Registered`は、新しいagentを登録した時に発行するイベントです。
イベントログは番号、所有者、基本接続先を追えますが、完全なmetadataは保存値を別に読みます。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `agentId` | インデックス付きの新しいagent IDです。 |
| `owner` | インデックス付きの登録後の所有者です。 |
| `service_type` | 通信方式です。未設定なら空文字列です。 |
| `service` | 接続先URIです。未設定なら空文字列です。 |
| `agent_account` | 取引に使うアカウントです。未設定ならzero addressです。 |

#### `register`

```js
function register(address owner, string calldata service_type, string calldata service, address agent_account) external returns (uint256 agentId);
```

この`register`関数は、所有者と基本接続情報から1件のagentを登録する関数です。

1. **agent IDを割り当てる**
    重複しない番号を決める。
2. **所有者と残高1を保存する**
    `owner`とERC6909の残高を記録する。
3. **イベントを発行する**
    接続情報と`Registered`を記録し、agent IDを返す。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `owner` | 登録後の所有者です。 |
| `service_type` | 基本接続に使う通信方式です。 |
| `service` | 基本接続先のURIです。 |
| `agent_account` | agentが使うアカウントです。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `agentId` | 新しく割り当てた番号です。 |

- **実行条件**

  - 未設定の接続情報はイベントへ空文字列またはzero addressとして記録します。

#### `register`

```js
function register(address owner, MetadataEntry[] calldata metadata) external returns (uint256 agentId);
```

この`register`関数は、任意のmetadata配列から1件のagentを登録する関数です。

1. **agent IDを割り当てる**
    重複しない番号を決める。
2. **所有者と残高1を保存する**
    `owner`と残高を記録する。
3. **metadataを保存する**
    各`MetadataEntry`をagent IDへ結び付けて保存する。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `owner` | 登録後の所有者です。 |
| `metadata` | 保存するキーと値の組です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `agentId` | 新しく割り当てた番号です。 |

- **実行条件**

  - 基本接続情報がない時も登録は成立し、イベントには既定の空値を入れます。

#### `registerBatch`

```js
function registerBatch(address[] calldata owners, MetadataEntry[][] calldata metadata) external returns (uint256[] memory agentIds);
```

`registerBatch`関数は、所有者とmetadataの対応する配列から複数のagentを登録する関数です。

1. **入力を対応付ける**
    同じ添字の`owners`と`metadata`を1件のagentとして読む。
2. **各agentを登録する**
    一意な番号、所有者、残高1、metadataを設定する。
3. **番号の配列を返す**
    入力と同じ順序のagent IDを返す。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `owners` | 各agentの所有者を並べた配列です。 |
| `metadata` | 各agentへ保存する値を並べた配列です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `agentIds` | 登録結果のagent IDです。 |

- **実行条件**

  - 所有者とmetadataは同じ添字で1件のagentを表します。

#### `ownerOf`

```js
function ownerOf(uint256 agentId) external view returns (address);
```

`ownerOf`関数は、agent IDから現在の所有者を返す参照関数です。

1. **番号の存在を確認する**
    指定したagent IDに保存値があるかを確認する。
2. **所有者を返す**
    存在する時は現在の所有者を返す。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `agentId` | 所有者を読みたい番号です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `address` | 現在の所有者です。 |

- **実行条件**

  - 存在しないagent IDを渡すとrevertします。

### `IERC8049SetContractMetadata`

```js
interface IERC8049SetContractMetadata {
    function setContractMetadata(string calldata key, bytes calldata value) external;
}
```

#### `setContractMetadata`

```js
function setContractMetadata(string calldata key, bytes calldata value) external;
```

`setContractMetadata`関数は、registry自身を説明するキーと値を設定する関数です。

1. **情報種別を指定する**
    `key`に更新するregistry metadataの種類を渡す。
2. **値を保存する**
    `value`をregistry単位で更新する。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `key` | 更新する情報の種類です。 |
| `value` | 保存する`bytes`値です。 |

- **実行条件**

  - 呼び出し権限は各registry実装が決めます。

### 登録時に起きること

登録者は、agentを受け取る所有者と、候補を説明するmetadataをregistryへ渡します。
registryは新しいagent ID、所有者、残高1、metadataの保存値を同じ処理で確定します。

![登録入力から保存値とイベントを作る](/images/qiita-2eebdf97-fdae-49a5-8ed9-07a96e7294e2.webp)

その後、利用者はイベントログだけで接続先を決めません。
`Transfer`、`MetadataSet`、`Registered`を同期して候補の追加を見つけ、完全なmetadataと現在の所有者はregistryの保存値から読み直します。

## 補足

#### metadataから接続先を読む

クライアントは`service_type`から通信方式を選び、対応する`service`から接続先を読みます。
URIだけを根拠に公開者を判断せず、所有者は`ownerOf`で別に確認します。

たとえば`service`が`https://api.example.com/agents/{id}`なら、クライアントは登録されたagent IDで`{id}`を置き換えて接続先を解決します。
複数の通信方式を登録する時は、追加の番号付きキーを使い、方式とURIの組を崩さずに読み取ります。
URIの解決結果はオフチェーンの応答なので、解決できたことと正しい相手であることは別に検証します。

#### 更新できる情報と所有者

agent metadataは所有者、承認済み利用者、operatorが更新できます。
registry metadataの更新権限は実装ごとに異なるため、collectionの説明を信頼条件に使う時は運営者と更新規則も確認します。

この分離により、所有者の移転とサービス情報の更新を同じ出来事として扱わずに済みます。
利用者は`ownerOf`で現在の所有者を確認し、metadataの更新権限で接続先が変わり得ることを前提に、サービス側の認証や評判情報を別に評価します。

## 互換性

#### 既存規格の操作とデータ形式をそのまま使う

ERC8122はERC6909の送付、ERC8048のagent metadata、ERC8049のregistry metadataを組み合わせます。
ERC8122は、ERC6909の残高・移転、ERC8048のmetadataキー・値、ERC8049のregistry metadataという既存の操作とデータ形式を別の意味に置き換えません。
その上で、agentを1人だけが所有すること、登録時に必要なイベント、agent発見に使う追加APIを定めます。

この組み合わせを採用する実装は、ERC6909の残高と移転イベントを理解する既存ツールを登録・移転の監視に利用できます。
ERC8048対応のクライアントはキーと値を読み、ERC8049対応のクライアントはregistry全体の説明を読みます。
ただし、既存ツールがERC8122の`Registered`イベントや単一所有権の意味を知らなければ、agent発見まで自動で対応できるわけではありません。

#### 既存registryと共存する

既存のagent registryへ移行は要求しません。
ERC8122を実装した新しいregistryが、番号、単一所有者、オンチェーンmetadataの共通形式を提供します。

そのため、導入時に既存registryの番号をERC8122へ一括移行したり、既存の所有者を強制的に置き換えたりする処理はありません。
アプリケーションは対応するregistryのアドレスを一覧へ追加し、各registryがERC8122のinterfaceを実装しているかを確認してから検索対象にします。
同じagent名や番号が別registryに存在しても、registryの場所が違えば別の登録物として扱います。

## セキュリティ

#### metadataの更新権限

仕様のセキュリティ節は追加要件を定めません。
実装は更新できる主体を正しく制限し、クライアントは所有者の確認と更新可能な接続先の評価を分けます。

ここで「**追加要件がない**」とは、metadataの内容やオフチェーンサービスをERC8122が安全だと認定する意味ではありません。
実装が所有者、承認済み利用者、operator以外の`setMetadata`を拒否しなければ、第三者が接続先を書き換えられます。
クライアントは更新権限と`MetadataSet`イベントから更新履歴を確認し、取得した値を無期限にキャッシュせず、必要な時点で現在値を再読します。

#### offchain serviceへの接続

`service`のURIは通信先を示しますが、安全な相手であることを保証しません。
クライアントは通信方式ごとの認証、URIの解決先、IPFS gatewayの信頼条件を確認してから接続します。

攻撃者が正規のagentに見えるURIを登録したり、更新権限を持つ主体が悪意あるURIへ変更したりすると、metadataを読めても安全な接続先にはなりません。
接続前にTLSや通信方式固有の認証を確認し、IPFSではgatewayが返す内容を信頼できるか、URIの`{id}`置換で別agentの情報を取得していないかを検証します。
所有者のアドレスが正しくても、所有者が運営するサービスの可用性、応答内容、鍵管理まで保証されるわけではありません。

## 最後に

ERC8122は、registryの場所とagent IDでAIエージェントを特定し、所有者とオンチェーンmetadataを読み出す仕組みです。
単一所有権、用途別registry、登録時のイベントを組み合わせることで、候補の登録先、公開者、接続先を分けて確認できます。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/

#### 参照

https://eips.ethereum.org/EIPS/eip-8122
