# [ERC8444] AIエージェントの提供機能と報酬付きの依頼をオンチェーンで見つける仕組み

> 旅行を計画するAIエージェントが、航空券を検索できる機能と、旅程作成を報酬付きで依頼している相手を探す場面を考えます。

- URL: https://cardene.dev/articles/03cafeb763efd8d1e6c5
- 公開日: 2026-10-17
- 更新日: 2026-10-17
- トピック: Blockchain
- タグ: ethereum, erc, ai, solidity

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

LLMを使った仕様駆動開発を扱う書籍『LLMと進める仕様駆動開発』を発売予定です。
ハンズオンや、LLMによって開発がどう変わるのか、実践のTipsをまとめています。

https://www.amazon.co.jp/exec/obidos/ASIN/4863545231/honmori-22

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

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

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

AIエージェントが使える機能や報酬付きの依頼を探す時、説明文だけでは現在の所有者、支払い先、預けられた報酬まで確認できません。
ERC8444は、普段の検索結果へオンチェーンの根拠を結び付け、利用前に再確認できるようにする仕組みです。

2026年10月17日時点では `Draft` です。
この記事では、前提となる検索方式、全API、公式参考実装、テスト、安全性まで解説します。

公式仕様は以下です。

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

## 概要

旅行を計画するAIエージェントが、航空券を検索できる機能と、旅程作成を報酬付きで依頼している相手を探す場面を考えます。
検索結果に「**予約API**」、「**報酬100 USDC**」と書かれていても、その説明を現在の所有者が承認しているか、報酬が実際に預けられているかは文章だけでは分かりません。
古い所有者の支払い先が残っていれば、正しい機能を見つけても誤った相手へ支払う可能性があります。

Web上の公開情報を集め、ふつうの言葉でAI向けの機能や接続先を探せる共通方式を `Agentic Resource Discovery`、略して `ARD` と呼びます。
ERC8444はARDを置き換えません。
ARDの検索結果と、トークン、依頼に付けた報酬、AIエージェントの身元、検索サービスの運営者をオンチェーン情報で結び付けます。

提供者は、検索結果へ載せる説明文を作り、その内容から得たハッシュ値をオンチェーンへ記録します。
検索サービスは説明文を取得し、記録済みのハッシュ値、現在の所有者、価値の裏付け、公開元ドメインとの関係を照合します。
利用するAIエージェントは、検索の関連度とオンチェーンの根拠を分けて読み、候補を選びます。

#### AIエージェントが機能と報酬付きの依頼を探す場面

ARDでは、利用者側のAIエージェントが「**BaseでUSDC払いの画像生成機能**」のような、ふつうの言葉で書いた検索条件を送ります。
検索側は、機能、API、AIエージェントなどを表す `entry` を返します。
`entry` は、誰が公開したどの接続先かを検索結果として表すJSONです。

ERC8444は、提供する機能だけでなく、作業を依頼する側も検索対象にします。
報酬付きの依頼には、求める成果物の形式、報酬、期限、受入条件を載せられます。
依頼を引き受けるAIエージェントは、自分が提供できる成果物と条件が合う報酬付きの依頼を探せます。

#### オンチェーン情報を検索結果へ結び付ける役割

検索結果の説明とオンチェーン情報の接点になる対象を `anchor` と呼びます。
たとえば、機能を表すNFT、報酬を預けた依頼を表すNFT、AIエージェントを表すaddressがanchorになります。
anchorごとに、説明文のハッシュ値や公開名を保存するオンチェーン記録が `record` です。

説明文を含むARD entryを `descriptor` と呼びます。
現在の所有者はdescriptorのうち提供条件に関わる部分をハッシュ化し、その値をrecordへ登録します。
取得先がdescriptorを書き換えても、検索側はハッシュ不一致として検出できます。

anchorの種類ごとに、既存contractの読み方は異なります。
そこで、対象contractから「**存在するか**」、「**現在誰が操作できるか**」、「**価値の裏付けがあるか**」、「**説明文と照合する値は何か**」を同じ形で読む接続部品を `adapter` と呼びます。
adapterは値を保存し直さず、呼び出された時点の情報を対象contractから読みます。

取得した文書とオンチェーン情報を照合し、検索結果へ値を追加するソフトウェアが `resolver` です。
resolverがARDの検索APIまで公開する場合は `gateway` と呼びます。

#### 検索から利用判断まで

検索開始から利用判断までは、以下の順で進みます。

1. **提供者がdescriptorを公開し、現在の所有者が内容のハッシュ値をrecordへ登録します**
2. **gatewayはrecordとadapterを読み、descriptorとドメイン上の公開文書を取得します**
3. **resolverはハッシュ値、現在の所有者、公開ドメインとの関係、価値の裏付けを計算します**
4. **gatewayは整合したentryだけを通常のARD検索結果へ載せます**
5. **利用側は、検索条件との近さ、状態、所有者の承認、価値の裏付けを別々に確認します**

![ERC8444でふつうの言葉による検索からオンチェーン情報を照合した検索結果を得る流れ](/images/qiita-cad8a529-1eaa-48c2-b84b-491facda9fc5.webp)

検索順位の点数は、検索条件に近いかを示す値であり、信用度ではありません。
また、所有者が変わった後に新しい所有者がdescriptorを承認していないrecordや、廃止されたrecordは検索結果から外れます。

一時停止中のrecordを検索へ載せるか、どのlevelで示すかは、現行仕様だけでは一意に確定できません。
参考gatewayは一時停止を数値上のlevel 0として扱いますが、これを規格要件とは断定できません。
利用側は `status` と所有者の承認状態を個別に確認し、一時停止中のofferを有効な候補として扱わない実装にします。

## 動機

ARDは、どの公開元がどの機能を案内しているかを検索する仕組みです。
一方、価格、支払い、報酬の預け入れ、信用情報までは定めません。
ERC8444は、既存ARD entryをそのまま利用しながら、価値に関わる事実を既存contractから読めるようにします。

#### 発見だけでは読めない価値

「**画像を生成できる**」という説明から、呼び出し1回の価格や支払い先は確定できません。
「**報酬100 USDCの画像作成依頼**」という説明からも、100 USDCが預けられたままか、依頼が取り消されていないかは分かりません。

価値に関する情報を検索用registryへ複製すると、元contractの残高や状態とずれる可能性があります。
ERC8444は、recordへ説明文のcommitmentを置き、残高や所有者はadapter経由で元contractから都度読みます。
検索用のコピーを第二の正本にしない設計です。

#### domainと支払いaddressの分離

Webのdomainを管理する主体と、資金を受け取るaddressを管理する主体は同じとは限りません。
両者を同格の権限者にすると、domain側がaddress所有者の承認していない価格を公開したり、address側がdomain名を勝手に名乗ったりできます。

ERC8444では、資金やanchorを操作するaddressを価値情報の起点にします。
domainは、そのaddressを自分の公開文書で認めることで信頼関係を追加します。
秘密鍵を持たないdomain公開者も従来のARD entryを公開でき、その場合はオンチェーン根拠のない段階として検索できます。

#### 報酬付きの依頼を見つける方向

従来のARD検索は、利用者が使いたい機能を探す方向が中心です。
ERC8444の `task` は、成果物を依頼し、受入条件を満たした相手へ支払う報酬を示す公開情報をentryとして表します。

依頼する側は、受け取れる成果物の形式、報酬額、期限、審査方式、応募条件を公開します。
提供側のAIエージェントは「**自分の機能を誰が必要としているか**」を検索できます。
ただし、成果物の提出、審査、報酬解放そのものはERC8444の範囲外です。

#### 移転後の古い説明を止める理由

機能NFTを売却しても、recordへ保存されたdescriptorのハッシュ値は自動では変わりません。
そのまま配信すると、買い手が旧所有者のendpoint、利用条件、支払い先を支持しているように見えます。
受け取りを望まないaddressへNFTを送り付けるケースも同じです。

ERC8444は、descriptorを最後に登録したaddressと現在の所有者を比較します。
一致しない間は検索結果から外し、新しい所有者が同じハッシュ値または新しいハッシュ値を登録した後に再掲載します。

## 仕様

### 4種類のkind

検索対象が取引のどちら側にいるかを表す区分を `kind` と呼びます。
データ形式を表すARDの `type` とは別の値です。

| `kind` | 立場 | anchor | 現在操作できる主体 | 裏付けの条件 |
| --- | --- | --- | --- | --- |
| `skill` | 機能の提供 | 実行可能な機能を表すtoken | token owner | package hashが存在する |
| `task` | 報酬付きの依頼 | 報酬付きの依頼を表すtoken | token owner | 未取消、報酬あり、期限内 |
| `agent` | 取引主体 | address | そのaddress | identity registryでactive |
| `registry` | 検索先 | 運営addressと識別値 | そのaddress | 常に裏付けなし |

`skill`、`task`、`agent`、`registry` の4種類はすべてのregistry instanceが対応します。
新しいJSON形式が増えてもkindは増やしません。
`assertion`、`policy`、`market` は将来用に予約されています。

kind IDは以下の式で作ります。

```js
bytes32 kindId = keccak256("ard-onchain.kind.<name>");
```

### AnchorとRecord

anchorは `(chainId, target, id)` の組です。
tokenをanchorにする場合はchain、token contract、token IDを示すCAIP-19形式、addressをanchorにする場合はchainとaddressを示すCAIP-10形式で表示します。
CAIP形式により、利用側はどのchainのどのcontractまたはaddressを確認すべきか特定できます。

record IDはanchorから以下の式で作ります。

```js
recordId = keccak256(abi.encode(block.chainid, target, id));
```

同じanchorには1つのrecordだけを登録できます。
別kindを指定しても2件目は登録できません。
別chain上の同じtoken addressとIDは、別のanchorです。

ARDの公開名は `urn:air:<publisher>:<namespace>:<name>` 形式です。
このURNは別名であり、recordの同一性はanchorで比較します。
現在の所有者とpublisher domainの関係が確認でき、domainのmanifestにも同じURNが載る間だけcanonical nameとして使えます。

gatewayは自分のdomainで到達できるaliasを作れます。
複数gatewayが別のaliasを返しても、利用側は `value:anchor` を比較して同じrecordか判断します。

### Descriptorの検証

descriptorは、表示名、接続先、説明、提供条件などを載せたARD entryです。
現在の所有者は、提供内容に関わる部分だけを固定順序のJSONへ変換してSHA-256を計算し、recordへ保存します。

#### Hashへ入る情報

descriptorのmemberは3種類に分かれます。

| 区分 | 主な値 | hash対象 | 理由 |
| --- | --- | --- | --- |
| 公開側の情報 | `identifier`、`trustManifest`、`@context`、`@id` | 対象外 | gatewayごとに別名や公開情報を持てるようにする |
| 内容の情報 | `displayName`、`type`、`url`または`data`、説明、提供条件 | 対象 | 所有者が何を提供または依頼したか固定する |
| resolverの計算結果 | `value:anchor`、binding、level、status、adoptedなど | 対象外 | 読み取り時点のchainとdomainから再計算する |

内容の情報はRFC 8785のJSON Canonicalization Schemeで正規化します。
これは、同じJSON内容を空白やkey順に左右されない同一のbyte列へ変換する方式です。
そのUTF-8 byte列へSHA-256を適用した値が `descriptorHash` です。

![ERC8444でpublisherが作る情報、hash対象、resolverが加える情報を分ける境界](/images/qiita-cfe4ecf3-7a12-475d-9e46-603f711293d3.webp)

`descriptorURI` は取得先のヒントであり、信頼の根拠ではありません。
取得できない場合やhashが一致しない場合、resolverは整合したrecordとして配信できません。

#### 提供条件と報酬付きの依頼のJSON

所有者が書く追加情報には次があります。

| member | 読者が確認する内容 |
| --- | --- |
| `value:kind` | entryが機能提供、報酬付きの依頼、主体、検索先のどれか |
| `value:controller` | 公開者が主張するcontroller。権限判定には使わない |
| `value:terms` | 機能の単位、価格、通貨、供給量 |
| `value:settlement` | onchain、x402、宣言のみの支払い経路と支払い先 |
| `value:provenance` | packageと公開cardのdigest |
| `value:credit` | 外部の信用情報を確認するregistryとlevel |
| `value:dispute` | 紛争判断を確認するpolicyとregistry |
| `value:eligibility` | taskへ応募できる主体の条件 |
| `value:registration` | agentのregistration file |
| `value:indexes` | registryが索引するregistry instance |

`task` のpayloadは `application/ai-task+json` を使います。
求める成果物形式を表す `wants` と、報酬額・通貨を表す `bounty` は必須です。
期限、件数、審査方式、応募条件は任意で追加できます。
オンチェーン情報がなくても有効なARD entryになるため、domainだけの報酬付きの依頼も公開できます。

### Domain Binding

domainとaddressが互いを認めたかを示す関係が `binding` です。
address側のオンチェーン宣言と、domain側のWeb上の承認をresolverが照合します。

#### Onchain claim

addressは、対象domainを自分と結び付けたいという宣言をregistryへ保存します。
本人がtransactionを送る方法と、署名を第三者に提出してもらう方法があります。

domainは小文字ASCII、数字、`-`、`.` の1〜253 bytesで正規化します。
先頭・末尾の`.`と空labelは使えません。
国際化domainはpunycodeへ変換してから渡します。

同じdomainを複数addressがclaimできます。
registryは本当の所有者を決めず、各addressの宣言だけを保存します。

#### Domainのacknowledgment

domain側は `/.well-known/ard.json` にaddressを載せて承認します。
manifest上部の `value:controllers` に含めるか、entryの `value:controller` に書きます。

chainはHTTPS上のmanifestを読めません。
そのため、registry contractがverified判定を保存するのではなく、resolverがonchain claimとmanifestを合わせて計算します。

![ERC8444でaddressのclaimとdomainの承認を照合してbindingを決める流れ](/images/qiita-0edb6912-6aac-4063-a879-78af8238e3f1.webp)

#### 4つのbinding状態

bindingは、domainとaddressの間にどの宣言がそろったかを表します。
利用側は、この状態を読んでdomain名を信頼表示へ使えるか判断します。

| 状態 | 保存・公開されている条件 | 利用側の判断 |
| --- | --- | --- |
| `unbound` | addressのclaimがない | 関係なしとして扱う |
| `claimed` | 1 addressだけがclaimし、domainは未承認 | address側だけの主張として扱う |
| `verified` | addressがclaimし、domainも承認 | domainとaddressの相互確認済みとして扱う |
| `contested` | 複数addressがclaimし、対象addressは未承認 | 競合中として扱う |

recordにURNがない場合は `none`、現在のcontrollerがURNのpublisher domainをclaimしていない場合は `third-party` として表示します。
承認が消えるとcanonical URNはstaleになります。
後から承認が戻っても、承認がなかった期間に配信された内容まで遡って正当化しません。

### LevelとAdoption

#### DormantからBackedまで

levelは、検索対象にどのオンチェーン根拠があるかを表します。
domainとのbindingや信用情報とは独立しています。

| level | 条件 | 利用側が確認できること |
| --- | --- | --- |
| `L0` | recordがない | domain公開者の宣言だけである |
| `L1` | statusがActiveのrecordがある | anchorと現在のcontrollerをchainで読める |
| `L2` | L1に加えadapterがbackedを返し、必要なprovenanceも一致する | 報酬、package、identityなどkind固有の裏付けがある |

`L2` でも価格の妥当性や提供品質まで保証しません。
利用側は `(level, binding, credit)` を別々に読みます。

#### 一時停止と廃止

recordには、利用中、一時停止、廃止済みのどれかが保存されます。
一時停止は現在のcontrollerが解除できますが、廃止は後の所有者も解除できません。

| 保存状態 | 次に許される変更 | 検索時の扱い |
| --- | --- | --- |
| `Active` | `Suspended`または`Retired` | L1またはL2の候補になる |
| `Suspended` | `Active`または`Retired` | 仕様上の検索掲載とlevelに曖昧さが残るためstatusを明示確認する |
| `Retired` | 変更不可 | 検索結果へ載せない |

購入前にはstatusを確認します。
一時的に止めるだけなら廃止を選びません。

#### 移転後のadoption

descriptorを最後にcommitしたaddressを `attestor` と呼びます。
attestorと現在のcontrollerが一致している状態が `adopted`、一致しない状態が `unadopted` です。

![ERC8444でtoken移転後に検索から外れ新所有者の再commitで戻る流れ](/images/qiita-368d5678-c711-476d-915d-28409f425597.webp)

unadoptedでもrecord、level、statusは残ります。
ただしresolverは検索結果と一覧から外します。
新しいcontrollerが `updateDescriptor` 関数で同じdescriptorHashを再登録すると、そのaddressがattestorになり再掲載できます。

### `IARDOnchainRegistry`

`IARDOnchainRegistry` は、anchorごとのcommitment、domain claim、kindとadapterの対応を扱うinterfaceです。

#### `Status`

```js
enum Status { None, Active, Suspended, Retired }
```

`Status` は、recordが存在するか、現在利用中か、一時停止か、廃止済みかを表すEnum型です。

| 値 | 保存状態 | 判定時の扱い |
| --- | --- | --- |
| `None` | recordが未登録 | 更新や状態変更を拒否する |
| `Active` | 利用中 | L1またはL2の判定へ進む |
| `Suspended` | controllerが一時停止 | 利用候補から外す判断に使う |
| `Retired` | 永続的に廃止 | 更新を拒否し検索へ載せない |

#### `Record`

```js
struct Record {
    bytes32 kind;
    address target;
    uint256 id;
    bytes32 descriptorHash;
    string descriptorURI;
    string urn;
    Status status;
    address attestor;
    uint64 updatedAt;
}
```

`Record` は、controllerが登録したdescriptorのcommitmentとanchorをまとめて保存する構造体です。

- **フィールド**

| field | 保存する値 |
| --- | --- |
| `kind` | 4種類のどれとして読むか |
| `target` | token contractまたは主体address |
| `id` | token ID、agentの0、registryの非0識別値 |
| `descriptorHash` | 内容memberのSHA-256 |
| `descriptorURI` | descriptor取得先のヒント |
| `urn` | controllerが提示したcanonical候補 |
| `status` | 利用中、一時停止、廃止の状態 |
| `attestor` | descriptorを最後にcommitしたcontroller |
| `updatedAt` | 最終更新時刻 |

controller、backing、anchor hintは保存せず、adapterから毎回読みます。

#### `Registered`

```js
event Registered(bytes32 indexed recordId, bytes32 indexed kind, address indexed target, uint256 id, address attestor, bytes32 descriptorHash, string descriptorURI, string urn);
```

anchorへ最初のrecordを登録する時に発行するイベントです。
indexerはこのログから候補を列挙できます。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `recordId` | anchorから作ったrecord識別子 |
| `kind` | adapterを選ぶkind ID |
| `target` | anchorのcontractまたはaddress |
| `id` | token IDまたはaddress anchorの識別値 |
| `attestor` | 登録時のcontroller |
| `descriptorHash` | commitした内容hash |
| `descriptorURI` | descriptor取得先 |
| `urn` | canonical候補 |

#### `DescriptorUpdated`

```js
event DescriptorUpdated(bytes32 indexed recordId, bytes32 descriptorHash, string descriptorURI, string urn);
```

現在のcontrollerがdescriptor commitmentを更新する時に発行するイベントです。
同じhashの再commitも記録されますが、attestorの新しい値はrecordを再読取して確認します。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `recordId` | 更新したrecord |
| `descriptorHash` | 更新後の内容hash |
| `descriptorURI` | 更新後の取得先 |
| `urn` | 更新後のcanonical候補 |

#### `StatusChanged`

```js
event StatusChanged(bytes32 indexed recordId, Status status);
```

recordの利用状態を変更する時に発行するイベントです。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `recordId` | 状態を変えたrecord |
| `status` | 変更後の状態 |

#### `DomainClaimed`

```js
event DomainClaimed(bytes32 indexed domainHash, address indexed controller, string domain);
```

addressのdomain claimを保存する時に発行するイベントです。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `domainHash` | 正規化済みdomainのkeccak256 |
| `controller` | claimしたaddress |
| `domain` | 正規化済みdomain文字列 |

#### `DomainReleased`

```js
event DomainReleased(bytes32 indexed domainHash, address indexed controller);
```

addressが自分のdomain claimを取り下げる時に発行するイベントです。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `domainHash` | 対象domainのhash |
| `controller` | claimを取り下げたaddress |

#### `KindRegistered`

```js
event KindRegistered(bytes32 indexed kind, address adapter, string uri);
```

instance ownerがkindとadapterの対応を初めて登録する時に発行するイベントです。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `kind` | 登録したkind ID |
| `adapter` | live情報を読むadapter address |
| `uri` | adapter情報の参照先 |

#### `OwnershipTransferred`

```js
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
```

registry instanceの管理者を変更する時に発行するイベントです。

- **フィールド**

| field | 詳細 |
| --- | --- |
| `previousOwner` | 変更前のinstance owner |
| `newOwner` | 変更後のinstance owner |

#### `register`

```js
function register(bytes32 kind, address target, uint256 id, bytes32 descriptorHash, string calldata descriptorURI, string calldata urn) external returns (bytes32 recordId);
```

anchorへ最初のrecordを登録する関数です。

1. **kindにadapterがあり、anchorが存在し、呼び出し元が現在のcontrollerであることを確認します**
2. **anchorからrecord IDを作り、重複と空のdescriptor hashを拒否します**
3. **Activeのrecordを保存し、呼び出し元をattestorとして `Registered` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `kind` | adapterを選ぶkind ID |
| `target` | token contractまたは主体address |
| `id` | token IDまたはaddress anchorの識別値 |
| `descriptorHash` | 内容memberのSHA-256 |
| `descriptorURI` | descriptor取得先 |
| `urn` | canonical候補。空文字列も可能 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `recordId` | chain ID、target、idから作った識別子 |

- **実行条件**

- kindが登録済みであること。
- anchorが存在し、呼び出し元が現在のcontrollerであること。
- descriptorHashが0でなく、anchorにrecordが未登録であること。

#### `updateDescriptor`

```js
function updateDescriptor(bytes32 recordId, bytes32 descriptorHash, string calldata descriptorURI, string calldata urn) external;
```

既存recordのdescriptor commitmentを更新する関数です。

1. **recordが存在し、廃止済みでないことを確認します**
2. **adapterが返す現在のcontrollerと呼び出し元を比較します**
3. **hash、URI、URN、attestor、更新時刻を保存し、 `DescriptorUpdated` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `recordId` | 更新するrecord |
| `descriptorHash` | 新しい内容hash。同じhashの再commitも可能 |
| `descriptorURI` | 新しい取得先 |
| `urn` | 新しいcanonical候補 |

- **実行条件**

- recordが存在し、statusがRetiredでないこと。
- 呼び出し元が現在のcontrollerであること。
- descriptorHashが0でないこと。

同じhashを許すため、移転後の新しいcontrollerは内容を変更せずadoptできます。

#### `setStatus`

```js
function setStatus(bytes32 recordId, Status status) external;
```

recordを一時停止、再開、または廃止する関数です。

1. **recordと現在のcontrollerを確認します**
2. **ActiveとSuspendedの往復、または両状態からRetiredへの変更だけを許可します**
3. **statusを保存し、 `StatusChanged` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `recordId` | 状態を変更するrecord |
| `status` | 変更後の状態 |

- **実行条件**

- 呼び出し元が現在のcontrollerであること。
- Retiredではないこと。
- 許可された遷移であること。

この関数はattestorを変更しません。

#### `claimDomain`

```js
function claimDomain(string calldata domain) external;
```

呼び出し元addressがdomainとの関係を主張する関数です。

1. **domainの文字、長さ、label境界を検証します**
2. **同じaddressによる重複claimと、実装上のclaim数上限を確認します**
3. **claimを保存し、 `DomainClaimed` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `domain` | 正規化済みのdomain文字列 |

- **実行条件**

- domainが1〜253 bytesの正規形であること。
- 呼び出し元が同じdomainを未claimであること。

#### `claimDomainBySig`

```js
function claimDomainBySig(string calldata domain, address controller, uint256 nonce, uint256 deadline, bytes calldata signature) external;
```

controllerの署名を使い、第三者がdomain claimを提出する関数です。

1. **domain、期限、現在のnonceを確認します**
2. **chain ID、registry address、controller、nonce、期限を含む型付きdigestを再計算します**
3. **署名を検証し、claimを保存してnonceを1増やします**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `domain` | claimする正規化済みdomain |
| `controller` | claim主体のaddress |
| `nonce` | 署名を1度だけ使うための現在値 |
| `deadline` | 署名を受け付ける期限 |
| `signature` | controllerが作成した署名 |

- **実行条件**

- deadlineを過ぎていないこと。
- nonceが `nonces(controller)` と一致すること。
- EOAまたはcontract accountの署名検証に成功すること。

署名対象をchainとregistryへ結び付け、別環境での再利用を防ぐ方式がEIP712です。
利用側は `DOMAIN_SEPARATOR`、nonce、deadlineを使って同じdigestを再計算できます。

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

[\[EIP712\] 型付き構造化データのハッシュ化と署名の仕組みを理解しよう！](/articles/f54bc85cb66de5a54257)

controllerがcontractの場合は、そのcontractへdigestと署名を渡し、所定の承認値が返るか確認します。
multisigやpolicy contractの署名承認を扱うこの方式がERC1271です。

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

[\[ERC1271\] コントラクトで署名を検証する仕組みを理解しよう！](/articles/da237b1b8cc185491512)

#### `releaseDomain`

```js
function releaseDomain(string calldata domain) external;
```

呼び出し元が自分のdomain claimを取り下げる関数です。

1. **domainを検証してhashを計算します**
2. **呼び出し元のclaimが存在することを確認します**
3. **claimを削除し、 `DomainReleased` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `domain` | claimを取り下げるdomain |

- **実行条件**

- domainが正規形であること。
- 呼び出し元のclaimが存在すること。

#### `registerKind`

```js
function registerKind(bytes32 kind, address adapter, string calldata uri) external;
```

kindとadapterの対応を1度だけ登録する関数です。

1. **呼び出し元がinstance ownerで、adapterがzero addressでないことを確認します**
2. **kindが未登録で、adapter自身が返すkindと一致することを確認します**
3. **対応を保存し、 `KindRegistered` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `kind` | 登録するkind ID |
| `adapter` | 読み取りを担当するcontract |
| `uri` | adapter情報の参照先 |

- **実行条件**

- instance ownerだけが呼べること。
- kindが未登録であること。
- adapterの報告するkindが一致すること。

kind tableはwrite-onceです。
誤ったadapterを登録しても置き換えられません。

#### `transferOwnership`

```js
function transferOwnership(address newOwner) external;
```

registry instanceの管理権限を別addressへ移す関数です。

1. **呼び出し元が現在のownerであることを確認します**
2. **ownerをnewOwnerへ更新します**
3. **`OwnershipTransferred` イベントを発行します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `newOwner` | 新しいinstance owner。zero addressなら管理を終了する |

- **実行条件**

- 現在のownerだけが呼べること。

zero addressへ移すと、以後kindを追加できません。

#### `owner`

```js
function owner() external view returns (address);
```

kind登録とownership移転を実行できるinstance ownerを返す関数です。

1. **保存されているowner addressを読み、その値を返します**

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `address` | 現在のowner。zero addressなら管理終了済み |

- **実行条件**

- 誰でも呼び出せます。

#### `recordIdOf`

```js
function recordIdOf(address target, uint256 id) external view returns (bytes32);
```

現在のchain ID、target、idからrecord IDを計算する関数です。

1. **chain ID、target、idをABI encodeし、keccak256の結果を返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `target` | anchorのcontractまたはaddress |
| `id` | token IDまたはaddress anchorの識別値 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | 計算したrecord ID |

- **実行条件**

- 誰でも任意のtargetとidを指定できます。

#### `recordOf`

```js
function recordOf(bytes32 recordId) external view returns (Record memory);
```

record IDに対応する保存済みRecordを返す関数です。

1. **record IDをkeyに保存値を読み、Record構造体として返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `recordId` | 読み取るrecord |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `Record` | commitment、anchor、status、attestor、更新時刻 |

未登録のIDでは `status` がNoneの値を読み取れます。

- **実行条件**

- 誰でも呼び出せます。

#### `controllerOf`

```js
function controllerOf(bytes32 recordId) external view returns (address);
```

adapterへ問い合わせ、anchorを現在操作できるaddressを返す関数です。

1. **recordのkindとanchorを読み、対応adapterのcontrollerOfへ転送します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `recordId` | controllerを確認するrecord |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `address` | 現在のcontroller。anchor消滅時はzero address |

- **実行条件**

- recordが存在すること。

保存値ではないため、token移転後は新しいownerを返します。

#### `backed`

```js
function backed(bytes32 recordId) external view returns (bool);
```

adapterへ問い合わせ、kind固有の価値の裏付けが現在あるか返す関数です。

1. **recordのkindとanchorを読み、対応adapterのbackedへ転送します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `recordId` | 裏付けを確認するrecord |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bool` | 裏付けがあればtrue |

- **実行条件**

- recordが存在すること。

#### `anchorHintOf`

```js
function anchorHintOf(bytes32 recordId) external view returns (bytes32);
```

descriptorのprovenanceと照合するkind固有の値をadapterから返す関数です。

1. **recordのkindとanchorを読み、対応adapterのanchorHintへ転送します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `recordId` | hintを確認するrecord |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | package hash、task hash、identity digest、または0 |

- **実行条件**

- recordが存在すること。

#### `claimsOf`

```js
function claimsOf(bytes32 domainHash) external view returns (address[] memory);
```

1つのdomainをclaimしている全addressを返す関数です。

1. **domainHashをkeyにclaimant一覧を読み、その配列を返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `domainHash` | 正規化済みdomainから計算したhash |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `addresses` | claim中のaddress一覧 |

- **実行条件**

- 誰でも任意のdomainHashを指定できます。

resolverは一覧とdomain manifestを照合してcontested状態を計算します。

#### `hasClaim`

```js
function hasClaim(bytes32 domainHash, address controller) external view returns (bool);
```

特定addressのdomain claimが保存されているか返す関数です。

1. **domainHashとcontrollerの組をclaim indexで調べ、存在結果を返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `domainHash` | 対象domainのhash |
| `controller` | 確認するaddress |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bool` | claimがあればtrue |

- **実行条件**

- 誰でも任意のdomainHashとcontrollerを指定できます。

#### `domainHashOf`

```js
function domainHashOf(string calldata domain) external pure returns (bytes32);
```

渡された文字列のbyte列へkeccak256を適用する関数です。

1. **domainをbyte列へ変換し、keccak256の結果を返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `domain` | hash化するdomain文字列 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | `keccak256(bytes(domain))` の結果 |

このview自体はdomainの正規形を検証しません。
claim時の検証済み文字列と同じ形式を渡します。

- **実行条件**

- 誰でも任意の文字列を指定できます。

#### `kindAdapter`

```js
function kindAdapter(bytes32 kind) external view returns (address);
```

kindに固定されたadapter addressを返す関数です。

1. **kindをkeyにadapter tableを読み、そのaddressを返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `kind` | 確認するkind ID |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `address` | 登録済みadapter。未登録ならzero address |

- **実行条件**

- 誰でも任意のkind IDを指定できます。

#### `nonces`

```js
function nonces(address controller) external view returns (uint256);
```

controllerが署名付きdomain claimで使うnonceを返す関数です。

1. **controllerをkeyにnonceを読み、その現在値を返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `controller` | nonceを確認するaddress |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `uint256` | 次に受け付けるnonce |

成功した署名付きclaimごとに1増え、同じ署名の再利用を防ぎます。

- **実行条件**

- 誰でも任意のcontrollerを指定できます。

#### `DOMAIN_SEPARATOR`

```js
function DOMAIN_SEPARATOR() external view returns (bytes32);
```

署名対象を特定のchainとregistry instanceへ結び付けるEIP712 domain separatorを返す関数です。

1. **deployment時に固定したdomain separatorを読み、その値を返します**

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | name、version、chain ID、verifying contractから作った値 |

- **実行条件**

- 誰でも呼び出せます。

#### `supportsInterface`

```js
function supportsInterface(bytes4 interfaceId) external view returns (bool);
```

対象contractが指定interfaceへの対応を宣言しているか返す関数です。

1. **interfaceIdをregistry interfaceとERC165のIDへ照合し、対応結果を返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `interfaceId` | 対応を確認する4 bytesのID |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bool` | 対応していればtrue |

呼び出すinterfaceを事前に選べるこの検出方法がERC165です。
ERC8444 registryは `0x881908a5` にtrueを返します。

- **実行条件**

- 誰でも任意のinterfaceIdを指定できます。

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

[\[ERC165\] コントラクトが特定のインターフェースをサポートしているか確認する仕組みを理解しよう！](/articles/541184977a5cba51b759)

#### Custom errors

custom errorは、どの入力または保存状態を拒否したかを呼び出し元へ返します。

```js
error NotOwner();
error ZeroAddress();
error KindExists(bytes32 kind);
error UnknownKind(bytes32 kind);
error AdapterKindMismatch(bytes32 expected, bytes32 reported);
error AnchorMissing();
error NotController();
error EmptyDescriptor();
error AlreadyRegistered(bytes32 recordId);
error UnknownRecord(bytes32 recordId);
error RecordRetired(bytes32 recordId);
error InvalidTransition(Status from, Status to);
error InvalidDomain();
error AlreadyClaimed();
error NotClaimed();
error TooManyClaims();
error SignatureExpired(uint256 deadline);
error BadNonce(uint256 expected, uint256 given);
error InvalidSignature();
```

| error | 拒否する状態 | 修正する内容 |
| --- | --- | --- |
| `NotOwner` | instance owner以外の管理操作 | owner accountから実行する |
| `ZeroAddress` | adapterなどにzero addressを指定 | 有効なaddressを渡す |
| `KindExists` | 同じkindを再登録 | 既存adapterを使う |
| `UnknownKind` | 未登録kindでrecord操作 | kindを先に登録する |
| `AdapterKindMismatch` | adapterの報告値と登録kindが不一致 | 正しいadapterを指定する |
| `AnchorMissing` | adapterがanchor不在を報告 | targetとid、焼却状態を確認する |
| `NotController` | 現在のcontroller以外が変更 | adapterのlive controllerを確認する |
| `EmptyDescriptor` | descriptorHashが0 | 内容から有効なhashを作る |
| `AlreadyRegistered` | anchorにrecordが存在 | 既存recordを更新する |
| `UnknownRecord` | record未登録 | 正しいrecord IDを使う |
| `RecordRetired` | 廃止済みrecordの変更 | 変更を中止する |
| `InvalidTransition` | 許可外のstatus遷移 | ActiveとSuspendedの往復かRetiredへの遷移を使う |
| `InvalidDomain` | domain形式が不正 | 小文字ASCIIの正規形へ直す |
| `AlreadyClaimed` | 同じaddressの重複claim | 既存claimを使う |
| `NotClaimed` | 存在しないclaimのrelease | claim状況を確認する |
| `TooManyClaims` | 実装上のclaim数上限超過 | 上限内になるまで待つ |
| `SignatureExpired` | deadline超過 | 新しい期限で署名し直す |
| `BadNonce` | 保存nonceと提出nonceが不一致 | 現在nonceで署名し直す |
| `InvalidSignature` | EOAまたはERC1271検証に失敗 | signer、digest、署名形式を確認する |

### `IARDKindAdapter`

`IARDKindAdapter` は、異なるanchor contractからregistryが必要とする5種類の値を同じ形で読むinterfaceです。
adapterのviewは失敗をrevertせず、false、zero address、0で返します。

#### `kind`

```js
function kind() external view returns (bytes32);
```

adapterが担当するkind IDを返す関数です。

1. **adapterへ固定されたkind IDを読み、その値を返します**

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | `keccak256("ard-onchain.kind.<name>")` の値 |

registryはkind登録時に期待値と比較します。

- **実行条件**

- 誰でも呼び出せます。

#### `exists`

```js
function exists(address target, uint256 id) external view returns (bool);
```

指定anchorが現在存在し、そのkindとして読めるか返す関数です。

1. **targetのinterfaceとidの存在を読み、失敗をfalseへ変えて返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `target` | 対象contractまたはaddress |
| `id` | token IDまたはaddress anchorの識別値 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bool` | 存在すればtrue。外部call失敗時はfalse |

- **実行条件**

- 誰でも任意のtargetとidを指定できます。

#### `controllerOf`

```js
function controllerOf(address target, uint256 id) external view returns (address);
```

anchorを現在操作できるaddressを返す関数です。

1. **kind固有のownerまたは主体addressを読み、失敗をzero addressへ変えて返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `target` | 対象contractまたはaddress |
| `id` | 対象ID |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `address` | token ownerまたは主体address。読取失敗時はzero address |

- **実行条件**

- 誰でも任意のtargetとidを指定できます。

#### `backed`

```js
function backed(address target, uint256 id) external view returns (bool);
```

anchorの価値を裏付けるkind固有の状態が現在成立しているか返す関数です。

1. **package、escrow、期限、identityのうちkind固有の条件を読み、boolを返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `target` | 対象contractまたはaddress |
| `id` | 対象ID |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bool` | package、escrow、identityの条件が成立すればtrue |

- **実行条件**

- 誰でも任意のtargetとidを指定できます。

#### `anchorHint`

```js
function anchorHint(address target, uint256 id) external view returns (bytes32);
```

descriptorのprovenanceと照合する値を対象contractから返す関数です。

1. **kind固有のpackage hash、task hash、identity digestを読み、失敗を0へ変えて返します**

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `target` | 対象contractまたはaddress |
| `id` | 対象ID |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | package hash、task hash、identity digest、または0 |

- **実行条件**

- 誰でも任意のtargetとidを指定できます。

### ResolverとGateway

resolverは以下の順で検索用entryを作ります。

1. **recordとadapterのlive viewを読みます**
2. **descriptorURIから文書を取得し、内容memberのhashを照合します**
3. **anchor、gateway alias、binding、canonical URN、level、statusを計算します**
4. **attestorと現在のcontrollerを比較してadoptedを計算します**
5. **内容memberへ公開側memberと計算済みmemberを加えて配信します**

descriptorを取得できない、hashが違う、unadopted、Retiredのrecordは検索結果へ載せません。
unadopted recordは、anchorやrecord IDを指定するinspection endpointで `value:adopted: false` として表示できます。

gatewayはARDのsearch endpointへ `auto`、`referrals`、`none` のfederation modeを実装します。
`auto` は必要に応じて他registryも検索し、`referrals` は照会先を返し、`none` は自分のindexだけを使うモードです。
gateway自身を説明する `application/ai-registry+json` のentryも `/.well-known/ard.json` で公開します。

## 補足

#### 検索関連度とTrust

検索scoreは、利用側が送った検索条件とentryの内容がどれだけ近いかを順位付けする値です。
高いscoreでも、descriptorが現在のcontrollerに採用されている、domainがverified、報酬がbackedという意味にはなりません。

利用側は少なくとも `value:anchor`、`value:descriptorHash`、`value:level`、`value:binding`、`value:status`、`value:adopted` を別々に表示します。
creditも外部registryへの参照であり、levelへ合算しません。

#### 名前と同一性

同じrecordが、controllerのcanonical URN、gateway Aのalias、gateway Bのaliasで返る場合があります。
名前の一致ではなく `value:anchor` の一致で同一recordと判断します。

canonical URNは、publisher domainとのverified bindingとmanifest上の同一identifierがそろう間だけ有効です。
条件を失ったURNはstaleとして表示し、anchor自体は変えません。

#### 価格表示とSettlement

`value:terms` は、call、epoch、license、copyの単位と価格を人やagentへ示します。
`value:settlement` は、支払いrail、payee、asset、endpointを示します。

`onchain` はanchored contractの支払い先を使います。
`x402` と `declared` はdescriptor内のpayeeを使うため、unadopted descriptorを配信しないことが特に重要です。
ERC8444は価格の正しさ、実際の決済、escrow解放を実行しません。

#### Cross chainと可用性

registryは自分と同じchainのanchorだけを検証します。
別chainの同じtokenは別recordであり、複数chainを同一対象として結ぶ方法は将来のprofileに委ねられています。

descriptorやdomain manifestはoffchainで取得します。
規格は取得サイズ、timeout、cache TTL、confirmation数、reorg後の回復、production SLAを定めていません。
運用者はlast sync、参照block、取得失敗、stale cacheを外部から確認できるようにします。

## テストケース

#### 固定値を再計算するvector

公式vectorは、独立実装が同じ値を作れるか確認するための固定入力と期待値です。

| 対象 | 例 | 期待値 |
| --- | --- | --- |
| skill kind | `keccak256("ard-onchain.kind.skill")` | `0x75357da1...8ca7b0bb` |
| record ID | chain 1、target `0x11...11`、id 42 | `0x0d08c6fa...047a6db1` |
| domain hash | `alice.example` | `0xd94dcba6...2b0f7a7` |
| registry descriptor | JCS化した内容member | SHA-256 `0xb5a38766...32445b6` |
| registry interface | 全function selectorのXOR | `0x881908a5` |
| adapter interface | 全function selectorのXOR | `0x67bde273` |

署名vectorでは、Sepoliaのchain ID、registry address、controller、nonce 0、deadlineを使った `claimDomainBySig` が成功し、nonceが1になります。
別chainや別registry、期限切れ、使用済みnonceでは同じ署名を再利用できません。

#### Recordとclaimの失敗条件

公式test suiteは、4 kindの登録に加え、unknown kind、空hash、存在しないanchor、controller以外、同じanchorの二重登録を拒否することを確認します。
token移転後もrecordのcontrollerは新ownerへ変わり、attestorは旧ownerのまま残り、同じhashの再commit後に一致することも確認します。

domainでは、不正形式、重複claim、期限切れ、nonce不一致、署名再利用、EOA署名、ERC1271署名を検証します。
taskでは、報酬あり、取消済み、期限切れを分け、agent identityではactive、stale、retiredを分けます。

#### Gatewayのconformance

参考実装の記録ではFoundry 44 tests、gateway e2e 20 steps、ARD conformanceが成功しています。
gateway e2eは、descriptor hash不一致、domain binding、検索、filter、facet、pagination、transfer後のunadopted、再commit後の再掲載を確認します。

これらは参考実装の測定結果であり、規格上の最低test数ではありません。
Suspendedの検索掲載を検証するe2e caseはなく、その挙動は未解決のままです。

## 互換性

#### 既存ARD Entry

ERC8444の追加memberはARDの拡張可能なJSON schemaへ載ります。
`value` namespaceを知らないregistryも、通常のdisplayName、type、url、dataなどを持つentryとしてindexできます。
既存のdomain-only entryは変更せずL0として残ります。

#### 既存Contractへの接続

skill、tender、identityの既存contractを変更する必要はありません。
adapterが既存viewを読み、ERC8444 registryは対象contractへ書き込みません。

agent identity registryがないchainでは、agent adapterの `backed` を常にfalseにできます。
その場合もL1のagent recordとして検索できますが、L2にはなりません。

#### 関連規格から読む値

agent entryはregistration fileのURIをdescriptorへ載せ、adapterはidentity registryから稼働状態を読みます。
利用側はURIで対象agentの登録情報を特定し、identityがactiveな場合だけbackedとして扱います。
このregistration fileとidentity registryの役割を定める関連規格がERC8004です。

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

[\[ERC8004\] トラストレスに動くエージェントの仕組みを理解しよう！](/articles/4958873135ba9892c298)

descriptorは、紛争判断に使うpolicy IDとdecision registryを `value:dispute` で参照できます。
利用側はその2値から判断記録の正本を開き、ERC8444 recordへ投票結果を複製しません。
複数主体による判断手続きを定める関連規格がERC8436です。

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

[\[ERC8436\] 複数のAIエージェントが投票で作業の承認結果を決める仕組み](/articles/7d23cebc8935c3282be1)

#### DraftとVersionの境界

ERC8444は2026年10月17日時点でDraftです。
前提のARDはv0.91 Proposalであり、schemaとJSON-LD contextは参考実装へ固定版として含まれます。

実装時は、EIPページの版、ARD schemaの版、deploymentのregistry address、4 adapter addressを記録します。
参考実装のtest成功は、Final化や第三者production環境での採用を保証しません。

## 参考実装

公式資料には、オンチェーンkernel、4 adapter、署名helper、Node.js gateway、example descriptor、test vectorが含まれます。
ここでは、規格が要求する責任と参考実装固有の選択を分けて読みます。

![ERC8444参考実装でkernelの保存値、adapterのlive読取、resolverの計算結果を分ける構造](/images/qiita-3083cc05-8f18-4275-b661-0115b0e11b58.webp)

#### Kernelの保存境界

`ARDOnchainRegistry.sol` は、record、kindとadapterの対応、domain claim、claim index、署名nonceをmappingへ保存します。
controller、escrow残高、identity状態は保存しません。
更新のたびにadapterへ問い合わせるため、token移転後に古いcontrollerを権限判定へ使いません。

kind tableはwrite-onceで、kernelはupgradeableではありません。
instance ownerが4 kindを登録した後にownershipをzero addressへ移せば、kind追加権限も終了します。
参考実装のruntime sizeは9,939 bytesですが、これは測定値であり規格上限ではありません。

#### 4つのAdapter

`SkillAdapter.sol` はERC165でskill kernel対応を確認し、ERC721のownerとpackage hashを読みます。
`TaskAdapter.sol` はowner、取消状態、escrow残高、settleBy、task hashを読み、報酬が残る期限内の依頼だけをbackedとします。

`AgentAdapter.sol` はaddress自身をcontrollerとし、設定されたidentity registryのactive状態とself-declared document digestを読みます。
`RegistryAdapter.sol` は非zero addressと非zero discriminatorを確認し、address自身をcontrollerとして返しますが、backedは常にfalseです。

外部callが失敗した時、adapterはrevertせずzero値へ変換します。
この動作により、anchor contractの故障や焼却がregistry全体の一覧処理を止めません。

#### 署名検証

`SignatureChecker.sol` は、controllerがcontractなら最初にERC1271の `isValidSignature` をstaticcallします。
戻り値がmagic valueでなければ拒否します。

EOAでは65 bytesのECDSA署名だけを受け付け、`v`、low-s、zero signerを確認してrecoverします。
malformed responseや外部call失敗はfalseとなり、claim全体がrevertします。

#### Gatewayの同期処理

参考gatewayのindexerは `Registered` イベントからrecord IDを列挙し、各recordの保存値とadapterのlive viewを再読取します。
resolverはdescriptor、hash、binding、level、adoptionを計算します。
serverは全件の解決に成功した時だけ、配信用in-memory indexとlastSyncを置き換えます。

既定の同期間隔は15秒です。
RPC同期に失敗すると直前のindexが残り得ます。
confirmation数、reorg回復、cache TTL、descriptor取得サイズとtimeout、production SLAは規格にも参考実装の契約にも固定されていません。

検索のscore、filter、facet、paginationは参考gateway固有の実装です。
ARDのpageSize上限は100、参考kernelの1 domainあたりclaim上限は64ですが、両者を規格一般の容量上限として扱いません。

#### Sepoliaでの移転と再掲載

公式evidenceは、旧holderがskill recordを登録し、検索結果に載ることを示します。
tokenを新holderへ移すと、adapterのcontrollerは新holderになりますが、recordのattestorは旧holderのままです。
gatewayはunadoptedとして検索から外します。

新holderが同じdescriptor hashを再commitするとattestorが更新され、再び検索結果へ載ります。
この結果は移転とadoptionの流れを実ネットワーク上で確認した証拠ですが、reorg時の回復や長期運用の可用性までは示しません。

## セキュリティ

#### Canonical URNとDomain

verified bindingがないURNをcanonicalとして表示しません。
domainの所有者やmanifestが変わって承認が消えたら、以前のcanonical URNをstaleへ移します。

gateway aliasはgateway自身のdomain内に限定されます。
表示名や説明文はpublisherが自由に書けるため、利用画面ではanchor、binding、levelを合わせて示します。
claimまたはacknowledgmentの片方だけでは、domainが現在の価値情報を支持している根拠になりません。
検索画面はcanonical URNとgateway aliasを同じ信頼度で見せず、bindingの状態と確認時点を併記します。

#### Descriptor差し替え

descriptor URIの配信者がJSONを書き換えても、content hash不一致を検出できます。
ただし、hostが文書を配信しない可用性問題、内容自体の虚偽、publisherが書く価格の妥当性はhashで解決しません。

resolverが不正な結果を返す可能性もあります。
利用側は検索結果のanchorとdescriptorHashをchainで再確認するか、別gatewayと照合します。
同期時刻や参照blockが不明なgatewayでは、古い結果として扱います。

#### 移転と古い支払い先

token移転後はattestorとcurrent controllerが不一致になります。
resolverはそのrecordを検索から外し、旧所有者のendpoint、terms、x402 payeeを新所有者のofferとして配信しません。

inspection endpointから直接読むclientも同じ比較を行います。
levelが残っていてもadoptedでなければ利用しません。
この比較を省くと、移転済みのtokenに紐づくrecordを、旧controllerが今も支持していると取り違えます。
一時停止中のrecordも、levelだけでは利用可否を決めず、`status` と `value:adopted` を個別に確認します。

#### 署名とReplay

署名付きclaimは、chain ID、registry address、controller、nonce、deadlineをdigestへ含めます。
第三者がtransactionを先に送っても、claimは署名者addressへ保存されます。
domain側のacknowledgmentがなければverifiedにもなりません。

秘密鍵管理だけでなく、contract accountが返すERC1271の判定条件も監査対象です。
nonceとdeadlineをUI上で確認できるようにし、意図しない長期署名を避けます。

#### AdapterとInstance owner

adapterはanchorの存在、controller、backingを決める信頼境界です。
deploymentはregistryと4 adapterのaddress、code hash、対応kindを公開します。

kind登録はwrite-onceなので、誤ったadapterを登録すると同じinstanceでは修正できません。
登録前にinterface ID、kind戻り値、外部call失敗時のzero値、対象contractの意味を確認します。
4 kindを固定した後はownershipを維持する理由を明示するか、zero addressへ移して追加権限を終了します。

## 最後に

ERC8444は、通常のARD検索を保ったまま、機能、報酬付きの依頼、AIエージェント、検索先をオンチェーンのanchorへ結び付けます。
実装時は、検索score、binding、level、status、adoptionを混ぜず、descriptorのhashと現在のcontrollerを利用直前に確認することが重要です。

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

https://amzn.asia/d/gxvJ0Pw

LLMを使った仕様駆動開発を扱う書籍『LLMと進める仕様駆動開発』を発売予定です。
ハンズオンや、LLMによって開発がどう変わるのか、実践のTipsをまとめています。

https://www.amazon.co.jp/exec/obidos/ASIN/4863545231/honmori-22

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

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
