One-liner
Cloudflare Workers + D1 で動く Discord bot。/toban-setup, /thx, /balance, /toban-link を提供。extensions/identity を import し、ThanksToken の mintFrom を呼ぶ。bot の署名鍵は Turnkey の TEE で管理し、Worker は秘密鍵を一切保持しない。
Background
Discord 上で /thx @user 10 ありがとう と打つだけで THX が送れるようにする。Bot は permanent state をほぼ持たず、identity(共通ライブラリ)と on-chain mintAllowance を読んで動く。
Identity 検証は bot-mediated 方式 : Discord 自身がスラッシュコマンドを認証する → bot が snowflake を確実に把握する → bot が verifier_token (JWT) を発行する → ユーザーが wallet で IdentityBinding に署名する → identity worker が両方を検証して紐付け確定。Privy には依存しない (Privy は frontend での wallet 署名手段としてだけ使う)。
mintFrom 送信に使う bot signer は Turnkey の sub-organization 内 TEE (AWS Nitro Enclave) に置く。Worker は Turnkey HTTP API を P-256 stamper credentials で叩いて署名を依頼するだけで、Ethereum 秘密鍵自体は Worker・Cloudflare・開発者のいずれも保持しない。Turnkey 側 policy engine で mintFrom 以外の関数呼び出しを構造的に拒否する。
Design
パッケージ構成
pkgs/extensions/discord-bot/
├── src/
│ ├── index.ts # Workers entry
│ ├── interactions/ # Ed25519 検証 + dispatch
│ ├── commands/
│ │ ├── toban-link.ts # admin が guild を tree_id に紐付け
│ │ ├── toban-setup.ts # verifier_token (JWT) 発行 + DM URL
│ │ ├── thx.ts # mintFrom 実行
│ │ └── balance.ts # mintAllowance 表示
│ ├── api/
│ │ └── install/callback.ts # Discord OAuth bot install callback
│ ├── signer/
│ │ └── turnkey.ts # Turnkey 署名 → viem LocalAccount 互換 wrapper
│ ├── verifier.ts # snowflake → verifier_token JWT 発行
│ ├── chain.ts # viem clients
│ └── env.ts # bindings 型定義
├── turnkey/
│ └── policy.json # Turnkey policy を版管理
├── docs/
│ ├── turnkey-setup.md # Turnkey sub-organization セットアップ手順
│ └── key-rotation.md # bot signer rotation runbook
├── migrations/ # discord-bot 固有テーブル(thx_log 等、オプション)
└── wrangler.toml
wrangler.toml
name = " toban-discord-bot"
compatibility_date = " 2026-01-01"
[[d1_databases ]]
binding = " DB"
database_name = " toban-identity"
database_id = " <shared with future bots>"
[vars ]
THANKS_TOKEN_ADDRESS = " 0x..."
TOBAN_FRONTEND_URL = " https://toban.xyz"
RPC_URL = " https://mainnet.base.org"
TURNKEY_ORGANIZATION_ID = " <sub-organization id>"
TURNKEY_BOT_SIGNER_ADDRESS = " 0x..." # Turnkey 上の signing key に対応する Ethereum address
# secrets (wrangler secret put):
# - DISCORD_PUBLIC_KEY # Discord interactions Ed25519 検証用
# - DISCORD_BOT_TOKEN
# - DISCORD_APP_ID
# - TURNKEY_API_PUBLIC_KEY # Turnkey API stamper 公開鍵 (P-256)
# - TURNKEY_API_PRIVATE_KEY # Turnkey API stamper 秘密鍵 (P-256)
# - VERIFIER_PRIVATE_KEY # verifier_token 署名用(identity 側で対応公開鍵を保持)
→ VERIFIER_PRIVATE_KEY の対応公開鍵を extensions/identity の env に登録する こと。identity の providers/discord.ts がこの鍵で verifier_token を検証する。
→ Ethereum 秘密鍵は Workers Secrets に存在しない 。Turnkey の TEE 内にあり、Worker は API stamper credentials だけを保持する。stamper key が漏洩しても、Turnkey policy で mintFrom 以外の関数呼び出しは拒否されるので被害は「全ユーザーの mintAllowance 上限まで」で頭打ち。
Slash commands
Command
誰が
何をする
/toban-link <workspace_url>
admin
Discord guild ↔ Toban tree_id を紐付け
/toban-setup
各ユーザー
identity 連携 URL(verifier_token 同梱)を ephemeral DM
/thx @user <amount> [message]
各ユーザー
THX を送る
/balance
各ユーザー
自分の mintAllowance(self, TURNKEY_BOT_SIGNER_ADDRESS) と mintableAmount を表示
Bot install フロー(admin)
1. Frontend の Workspace ページで「Discord 連携」
2. Discord OAuth install URL に redirect(state JWT に { tree_id, admin_wallet, exp:10min })
3. admin が Discord で server 選択 → callback
4. Bot Worker:
- state JWT を自分の公開鍵で検証
- admin_wallet が tree_id の admin Hat 保有者かを on-chain 検証
- identity.upsertPlatformLink('discord', guild_id, tree_id, admin_wallet)
- Discord に slash commands を guild scope で登録
5. Discord に「✅ Toban に接続されました」メッセージ
/toban-setup フロー(各ユーザー)
1. Discord で /toban-setup
→ Discord が interaction 内に snowflake を含めて bot に送る(Discord 認証済み)
2. Bot:
- verifier_token = JWT.sign(
{ provider: 'discord', accountId: snowflake, exp: now + 15min },
VERIFIER_PRIVATE_KEY,
{ issuer: 'toban-discord-bot', alg: 'ES256' }
)
- ephemeral reply: https://toban.xyz/connect/discord?token=<verifier_token>
3. Frontend (/connect/discord):
- verifier_token をデコードして UI に provider/snowflake を表示
- Privy ログイン → user wallet 取得(Privy の役割はここまで)
- IdentityBinding を構築:
{
wallet, provider:'discord', accountId: snowflake,
verifierTokenHash: keccak256(verifier_token),
expires: now + 30min, nonce: random
}
- Privy 経由で wallet が EIP-712 署名
- POST identity worker /api/connect:
{
provider: 'discord',
verifier_token,
identity_binding: { typedData, signature }
}
4. identity worker:
- providers.discord.verifyVerifierToken(verifier_token) → accountId
- EIP-712 sig recover → wallet
- cross-check 全部合致 → identities upsert
5. ✅ 完了画面
"Discord を連携しました。/thx を使うには bot に mint 許可を設定してください"
[→ /allowance/discord-bot へ]
/thx シーケンス
1. Discord interaction POST → Workers
2. Ed25519 sig 検証
3. 即 DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE で返答(3秒制限)
4. ctx.waitUntil 内で:
- identity.getPlatformLink('discord', guild_id) → tree_id
- identity.getIdentity('discord', sender_sf) → sender wallet
- identity.getIdentity('discord', recipient_sf) → recipient wallet
→ 無し: followup「相手が未連携です。/toban-setup を案内してください」
- tree_id から ThanksToken アドレスを resolve(subgraph or registry)
- viem: mintAllowance(sender, TURNKEY_BOT_SIGNER_ADDRESS) を読む
→ 不足: followup「残り N THX。https://toban.xyz/allowance/discord-bot で増やしてください」
- signer/turnkey.ts: Turnkey API に mintFrom 用 RLP-encoded tx の署名依頼
→ Turnkey が policy 検証 → 通れば署名返却、拒否なら error followup
- viem で broadcast
- tx 確定後 → followup(送信内容+explorer link+残量)
- revert: followup でエラー理由(mintableAmount 不足など)
Bot signer 運用(Turnkey)
Ethereum 秘密鍵は Turnkey の sub-organization 内 (AWS Nitro Enclave) に保管 。生成も Turnkey 内、export 不可
Worker は Turnkey HTTP API (/public/v1/submit/sign_raw_payload 等) を P-256 API stamper で認証して署名を依頼する。stamper credentials だけが Workers Secrets にある
signer/turnkey.ts で viem の LocalAccount 互換 wrapper を実装(signTransaction / signMessage を Turnkey 経由に差し替え)
環境分離: dev / prod で別 sub-organization。各々別 stamper credentials、別 signing address
Turnkey Policy engine で実行可能関数を構造制限 (turnkey/policy.json で版管理、PR レビュー対象):
eth_tx.to ∈ <ThanksToken contract registry> (登録済み Toban contract のみ)
eth_tx.data.selector == 0x<mintFrom selector> (mintFrom 以外は拒否、approveMint も拒否、upgradeTo も当然拒否)
eth_tx.data.from ∈ <subgraph 同期の連携済み wallet 集合> (関係ない EOA からの mint も拒否)
rate limit(per stamper key、Turnkey 側機能成熟後)
三重防壁 :
Turnkey TEE: 秘密鍵が外に出ない(開発者・Cloudflare・Worker いずれも触れない)
Turnkey policy: stamper credential が漏れても mintFrom 以外は呼べない
on-chain mintAllowance + mintableAmount: 上記が突破されても各ユーザー上限以上は mint 不可
Base ETH ガス補充は bot signer の Ethereum address 宛 。残高監視 cron で閾値割り込み時にアラート
鍵 rotation 手順
pkgs/extensions/discord-bot/docs/key-rotation.md に runbook を同梱する。
Scheduled rotation (最低 6 ヶ月に 1 回) :
Turnkey で新 signing key を発行(同 sub-org 内、新 address 0xNEW)
turnkey/policy.json の対象に 0xNEW を追加する PR を作成・マージ・Turnkey に apply
Workers env TURNKEY_BOT_SIGNER_ADDRESS を 0xNEW に更新し wrangler deploy
frontend /allowance/discord-bot から旧 signer 0xOLD に紐づく allowance を持つユーザー一覧を表示し、approveMint(0xNEW, value) への移行 UI を提示
全 platform_links の admin に Discord で告知(bot から各 guild に announce)
7 日間の grace period(旧鍵もまだ有効)後、Turnkey 側で 0xOLD を inactive に切替(新規署名拒否)
14 日後に 0xOLD を disabled (完全停止 & policy から除去)
docs/ops/rotation-log.md に append:
日付 (YYYY-MM-DD)
reason: scheduled / personnel-change / incident-<id>
旧 address / 新 address
実行者
移行率(旧 → 新へ approveMint し直したユーザーの割合)
Emergency rotation (鍵漏洩疑い・異常検知時) :
Turnkey で 0xOLD を即時 disabled (Worker からの署名 API が全失敗するようになる)
新 key 発行 + policy attach + wrangler deploy を 1 時間以内に完遂
全 platform_links admin と全連携ユーザーに緊急通知(Discord DM + frontend バナー)
全ユーザーに「approveMint(0xOLD, 0) で旧 signer を revoke」を強く推奨する UI を /allowance/discord-bot に出す
subgraph で MintFrom の spender == 0xOLD を全件抽出 → 不審 mint の identify と影響範囲確定
インシデントレポート pkgs/extensions/discord-bot/docs/incidents/<YYYY-MM-DD>.md を作成(タイムライン・原因・影響・対策)
Rotation トリガー :
期間: 最後の rotation から 6 ヶ月経過
人事: bot コード maintainer または Turnkey sub-org 権限保持者の入れ替わり
異常検知: Turnkey audit log で mintFrom 以外の API call、policy reject の急増、想定外の from address からの sign 要求
contract upgrade: ThanksToken の mintFrom selector が変わる migration 時
stamper key (Workers Secrets) の rotation も同基準 :
Turnkey 上で新 API stamper を発行 → policy attach → wrangler secret put で更新 → 旧 stamper を Turnkey で revoke
stamper key は signing key より低リスク(policy で守られる)だが手順は同じく runbook 化
Verifier 鍵運用
VERIFIER_PRIVATE_KEY は Turnkey 上の signing key とは別系統 (責務分離: bot signer = on-chain mint、verifier = JWT 発行)
対応公開鍵を extensions/identity の env に DISCORD_BOT_VERIFIER_PUBLIC_KEY として登録
MVP は Workers Secrets で運用。漏洩しても被害は「他人の Discord 連携を奪取できる」止まりで on-chain mint は奪えない(責務分離効果)
将来は verifier key も Turnkey に移す検討
rotation 時は identity 側の公開鍵登録も同時更新(手順は key-rotation.md に同居)
持つ可能性のあるテーブル(オプション)
CREATE TABLE discord_bot_thx_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL ,
sender_sf TEXT NOT NULL ,
recipient_sf TEXT NOT NULL ,
amount TEXT NOT NULL ,
message TEXT ,
tx_hash TEXT ,
status TEXT NOT NULL ,
created_at INTEGER NOT NULL
);
→ subgraph で MintFrom をインデクスすれば代替可能。MVP では Discord 内表示の即時性のため bot ローカルにも持つ可能性あり。
Tasks
Out of scope(別 issue)
活動歴ベースの自動 RoleShare 配布(issue feat(contract): 予約分配モジュール(Scheduled Distribution Module) #505 と連携)
pending THX(相手未連携時の保留)
多 channel → 多 workspace のマッピング(MVP は guild = 1 workspace)
Admin layer の鍵管理(contract owner / upgrade authority。別系統で扱う、本 issue では触れない)
高額 /thx のユーザー対面署名フォールバック(将来)
TEE attested deployment of Worker code(Turnkey で基本要件を満たすので将来要件)
verifier key の Turnkey 化(MVP は Workers Secrets)
Depends on
ThanksToken の mint allowance 実装(child issue)
extensions/identity の実装(child issue)
frontend の /connect/discord ページ実装
One-liner
Cloudflare Workers + D1 で動く Discord bot。
/toban-setup,/thx,/balance,/toban-linkを提供。extensions/identityを import し、ThanksToken のmintFromを呼ぶ。bot の署名鍵は Turnkey の TEE で管理し、Worker は秘密鍵を一切保持しない。Background
Discord 上で
/thx @user 10 ありがとうと打つだけで THX が送れるようにする。Bot は permanent state をほぼ持たず、identity(共通ライブラリ)と on-chainmintAllowanceを読んで動く。Identity 検証は bot-mediated 方式: Discord 自身がスラッシュコマンドを認証する → bot が snowflake を確実に把握する → bot が verifier_token (JWT) を発行する → ユーザーが wallet で IdentityBinding に署名する → identity worker が両方を検証して紐付け確定。Privy には依存しない(Privy は frontend での wallet 署名手段としてだけ使う)。
mintFrom送信に使う bot signer は Turnkey の sub-organization 内 TEE (AWS Nitro Enclave) に置く。Worker は Turnkey HTTP API を P-256 stamper credentials で叩いて署名を依頼するだけで、Ethereum 秘密鍵自体は Worker・Cloudflare・開発者のいずれも保持しない。Turnkey 側 policy engine でmintFrom以外の関数呼び出しを構造的に拒否する。Design
パッケージ構成
wrangler.toml→
VERIFIER_PRIVATE_KEYの対応公開鍵をextensions/identityの env に登録すること。identity のproviders/discord.tsがこの鍵で verifier_token を検証する。→ Ethereum 秘密鍵は Workers Secrets に存在しない。Turnkey の TEE 内にあり、Worker は API stamper credentials だけを保持する。stamper key が漏洩しても、Turnkey policy で
mintFrom以外の関数呼び出しは拒否されるので被害は「全ユーザーのmintAllowance上限まで」で頭打ち。Slash commands
/toban-link <workspace_url>/toban-setup/thx @user <amount> [message]/balancemintAllowance(self, TURNKEY_BOT_SIGNER_ADDRESS)とmintableAmountを表示Bot install フロー(admin)
/toban-setupフロー(各ユーザー)/thxシーケンスBot signer 運用(Turnkey)
/public/v1/submit/sign_raw_payload等) を P-256 API stamper で認証して署名を依頼する。stamper credentials だけが Workers Secrets にあるsigner/turnkey.tsで viem のLocalAccount互換 wrapper を実装(signTransaction/signMessageを Turnkey 経由に差し替え)dev/prodで別 sub-organization。各々別 stamper credentials、別 signing addressturnkey/policy.jsonで版管理、PR レビュー対象):eth_tx.to ∈ <ThanksToken contract registry>(登録済み Toban contract のみ)eth_tx.data.selector == 0x<mintFrom selector>(mintFrom以外は拒否、approveMintも拒否、upgradeToも当然拒否)eth_tx.data.from ∈ <subgraph 同期の連携済み wallet 集合>(関係ない EOA からの mint も拒否)mintFrom以外は呼べないmintAllowance+mintableAmount: 上記が突破されても各ユーザー上限以上は mint 不可鍵 rotation 手順
pkgs/extensions/discord-bot/docs/key-rotation.mdに runbook を同梱する。Scheduled rotation (最低 6 ヶ月に 1 回):
0xNEW)turnkey/policy.jsonの対象に0xNEWを追加する PR を作成・マージ・Turnkey に applyTURNKEY_BOT_SIGNER_ADDRESSを0xNEWに更新しwrangler deploy/allowance/discord-botから旧 signer0xOLDに紐づく allowance を持つユーザー一覧を表示し、approveMint(0xNEW, value)への移行 UI を提示platform_linksの admin に Discord で告知(bot から各 guild に announce)0xOLDを inactive に切替(新規署名拒否)0xOLDを disabled(完全停止 & policy から除去)docs/ops/rotation-log.mdに append:YYYY-MM-DD)scheduled/personnel-change/incident-<id>Emergency rotation (鍵漏洩疑い・異常検知時):
0xOLDを即時 disabled(Worker からの署名 API が全失敗するようになる)wrangler deployを 1 時間以内に完遂platform_linksadmin と全連携ユーザーに緊急通知(Discord DM + frontend バナー)approveMint(0xOLD, 0)で旧 signer を revoke」を強く推奨する UI を/allowance/discord-botに出すMintFromのspender == 0xOLDを全件抽出 → 不審 mint の identify と影響範囲確定pkgs/extensions/discord-bot/docs/incidents/<YYYY-MM-DD>.mdを作成(タイムライン・原因・影響・対策)Rotation トリガー:
mintFrom以外の API call、policy reject の急増、想定外のfromaddress からの sign 要求mintFromselector が変わる migration 時stamper key (Workers Secrets) の rotation も同基準:
wrangler secret putで更新 → 旧 stamper を Turnkey で revokeVerifier 鍵運用
VERIFIER_PRIVATE_KEYは Turnkey 上の signing key とは別系統(責務分離: bot signer = on-chain mint、verifier = JWT 発行)extensions/identityの env にDISCORD_BOT_VERIFIER_PUBLIC_KEYとして登録key-rotation.mdに同居)持つ可能性のあるテーブル(オプション)
→ subgraph で
MintFromをインデクスすれば代替可能。MVP では Discord 内表示の即時性のため bot ローカルにも持つ可能性あり。Tasks
wrangler.toml、env 型定義verifier.ts: snowflake → JWT 発行(ES256)/toban-link,/toban-setup,/thx,/balance実装/api/install/callback実装(OAuth callback + admin Hat 検証)signer/turnkey.ts: Turnkey API で tx 署名する viemLocalAccount互換 wrapperturnkey/policy.json:mintFrom専用 policy を版管理(contract registry, selector, from 集合, rate limit)docs/turnkey-setup.md: Turnkey sub-organization 作成・signing key 発行・stamper credentials 発行・policy apply の手順(dev / prod 別)docs/key-rotation.md: scheduled / emergency rotation runbookmintAllowance/mintFrom呼び出し(署名は Turnkey 経由)Out of scope(別 issue)
/thxのユーザー対面署名フォールバック(将来)Depends on
extensions/identityの実装(child issue)/connect/discordページ実装