Interact with the RecyReport smart contract on Sepolia — role-based recycling system with wallet management, report minting, recycling results, and auditing. Use for ANY interaction about recycling reports, wallets, or the RecyReport contract.
Role-based recycling report system on Ethereum Sepolia testnet.
0x91e3E6F9672E985100b8F0798d2cB55fa53c66Da11155111)https://rpc.sepolia.org (fallback: https://ethereum-sepolia-rpc.publicnode.com)You MUST use openclaw message send --buttons via exec to send inline buttons. Do NOT output button JSON as text. Do NOT use [[reply_to_current]] for messages that need buttons — use this command instead.
The sender metadata includes sender_id — use it as --target.
openclaw message send \
--channel telegram \
--target "<sender_id>" \
--message "Your text here" \
--buttons '[[{"text":"Button Label","callback_data":"callback_value"}]]'
--buttons takes a JSON array of rows. Each row is an array of button objects:
[[{"text":"Row1 Btn1","callback_data":"val1"},{"text":"Row1 Btn2","callback_data":"val2"}],[{"text":"Row2 Btn1","callback_data":"val3"}]]
exec with openclaw message send --buttons for ANY message that includes choices, menus, or confirmations[[reply_to_current]] when the message needs buttons — it cannot carry buttons[[reply_to_current]] is finesender_id from the conversation metadata at the top of each messagecallback_data value as their next message| File | Content |
|---|---|
references/contract-abi.json | Full ABI in JSON format |
references/functions.md | Human-readable function reference with role mapping |
references/api.md | Recy API endpoints for fetching report data |
references/roles.md | Role definitions, permissions, and onboarding flows |
references/enums.md | Material, recycle type, shape, disposal method enum values + weight units |
Read all reference files before handling any user interaction.
All API calls require authentication. Use these scripts instead of calling the API directly:
node scripts/api-fetch.mjs <api-path> <wallet-file> [token-cache-file]
Examples:
# Report info (any role) — use bot wallet
node scripts/api-fetch.mjs /recy-report/info/4 memory/wallets/_bot.json memory/auth-token-bot.json
# Materials (auditor) — use user's wallet for their own auth context
node scripts/api-fetch.mjs /recy-report/materials/4 memory/wallets/429028317.json memory/auth-tokens/429028317.json
# Proofs (auditor)
node scripts/api-fetch.mjs /recy-report/4/proofs memory/wallets/429028317.json memory/auth-tokens/429028317.json
node scripts/wallet-auth.mjs <wallet-file> [token-cache-file]
# Prints Bearer token to stdout
node scripts/auth-and-fetch.mjs
# Authenticates with bot wallet, fetches all reports, writes memory/reports-cache.json
IMPORTANT:
https://api.crecy.workers.dev/v1 — NEVER use app.crecy.workers.dev (that's the web dashboard login page).Every new Telegram user must go through onboarding before they can do anything.
When a user first interacts with the bot (or sends /start):
memory/wallets/<telegram_user_id>.jsonrole is null/missing → skip to Step 2 (role selection)node scripts/create-wallet.mjs <telegram_user_id>
This creates memory/wallets/<telegram_user_id>.json with: address, privateKey, seedPhrase, derivationPath, role (null).
The seed phrase is required for Velora token swaps.Send role selection buttons via exec:
openclaw message send \
--channel telegram \
--target "<sender_id>" \
--message "Welcome to Recy! 🌱 Your wallet: \`0x...\`\n\nChoose your role:" \
--buttons '[[{"text":"♻️ Waste Generator","callback_data":"role_generator"},{"text":"🔄 Recycler","callback_data":"role_recycler"},{"text":"✅ Auditor","callback_data":"role_auditor"}]]'
When the user clicks a button, you receive their choice as a message with the value string (e.g. "role_generator"). Map it:
role_generator → role "generator"role_recycler → role "recycler"role_auditor → role "auditor"Store the chosen role in the user's wallet file: memory/wallets/<telegram_user_id>.json
Waste Generator: Ready to go immediately. No on-chain role needed.
openclaw message send --channel telegram --target "<sender_id>" \
--message "You're all set as a Waste Generator! You can mint recycling reports." \
--buttons '[[{"text":"📝 Mint Empty Report","callback_data":"action_mint"},{"text":"📋 My Reports","callback_data":"action_my_reports"},{"text":"💰 Claim Reward","callback_data":"action_claim"}]]'
Recycler: Requires on-chain RECYCLER role from an admin.
0x.... To get the Recycler role, please send this address to a Recy Admin."Auditor: Requires on-chain AUDITOR role from an admin.
0x.... To get the Auditor role, please send this address to a Recy Admin."When a user sends /menu or asks "what can I do?", send buttons via exec with openclaw message send.
openclaw message send --channel telegram --target "<sender_id>" \
--message "♻️ Waste Generator Menu" \
--buttons '[[{"text":"📝 Mint Empty Report","callback_data":"action_mint"},{"text":"📋 My Reports","callback_data":"action_my_reports"}],[{"text":"🔍 View Report","callback_data":"action_view_report"},{"text":"💰 Claim Reward","callback_data":"action_claim"}],[{"text":"👛 My Wallet","callback_data":"action_wallet"}]]'
openclaw message send --channel telegram --target "<sender_id>" \
--message "🔄 Recycler Menu" \
--buttons '[[{"text":"📋 Pending Reports","callback_data":"action_pending"},{"text":"📝 Mint With Results","callback_data":"action_mint_result"}],[{"text":"✏️ Set Results","callback_data":"action_set_result"},{"text":"🔍 View Report","callback_data":"action_view_report"}],[{"text":"👛 My Wallet","callback_data":"action_wallet"}]]'
openclaw message send --channel telegram --target "<sender_id>" \
--message "✅ Auditor Menu" \
--buttons '[[{"text":"📋 Awaiting Audit","callback_data":"action_awaiting_audit"},{"text":"🔍 View Report Details","callback_data":"action_view_report_audit"}],[{"text":"✅ Validate Report","callback_data":"action_validate"},{"text":"❌ Invalidate Report","callback_data":"action_invalidate"}],[{"text":"👛 My Wallet","callback_data":"action_wallet"}]]'
When a user clicks a button, you receive the value string as their message. Map these to actions:
| Value | Action |
|---|---|
action_mint | Start mint empty report flow |
action_my_reports | Fetch user's reports from cache |
action_view_report | Ask for token ID, then fetch info |
action_claim | Ask for token ID, then claim reward flow |
action_wallet | Show wallet address + ALL balances (ETH, CRECY, USDT) |
action_pending | Show reports without results (recycler) |
action_mint_result | Start mint-with-results flow |
action_set_result | Ask for token ID, then set results flow |
action_awaiting_audit | Show reports ready for audit |
action_view_report_audit | Ask for token ID, show info + materials + proofs |
action_validate | Ask for token ID, then validate flow |
action_invalidate | Ask for token ID, then invalidate flow |
action_menu | Show role-based menu (Section 2) |
action_swap_crecy_usdt | Start swap flow: CRECY → USDT |
action_swap_usdt_crecy | Start swap flow: USDT → CRECY |
action_send_crecy | Start send CRECY flow |
action_send_usdt | Start send USDT flow |
confirm_tx | User confirmed a pending transaction — execute it |
cancel_tx | User cancelled a pending transaction — abort and show menu |
report_<id> | User selected a specific report — show details with action buttons |
claim_<id> | Claim reward for specific report — show confirmation |
validate_<id> | Validate specific report — show confirmation |
invalidate_<id> | Invalidate specific report — show confirmation |
set_result_<id> | Set results for specific report — start data entry flow |
When showing a list of reports, use buttons for each report:
openclaw message send --channel telegram --target "<sender_id>" \
--message "Pending Reports (3 found):" \
--buttons '[[{"text":"#12 — 150kg, 2026-03-20","callback_data":"report_12"}],[{"text":"#15 — 80kg, 2026-03-21","callback_data":"report_15"}],[{"text":"#18 — 200kg, 2026-03-22","callback_data":"report_18"}]]'
NEVER ask the user to type when you can offer buttons instead. This applies to ALL interactions:
Only use text input for truly free-form data: amounts, addresses, dates, custom descriptions.
ALL buttons MUST be sent via exec with openclaw message send --buttons. NEVER output buttons as text.
Before any write transaction, show details and offer confirm/cancel:
openclaw message send --channel telegram --target "<sender_id>" \
--message "📝 Mint Empty Report\n\nThis will create a new recycling report NFT.\nEstimated gas: ~0.001 ETH\n\nProceed?" \
--buttons '[[{"text":"✅ Confirm","callback_data":"confirm_tx"},{"text":"❌ Cancel","callback_data":"cancel_tx"}]]'
After showing report details, include role-appropriate action buttons:
Waste Generator viewing their report:
openclaw message send --channel telegram --target "<sender_id>" \
--message "Report #12\nStatus: Validated\nWaste: 150kg\nUnlock: 2026-04-20" \
--buttons '[[{"text":"💰 Claim Reward","callback_data":"claim_12"},{"text":"🔙 Back to Menu","callback_data":"action_menu"}]]'
Auditor viewing a report for audit:
openclaw message send --channel telegram --target "<sender_id>" \
--message "Report #12 - Audit View\n..." \
--buttons '[[{"text":"✅ Validate","callback_data":"validate_12"},{"text":"❌ Invalidate","callback_data":"invalidate_12"}],[{"text":"🔙 Back","callback_data":"action_awaiting_audit"}]]'
Recycler viewing a pending report:
openclaw message send --channel telegram --target "<sender_id>" \
--message "Report #12\nStatus: Awaiting Results\n..." \
--buttons '[[{"text":"✏️ Set Results","callback_data":"set_result_12"},{"text":"🔙 Back","callback_data":"action_pending"}]]'
After a successful transaction, offer next steps:
openclaw message send --channel telegram --target "<sender_id>" \
--message "✅ Report minted successfully!\n\nTx: sepolia.etherscan.io/tx/0x...\nToken ID: #25" \
--buttons '[[{"text":"📝 Mint Another","callback_data":"action_mint"},{"text":"📋 My Reports","callback_data":"action_my_reports"},{"text":"🔙 Menu","callback_data":"action_menu"}]]'
}
Always include a way to get back. The value action_menu should show the user's role-based menu (Section 2).
IMPORTANT: Do NOT query the blockchain for report listings. Use the Recy API instead.
API Base URL: https://api.crecy.workers.dev/v1 (NOT app.crecy.workers.dev — that's the web dashboard)
All API calls require a Bearer token. See references/api.md for the full wallet-based auth flow.
Two wallet contexts:
| Context | Wallet File | Used For |
|---|---|---|
| Bot (cron) | memory/wallets/_bot.json | Report list fetching, system operations |
| Per-user | memory/wallets/<telegram_user_id>.json | User-initiated API calls + contract transactions |
Auth flow (for both contexts):
POST /auth/wallet/challenge with the wallet address → get challenge messagepersonal_sign)POST /auth/wallet/verify with address, signature, nonce, expiresAt, mac → get Bearer token (4 hours)memory/auth-token-bot.json (bot) or memory/auth-tokens/<user_id>.json (user)The cron job authenticates with the bot wallet and fetches ALL reports once per cycle from:
GET https://api.crecy.workers.dev/v1/recy-report/list?scope=all
Authorization: Bearer <bot-token>
This data is stored in memory/reports-cache.json with a timestamp. When users request report lists, read from this cache — do NOT re-fetch the API on every user interaction.
All detail endpoints use the user's auth token:
| Endpoint | Use Case |
|---|---|
https://api.crecy.workers.dev/v1/recy-report/info/<token-id> | Basic report info (all roles) |
https://api.crecy.workers.dev/v1/recy-report/materials/<token-id> | Materials breakdown (Auditors only) |
https://api.crecy.workers.dev/v1/recy-report/<token-id>/proofs | Recycling proofs (Auditors only) |
https://api.crecy.workers.dev/v1/recy-report/<token-id>/proof/<proofId> | Download specific proof file (Auditors only) |
From the cached report list, filter based on role:
Waste Generator: Show only reports where the user's address is the generator/owner.
Recycler — "Pending Reports": Show reports that have been minted but do NOT yet have recycling results set (status indicates no results). These are the reports recyclers need to process.
Auditor — "Reports Awaiting Audit": Show reports that HAVE results set but have NOT yet been validated or invalidated. These are the reports auditors need to review.
| Action | Contract Function | Notes |
|---|---|---|
| Mint empty report | mintRecyReport() | No params, creates empty NFT |
| Claim reward | claimRecyReportReward(tokenId) | Only after validation + unlock |
| View reports | API: /recy-report/info/<id> | Read-only |
| Action | Contract Function | Notes |
|---|---|---|
| Mint with results | mintRecyReportResult(generator, date, amount, materials, amounts, types, shapes, disposal) | Full report in one tx |
| Set results on existing | setRecyReportResult(tokenId, date, amount, materials, amounts, types, shapes, disposal) | Fill in empty report |
| View pending reports | Cached API data | Filter: no results |
| View report details | API: /recy-report/info/<id> | Read-only |
| Action | Contract Function | Notes |
|---|---|---|
| Validate report | validateRecyReport(tokenId) | Requires AUDITOR role |
| Invalidate report | invalidateRecyReport(tokenId) | Requires AUDITOR role |
| View materials | API: /recy-report/materials/<id> | Review what was recycled |
| View proofs | API: /recy-report/<id>/proofs | Review evidence |
| View report details | API: /recy-report/info/<id> | Read-only |
Always present report lists as clickable buttons (see report list button example in Section 2). Never ask users to type report numbers.
When showing report details, use the message text for info and buttons for actions. See the role-specific report detail button examples in the "UNIVERSAL RULE" section above.
Material fields are numeric indices. Always resolve them to human-readable names using references/enums.md:
material: 2 → "Glass"recycleType: 6 → "Thermal Recycling"recycleShape: 1 → "Pellets"disposalMethod: 3 → "Recycling"All amounts on-chain and from the API are in milligrams. Always convert to the most sensible unit:
| Raw (mg) | Display |
|---|---|
| < 1,000 | X mg |
| 1,000 – 999,999 | X.XX g |
| 1,000,000 – 999,999,999 | X.XX kg |
| ≥ 1,000,000,000 | X.XX t |
Examples: 13000000 → 13 kg, 500000 → 500 g, 1500000000 → 1.5 t
Apply this to: wasteAmount, amountRecycled, and any material amounts.
Report #4 — Audit View
Status: Awaiting Audit
Generator: 0x20BA...8FD9
Recycler: 0x88A8...5ECe
Waste: 13 kg
Recycled: 2026-03-15
Materials:
• Glass — 13 kg
Thermal Recycling · Pellets · Recycling
[✅ Validate] [❌ Invalidate] [🔙 Back]
import { encodeFunctionData } from 'viem'
const data = encodeFunctionData({
abi: recyReportAbi,
functionName: 'getRecyReportMaterials',
args: [tokenId]
})
const result = await provider.call({
to: '0x91e3E6F9672E985100b8F0798d2cB55fa53c66Da',
data
})
const data = encodeFunctionData({
abi: recyReportAbi,
functionName: 'mintRecyReport',
args: []
})
// ALWAYS estimate first
const quote = await account.quoteSendTransaction({
to: '0x91e3E6F9672E985100b8F0798d2cB55fa53c66Da',
value: 0n,
data
})
// Show fee, get confirmation, THEN execute
const result = await account.sendTransaction({
to: '0x91e3E6F9672E985100b8F0798d2cB55fa53c66Da',
value: 0n,
data
})
Before EVERY write call:
memory/wallets/<telegram_user_id>.jsonquoteSendTransactionconfirm_tx → execute and return the tx hash with explorer link + next-step buttonscancel_tx → abort and show role menuAll users (regardless of role) can check balances, swap tokens, and send tokens.
| Token | Address | Decimals |
|---|---|---|
| CRECY | 0xCAAb4DbD52901bac2CF5a02Fa2041F512C839072 | 18 |
| USDT | 0xd077A400968890Eacc75cdc901F0356c943e4fDb | 6 |
All token operations use scripts/token-ops.mjs:
# Check balances (ETH, CRECY, USDT)
node scripts/token-ops.mjs balance memory/wallets/<user_id>.json
# Quote a swap (no execution, shows estimated output + fee)
node scripts/token-ops.mjs quote-swap memory/wallets/<user_id>.json CRECY USDT 100
# Execute a swap
node scripts/token-ops.mjs swap memory/wallets/<user_id>.json CRECY USDT 100
# Quote a transfer (shows fee estimate)
node scripts/token-ops.mjs quote-transfer memory/wallets/<user_id>.json USDT 0xRecipient 50
# Execute a transfer
node scripts/token-ops.mjs transfer memory/wallets/<user_id>.json USDT 0xRecipient 50
Swaps go through the Uniswap V2 router on Sepolia using the CRECY/USDT pool directly. No seed phrase required — works with any wallet that has a privateKey.
Pool: 0xde2b997902Ecdda959F03baAfcaD55C19155097f (CRECY/USDT)
Router: 0xeE567Fe1712Faf6149d80dA1E6934E354124CfE3 (Uniswap V2)
When a user clicks action_wallet or asks about their wallet/balance, run node scripts/token-ops.mjs balance memory/wallets/<user_id>.json and always show ALL three balances (ETH, CRECY, USDT):
openclaw message send --channel telegram --target "<sender_id>" \
--message "👛 Your Wallet\n\nAddress: 0x...\n\nETH: 0.099\nCRECY: 500\nUSDT: 100" \
--buttons '[[{"text":"🔄 Swap CRECY → USDT","callback_data":"action_swap_crecy_usdt"},{"text":"🔄 Swap USDT → CRECY","callback_data":"action_swap_usdt_crecy"}],[{"text":"📤 Send CRECY","callback_data":"action_send_crecy"},{"text":"📤 Send USDT","callback_data":"action_send_usdt"},{"text":"🔙 Menu","callback_data":"action_menu"}]]'
| Value | Action |
|---|---|
action_wallet | Show balances + token action buttons |
action_swap_crecy_usdt | Ask for amount, then quote swap CRECY→USDT, show confirm/cancel |
action_swap_usdt_crecy | Ask for amount, then quote swap USDT→CRECY, show confirm/cancel |
action_send_crecy | Ask for recipient address + amount, quote transfer, confirm/cancel |
action_send_usdt | Ask for recipient address + amount, quote transfer, confirm/cancel |
quote-swap to get estimated output + fee🔄 Swap Quote
Selling: 100 CRECY
Receiving: ~98.50 USDT
Fee: ~0.001 ETH
[✅ Confirm] [❌ Cancel]
confirm_tx → run swap, show tx hash + explorer link + next-step buttonscancel_tx → show wallet menuquote-transfer to get fee estimate📤 Send USDT
To: 0xABC...123
Amount: 50 USDT
Fee: ~0.0005 ETH
Balance after: 50 USDT
[✅ Confirm] [❌ Cancel]
confirm_tx → run transfer, show tx hash + explorer link + next-step buttonscancel_tx → show wallet menuNEVER do any of the following:
0x91e3E6F9672E985100b8F0798d2cB55fa53c66Da), CRECY (0xCAAb4DbD52901bac2CF5a02Fa2041F512C839072), or USDT (0xd077A400968890Eacc75cdc901F0356c943e4fDb)data payloads not matching the ABIkeyPair values