本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント・一次情報と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

前回Pontus-X の公開カタログを curl で覗くで、カタログのメタデータは認証なしで誰でも取得できることを確認しました。今回はその続きで、「では誰が参加者なのかは外から分かるのか」を調べます。

きっかけは、テストトークンの配布口 (faucet) を叩いたら弾かれたことでした。何を見て弾いているのかを追いかけたところ、判定に使われている ERC-721 コントラクトと、参加者一覧を返す API に行き着きました。

数値はすべて 2026-08-09 時点に筆者が取得したものです。追試の際は件数が変わっている前提でお読みください。

参加者情報を知る経路が 2 つあることを示した図。左が経路 A で、Pontus-X testnet (chainId 32457) 上の Identity Issuer NFT (symbol: ISSUER、0xCe1750f8… / totalSupply 675) が起点。そこから 2 本に分かれ、一方は faucet が balanceOf() を直接呼んで配布の可否を判定する流れ、もう一方はインデクサが取り込んで Registry Cache API (cache.registry.pontus-x.eu、GET /v1/identities で 665 件) となり、本記事の集計に使われる流れ。API が返す項目として legalName・VAT (Value Added Tax、付加価値税) 番号・登記住所・記録時刻が挙げられている。右が経路 B で、deltaDAO/mvg-portal の main に同梱された静的ファイル pontusxAddresses.json (968 アドレス / 773 組織、最終更新 2026-06-12、フォークも継承) がビルド時に import され、ポータル画面の publisher 名表示に使われる流れ。一覧に無いアドレスは生のアドレスのまま表示される。図のサブタイトルには「2026-08-09 に筆者が実測した件数。両者は同じ台帳の別スナップショットではなく、更新の経路が異なる」とある。下部に、両者に載るアドレスの重なりは 536 件で、経路 A にのみ 129 件、経路 B にのみ 432 件あることが記されている。

faucet が何を見ているのか

Pontus-X の faucet は 2 段階です。まず get_nonce でノンスを取り、それを含むメッセージにウォレットで署名して request_tokens/{network} に送ります。ポータル側の実装 (src/@utils/faucet/index.ts) を読むと、この 2 本だけでした。

手元の鍵とは関係のないバーンアドレス (0x…dEaD) で 1 段目を叩いてみます。

curl -s -X POST https://faucet.api.pontus-x.eu/get_nonce \
  -H 'content-type: application/json' \
  -d '{"address":"0x000000000000000000000000000000000000dEaD"}'
{"message":"Address not authorized","error":"Bad Request","statusCode":400}

署名の前段階で弾かれます。つまりノンスを出す時点で、そのアドレスが参加者かどうかを判定しています。

判定方法を知りたいので、あえてゼロアドレスで試すと、別のエラーが返りました。

{"message":"call revert exception [ … ] (method=\"balanceOf(address)\",
 data=\"0x89c62b640000000000000000000000000000000000000000000000000000000000000000\",
 … code=CALL_EXCEPTION, version=abi/5.8.0)","error":"Bad Request","statusCode":400}

エラーが素通しで返ってきているおかげで、内部の呼び出しが見えます。balanceOf(address) を呼んでいて、返り値の先頭 4 バイトが 0x89c62b64 です。これはカスタムエラーのセレクタなので、手元で照合します。

ethers v5 が入っている前提です(v6 には utils エクスポートがないので、npm i ethers@5 で入れています)。

node -e "const {utils}=require('ethers'); \
  console.log(utils.id('ERC721InvalidOwner(address)').slice(0,10))"
0x89c62b64

一致しました。ERC721InvalidOwner は OpenZeppelin の ERC-721 実装がゼロアドレスに対して投げるエラーです。したがって faucet は、ある ERC-721 トークンの保有数をチェックして、保有していないアドレスにはトークンを出さないという実装になっていると読めます。ゼロアドレスの場合は保有数を数える前にコントラクト側の入力検証で落ちた、というわけです。

公式ドキュメントのオンボーディングガイド参加者レジストリのページには、エコシステムに参加できるのは法人 (legal person) だと書かれています。身元の確認は、法人の登録番号(ドキュメントの例示は VAT ID・EORI (Economic Operators Registration and Identification、EU の事業者登録番号)・LEI (Legal Entity Identifier、取引主体識別子) Code)と、組織のメールアドレスによるドメイン検証を deltaDAO が行う、という手順です。ドキュメントは "e.g." と書いているので、この 3 つに限定されるわけではなさそうです。Gaia-X Trust Framework への完全準拠は任意でネイティブに対応している、と書かれていて(筆者訳)、必須の関門ではありません。faucet が見ている ERC-721 (Ethereum Request for Comments 721、代替不可能なトークンの標準) は、ここで発行される参加者クレデンシャルに対応するものと考えるのが自然です。

コントラクトをどう特定するか

エラーメッセージにはコントラクトのアドレスが含まれていません。そこで最初に考えたのは、チェーンから ERC-721 の発行 (Transfer イベントの from がゼロアドレス) を総当たりで探す方法です。しかし公開 RPC (Remote Procedure Call、ここではチェーンに問い合わせる API エンドポイント) には制限がありました。

R=https://rpc.dev.pontus-x.eu
LATEST=$(curl -s -X POST $R -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \
  | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(parseInt(JSON.parse(s).result,16)))")
FROM=$((LATEST-20000))

curl -s -X POST $R -H 'content-type: application/json' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{
       \"fromBlock\":\"0x$(printf %x $FROM)\",\"toBlock\":\"latest\",
       \"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",
                 \"0x0000000000000000000000000000000000000000000000000000000000000000\"]}]}"
{"code":-32000,"message":"invalid request: max allowed of rounds in logs query is: 5000"}

1 回のクエリで 5,000 ブロックまでです。devnet の現在高は 13,843,809 でしたから、全履歴を舐めるには約 2,770 回のリクエストが必要になります。公開エンドポイントに対してそれを回すのは、追試の再現性という意味でもあまり良い手ではありません。

代わりに、既にインデックスしている場所を探しました。ブロックエクスプローラ (explorer.pontus-x.eu) はシングルページアプリケーションで、HTML 自体は 1KB 程度しかありません。参照している JavaScript を落として URL を抜き出します。

curl -s https://explorer.pontus-x.eu/assets/index-uOTGEUh4.js \
  | grep -o "https\?://[a-zA-Z0-9./_%-]*" | sort -u | grep -i "api\|nexus\|registry"
https://cache.registry.pontus-x.eu
https://nexus.oasis.io/v1/
…

cache.registry.pontus-x.eu が見つかりました。ルートを叩くと素朴な稼働確認が返ります。

curl -s https://cache.registry.pontus-x.eu/
Hello World! Database connection is healthy. Indexer States: 1, Identities: 665

/api に Swagger UI が置かれていました。参加者レジストリのページには「スマートコントラクトを源泉とし、キャッシュされた REST API 経由で配られる」と書かれているので、存在自体は説明されています。ただし、この URL への直接のリンクは見つけられませんでした。

Registry Cache API の Swagger UI のスクリーンショット。タイトルは「Registry Cache API 1.0 / OAS 3.0」、説明文は「Cache layer of the Pontus-X on-chain registry data」。App グループに GET /、Identities グループに GET /v1/identities (Get all identities with optional search and pagination) と GET /v1/identities/{contractAddress}/{walletAddress} (Get a single identity by contract and wallet address) の 2 本が並び、下部の Schemas に IdentityDto、PaginationMetaDto、GetIdentitiesResponseDto が表示されている。

自称は「Cache layer of the Pontus-X on-chain registry data」です。オンチェーンのレジストリを取り込んで REST で返すキャッシュ層、という位置づけになります。

Registry Cache API の使い方

冒頭の図で「経路 A」としたのが、この API です。

OpenAPI 仕様は /api-json で取得できます。

curl -s https://cache.registry.pontus-x.eu/api-json \
  | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{
      const j=JSON.parse(s);
      Object.entries(j.paths).forEach(([p,m])=>
        console.log(Object.keys(m).join(',').toUpperCase().padEnd(6),p));})"
GET    /
GET    /v1/identities
GET    /v1/identities/{contractAddress}/{walletAddress}

クエリパラメータは 3 つだけでした。

パラメータ既定値内容
page1ページ番号 (1 始まり)
limit201 ページあたりの件数
search法人名での絞り込み(大文字小文字を区別しない。仕様書の記載はここまでで、一致方式は明記されていない)

日付やコントラクト単位での絞り込みは用意されていないので、全件取ってから手元で処理することになります。件数は 665 なので limit を大きめにすれば一括で取れました。

curl -s "https://cache.registry.pontus-x.eu/v1/identities?page=1&limit=1000" > identities.json

1 件あたりの内容は、想像していたより詳細でした。

{
  "walletAddress": "0x…",
  "contractAddress": "0xCe1750f8A5A645935F6451f6a92Ed1968F6B4430",
  "tokenId": "836",
  "txHash": "0x…",
  "lastBlockNumber": "11697809",
  "blockTime": "2026-08-03T13:13:02.000Z",
  "legalName": "(法人名)",
  "presentationUrl": "https://credentials.delta-dao.com/presentations/…",
  "credentialsData": {
    "gx:VatID": { "gx:vatID": "(VAT 番号)", "gx:countryCode": "DE" },
    "gx:LegalPerson": {
      "schema:name": "(法人名)",
      "gx:legalAddress": {
        "gx:countryCode": "DE", "vcard:locality": "(市区町村)",
        "vcard:postal-code": "(郵便番号)", "vcard:street-address": "(番地)"
      }
    }
  }
}

構造を示すことが目的なので、個別の法人が特定される値は伏せています(実際のレスポンスには具体的な値が入ります)。法人名、VAT 番号、登記住所、そして記録されたトランザクションとブロック時刻まで含まれます。Gaia-X Trust Framework では、登録番号は VAT ID のほか EORI・EUID (EU 共通の会社識別子)・LEI といった形式も認められているので、VAT 番号はその一形式という位置づけです。

presentationUrl を開くと、Gaia-X のクレデンシャルを内包した W3C Verifiable Presentation(JWT 形式)が返ります。「参加者は公開の識別子で識別される」という設計を、そのまま実装したものと読めます。

どのチェーンに置かれているのか

contractAddress が分かったので、どのネットワークにあるのかを確認します。Pontus-X には Devnet (chainId 32456) と Testnet (chainId 32457) があるので、両方に eth_getCode を投げます。

for n in dev test; do
  printf "%s: " "$n"
  curl -s -X POST https://rpc.$n.pontus-x.eu -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
         "params":["0xCe1750f8A5A645935F6451f6a92Ed1968F6B4430","latest"]}'
  echo
done
dev:  {"jsonrpc":"2.0","id":1,"result":"0x"}
test: {"jsonrpc":"2.0","id":1,"result":"0x363d3d373d3d3d363d73aec051fe6ad9…5af43d82803e903d91602b57fd5bf3"}

Devnet には存在せず、Testnet にのみありました。念のため、API が返した lastBlockNumberblockTime の対応も確認します。

curl -s -X POST https://rpc.test.pontus-x.eu -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["0xb27e91",false]}' \
  | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>
      console.log(new Date(parseInt(JSON.parse(s).result.timestamp,16)*1000).toISOString()))"
2026-08-03T13:13:02.000Z

API の blockTime と一致しました。Devnet の同じ番号のブロックは 2026-03-21 で、一致しません。参加者レジストリは Testnet 側に置かれている、と確認できます。

なお Oasis Sapphire の Mainnet (23294) と Testnet (23295) にも同じアドレスを問い合わせましたが、どちらにもコントラクトはありませんでした。Pontus-X は Oasis の技術基盤を使っていますが、Sapphire の公開ネットワークとは別のチェーンとして動いています。

コントラクト自体にも問い合わせてみます。

関数セレクタは name()0x06fdde03symbol()0x95d89b41totalSupply()0x18160ddd です。

curl -s -X POST https://rpc.test.pontus-x.eu -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[
       {"to":"0xCe1750f8A5A645935F6451f6a92Ed1968F6B4430","data":"0x06fdde03"},"latest"]}'

デコードすると次のようになりました。

呼び出し返り値
name()Identity Issuer NFT
symbol()ISSUER
totalSupply()675

バイトコードが 45 バイトしかないのも特徴的で、これは EIP-1167 (EIP = Ethereum Improvement Proposal) の最小プロキシ (minimal proxy) の形をしています。パターンに当てはめると、実装は 0xaec051fe6ad9da227ccf0d4cc63c31f45e08551f に置かれていました。テンプレートを複製して使う設計になっているようです。

totalSupply() が 675 で API が返す件数が 665 なので、10 件の差があります。この差の理由は今回は確認できませんでした。

念のため、特定したコントラクトに faucet と同じ呼び出しを直接投げて突き合わせました。

curl -s -X POST https://rpc.test.pontus-x.eu -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[
       {"to":"0xCe1750f8A5A645935F6451f6a92Ed1968F6B4430",
        "data":"0x70a082310000000000000000000000000000000000000000000000000000000000000000"},"latest"]}'
{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted",
 "data":"0x89c62b640000000000000000000000000000000000000000000000000000000000000000"}}

faucet が漏らしたリバートデータとバイト単位で同じものが返りました。バーンアドレスで同じ呼び出しをすると素直に残高 0 が返るので、get_nonce の「Address not authorized」はこの残高 0 に対応していると読めます。推論ではなく、突き合わせで確かめられた形です。

665 件を集計してみる

取得した JSON を月別に数えます。

node -e "
const a=require('./identities.json').data;
const m={}; a.forEach(x=>{const k=(x.blockTime||'').slice(0,7); m[k]=(m[k]||0)+1});
Object.keys(m).sort().forEach(k=>console.log(k, String(m[k]).padStart(4), '|'+'#'.repeat(Math.min(60,m[k]))));"
2025-11    6 |######
2025-12   29 |#############################
2026-01    7 |#######
2026-02  271 |############################################################
2026-03  309 |############################################################
2026-04   13 |#############
2026-05   23 |#######################
2026-06    3 |###
2026-07    3 |###
2026-08    1 |#

最古が 2025-11-17、最新が 2026-08-03 でした。

ここで注意が必要なのは、blockTime は「オンチェーンに記録された時刻」であって、その組織が審査を通った時刻とは限らない、という点です。2026 年 2〜3 月に 665 件中 580 件が集中しているのは、既存の参加者をまとめてオンチェーンへ移行した結果と読むのが自然です。運営元である deltaDAO AG の名義で登録されたエントリ 91 件も、2025-11-17 から 2026-05-28 に分布し、そのうち 70 件が 2026-03 に集中していました。調査した限り、公式ドキュメントの参加者レジストリのページにも、レジストリ 1.0 が手動運用で、その後スマートコントラクトを源泉とする方式へ移行したと記載があります。

したがってこのデータから読み取れるのは「いつ台帳に載ったか」までで、「いつ承認されたか」「なぜ承認されたか」は、公開されている範囲では確認できませんでした。

大学や図書館に該当しそうな法人名を、librar / bibliot / archiv / museum / universit / universi / hochschule / akadem のいずれかを含むもの、という粗いキーワードで抜き出すと 51 件(22 組織)ありました。一部を挙げます。1 つの組織が複数のアドレスを持つ場合があるので、同じ名前が別の日付で複数回現れることもあります。

2026-02-26  Staatsbibliothek zu Berlin - Preußischer Kulturbesitz
2026-02-27  University of British Columbia
2026-04-16  Czech Technical University in Prague
2026-04-16  Universität Stuttgart
2026-04-21  Technische Universität Wien
2026-05-04  Universitat de Lleida

前回のカタログ調査では製造・農業ドメインの厚みが目立ちましたが、参加者一覧のほうを見ると、図書館や大学も入っていることが分かります。

ポータルに同梱されている一覧との違い

参加者名を知る経路は、実はもう 1 つあります。Ocean Market 系のポータル実装 (deltaDAO の mvg-portal) には pontusxAddresses.json という静的ファイルが同梱されていて、画面上の publisher 名はここから引かれています。冒頭の図で「経路 B」としたものです。

両者を突き合わせてみます。ここでは deltaDAO/mvg-portal の main ブランチにある現行のファイルを使います。

curl -s https://raw.githubusercontent.com/deltaDAO/mvg-portal/refs/heads/main/pontusxAddresses.json \
  -o pontusxAddresses.json

node -e "
const api=require('./identities.json').data;
const st=require('./pontusxAddresses.json');
const A=new Set(api.map(x=>x.walletAddress.toLowerCase()));
const B=Object.keys(st).map(x=>x.toLowerCase());
console.log('API :', api.length, 'アドレス /', new Set(api.map(x=>x.legalName)).size, '組織');
console.log('JSON:', B.length, 'アドレス /', new Set(Object.values(st)).size, '組織');
console.log('JSON のみ:', B.filter(x=>!A.has(x)).length);
console.log('API  のみ:', [...A].filter(x=>!B.includes(x)).length);
console.log('重なり  :', B.filter(x=>A.has(x)).length);"
API : 665 アドレス / 471 組織
JSON: 968 アドレス / 773 組織
JSON のみ: 432
API  のみ: 129
重なり  : 536

静的ファイルのほうが件数は多く、重なっているのは 536 件でした。API 側にしか無いものが 129 件、静的ファイル側にしか無いものが 432 件です。

pontusxAddresses.json の Git 履歴を見ると、参加者を 1 件ずつプルリクエストで追記する運用が続いていて、この記事を書いている時点の最終更新は 2026-06-12(Update pontusxAddresses.json (#848))でした。オンチェーンのレジストリに最初の記録が入るのが 2025-11-17 なので、片方が止まってもう片方に移ったのではなく、2 つの経路が並行して維持されている状態のようです。そして内容は一致していません。

なお、このファイルはフォークにもそのまま引き継がれます。手元にあった ciferresearch/ClioX-mvg-portal(Ocean Market 系ポータルのフォークの一つ)の同名ファイルは 680 アドレス / 505 組織で、当該ファイルの最終コミットは 2025-04-16 でした。フォーク側は上流の更新を追従しない限り、その時点のスナップショットのまま残ります。

この静的ファイルを読む実装では、一覧に無いアドレスはウォレットアドレスのまま表示されます。ポータルをフォークして使う場合、参加者名の表示をどちらの経路に合わせるかは、意識しておく必要がありそうです。

まとめと、確認できなかったこと

調べた範囲を整理します。

  • テストトークンの配布口は、ERC721InvalidOwner のセレクタから判断する限り、ERC-721 の保有数で可否を判定している
  • そのコントラクトは Pontus-X Testnet (32457) 上の 0xCe1750f8…Identity Issuer NFT (symbol ISSUER)、EIP-1167 の最小プロキシ
  • 参加者一覧は cache.registry.pontus-x.eu のキャッシュ API から認証なしで取得でき、法人名・VAT 番号・登記住所・記録時刻まで含まれる
  • 2026-08-09 時点で 665 件。記録時刻の分布からは、既存参加者の一括移行が含まれると読める
  • ポータル同梱の静的ファイルとは内容が大きく異なり、重なりは 536 件(API 側にしか無いものが 129 件、静的ファイル側にしか無いものが 432 件)

確認できなかったことも挙げておきます。

  • totalSupply() の 675 と API の 665 の差 (10 件) の理由
  • 実装コントラクト 0xaec051fe… の中身 (ソースコードは未確認)
  • 承認の基準や過程。ドキュメントには法人であることと、登録番号およびドメイン検証による確認が示されていますが(Gaia-X Trust Framework への完全準拠は任意とされています)、個別の判断が公開されているわけではありません
  • 個人 (自然人) が参加できるかどうか。ドキュメントには法人のみと書かれており、実際にどう運用されているかまでは追えていません

読み取り系のエンドポイントは、今回叩いた範囲ではすべて認証なしで応答しました。カタログに続いて参加者一覧も外から確認できるので、「データスペースに誰がいるのか」を調べる用途では、この API が出発点として使えそうです。