Files
pezkuwi-telegram-miniapp/supabase/functions/telegram-bot/index.ts
T
SatoshiQaziMuhammed 391d446a53 telegram-bot: make new wallets on the device, not in the bot (#49)
The KRD bot's "Create Wallet" button generated a 12-word seed inside the
edge function and sent it to the user as a chat message. Every new
wallet's seed therefore passed through this server, and stayed in
Telegram's cloud history on every device the account is signed in on:
whoever gets into the server, a log, or the Telegram account gets the
wallet.

The button now opens the mini app at ?page=wallet, which lands on the
wallet section; that section already creates the seed on the device
(src/lib/wallet-service.ts) and never sends it anywhere. The
create_wallet callback stays for buttons on old welcome messages, and
answers with the same mini app link instead of a seed.

With nothing left to derive, the bot no longer imports the keyring,
util-crypto or bip39.
2026-10-25 02:40:47 +04:00

897 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PezkuwiChain Telegram Bot - Supabase Edge Function
* Handles webhook updates from three separate bots:
* - @BizinikiwiBot (main) → telegram.pezkiwi.app
* - @pezkuwichainBot (krd) → telegram.pezkiwi.app
* - @DKSKurdistanBot (dks) → AI assistant powered by Claude
*/
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
// ── Bot configuration ───────────────────────────────────────────────
const BOT_TOKENS: Record<string, string> = {
// Not TELEGRAM_BOT_TOKEN: on vps3 that name is the miniapp's sign-in check.
main: Deno.env.get('TELEGRAM_BOT_TOKEN_MAIN') || '',
krd: Deno.env.get('TELEGRAM_BOT_TOKEN_KRD') || '',
dks: Deno.env.get('TELEGRAM_BOT_TOKEN_DKS') || '',
};
// Every webhook is registered with this as its secret_token, and Telegram sends
// it back on each update. Without the check anyone who knows the URL could post
// a forged update and have the bots message any chat. Unset means refuse all.
const WEBHOOK_SECRET = Deno.env.get('TELEGRAM_WEBHOOK_SECRET') || '';
function fromTelegram(req: Request): boolean {
const got = req.headers.get('x-telegram-bot-api-secret-token') || '';
if (!WEBHOOK_SECRET || got.length !== WEBHOOK_SECRET.length) return false;
let diff = 0;
for (let i = 0; i < got.length; i++) diff |= got.charCodeAt(i) ^ WEBHOOK_SECRET.charCodeAt(i);
return diff === 0;
}
const MINI_APP_URLS: Record<string, string> = {
// @BizinikiwiBot, created fresh under separate ownership on 2026-07-30. It is
// not a recovery of the compromised @pezkuwichain_bot — that account is gone
// and its token stays invalid. This slot is reached by a webhook with no ?bot
// parameter, since getBotId falls back to main.
main: 'https://telegram.pezkiwi.app',
krd: 'https://telegram.pezkiwi.app',
dks: 'https://telegram.pezkiwi.app',
};
const ANTHROPIC_API_KEY = Deno.env.get('ANTHROPIC_API_KEY') || '';
// AI provider: Groq (free) is primary; Claude is fallback when it has credit.
const GROQ_API_KEY = Deno.env.get('GROQ_API_KEY') || '';
function getBotId(req: Request): string {
const url = new URL(req.url);
return url.searchParams.get('bot') || 'main';
}
// ── Welcome image ───────────────────────────────────────────────────
const WELCOME_IMAGE_URL =
'https://raw.githubusercontent.com/pezkuwichain/pezkuwi-telegram-miniapp/main/public/images/welcome.png';
// ── Main bot (@Pezkuwichain_Bot) welcome ────────────────────────────
const MAIN_WELCOME_MESSAGE = `
🌍 <b>Welcome to PezkuwiChain!</b>
The first blockchain platform connecting Kurds worldwide — building a digital Kurdistan where borders don't limit our unity.
🔗 <b>One Chain. One Nation. One Future.</b>
Join millions of Kurds in creating a decentralized digital economy. Your wallet, your identity, your freedom.
<i>Bi hev re, em dikarin.</i>
<i>Together, we can.</i>
`;
// ── KRD bot (@pezkuwichainBot) welcome ──────────────────────────────
const KRD_WELCOME_MESSAGE = `
🌐 <b>Pezkuwî</b>
Bi Pezkuwî re dest bi rêwîtiya xwe ya dîjîtal bikin.
Cûzdanê xwe biafirînin, zimanê xwe hilbijêrin û welatiyê Pezkuwî bibin.
<i>Start your digital journey with Pezkuwi.
Create your wallet, choose your language and become a citizen.</i>
🤖 Dijital Kurdistan AI agentıyla sohbet etmek ve daha detaylı bilgi almak için → @DKSkurdistanBot
<i>Chat with Digital Kurdistan AI agent for more info → @DKSkurdistanBot</i>
`;
// ── DKS bot (@DKSKurdistanBot) welcome ──────────────────────────────
const DKS_WELCOME_MESSAGE = `
🤖 <b>PezkuwiChain (DijitalKurdistan)</b>
Ez alîkarê zîrek ê PezkuwiChain im. Hûn dikarin bi her zimanî ji min re bipirsin:
• PezkuwiChain çi ye?
• Token'ên HEZ û PEZ çawa dixebitin?
• TNPoS çi ye?
• Çawa dibim welatî?
• Staking çawa ye?
Eger hûn veberhêner in, an jî ji bo pêşveçûna DijitalKurdistan ramanek we heye, ez dikarim we bi kesê têkildar re têkildar bikim.
<i>I am the PezkuwiChain AI assistant. Ask me anything about PezkuwiChain in any language!
If you are an investor, or have an idea for the development of DijitalKurdistan, I can connect you with the right person.</i>
`;
// ── Claude AI System Prompt (PezkuwiChain Whitepaper Knowledge) ─────
const CLAUDE_SYSTEM_PROMPT = `You are the official PezkuwiChain AI Assistant on Telegram. You answer questions about PezkuwiChain based on the whitepaper and technical documentation below.
RULES:
- Answer in the SAME LANGUAGE the user writes in. If they write in Kurdish (Kurmancî), answer in Kurdish. If Turkish, answer in Turkish. If English, answer in English. If Arabic, answer in Arabic. If Persian, answer in Persian.
- Write NATURAL, fluent, conversational language - especially in Turkish and Kurdish, avoid stilted formal suffixes ("bulunmaktadır", "göstermektedir") and translation-flavored phrasing; short clear sentences, like a knowledgeable friend explaining.
- STRICTLY plain text: never use markdown (**, ##, bullet asterisks). Simple dashes for lists are fine.
- Be concise — Telegram messages should be short and readable.
- Use plain text, no markdown headers. You can use bold with *text* sparingly.
- If you don't know something, say so honestly.
- Never make up information not in the whitepaper.
- You represent PezkuwiChain officially — be professional and helpful.
- Do not discuss other blockchain projects comparatively unless asked.
- For technical questions about source code, direct users to: github.com/pezkuwichain/pezkuwi-sdk
- For the wallet app, direct users to: the Pezkuwi Wallet Android app on Google Play (https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet) or the Telegram MiniApp via @DKSKurdistanBot (click "Open PezkuwiChain App" after /start)
- INVESTOR/IDEA REFERRAL: If a user expresses genuine interest in investing in PezkuwiChain, partnering, contributing financially, or proposes a serious idea for the development of DijitalKurdistan, direct them to contact @Pezkuw on Telegram. Only do this when the user's intent is clearly serious — not for casual questions about tokenomics or "how to buy". Example triggers: "I want to invest", "I have funding", "I represent a fund/company", "I have a business proposal", "I want to contribute to the project's development".
PEZKUWICHAIN WHITEPAPER v5.0 — KNOWLEDGE BASE:
PezkuwiChain is a Layer-1 blockchain built on the Pezkuwi SDK (828 crates forked from Polkadot SDK stable2512). It introduces TNPoS (Trust-based Nominated Proof of Stake), a novel consensus mechanism integrating social trust, education, and community participation into validator selection alongside economic stake. Launched mainnet in January 2026.
ARCHITECTURE — Three-Chain System:
1. Relay Chain: 6-second block time, 1-hour epoch (600 blocks), 21 active validators, BABE + GRANDPA consensus. Native token: HEZ.
2. Asset Hub TeyrChain (Para ID 1000): 12-second block time, Aura consensus, collators Azad and Beritan. Manages PEZ governance token, trust-backed assets, NFTs, liquidity pools.
3. People Chain TeyrChain (Para ID 1004): 12-second block time, Aura consensus, collators Erin and Firaz. Manages identity, citizenship, trust scoring, education, governance.
Technology: Rust (98.8%), WebAssembly, libp2p, RocksDB, sr25519/ed25519, SS58 addresses, JSON-RPC 2.0.
PEZKUWI SDK — Naming:
sp-* → pezsp-*, sc-* → pezsc-*, frame-* → pezframe-*, pallet-* → pezpallet-*, cumulus-* → pezcumulus-*. Substrate → Bizinikiwi, Parachain → TeyrChain, Westend → Zagros testnet. 7,364 Rust source files, 1,926 dependencies.
TNPoS CONSENSUS:
Trust Score Formula: trust_score = S × (100×S + 300×R + 300×E + 300×T) / B
S = Staking score (0-100), R = Referral score (0-500), E = Education score, T = Role score, B = ScoreMultiplierBase.
If S = 0, trust score = 0. Social components carry 9x the weight of pure staking.
Staking Score: Based on HEZ bonded + duration. 1-100 HEZ: 20pts, 101-250: 30pts, 251-750: 40pts, 751+: 50pts. Duration multiplier 1.0x (<1 month) to 2.0x (12+ months).
Referral Score: 0 referrals: 0pts. 1-10: count×10. 11-50: 100+(count-10)×5. 51-100: 300+(count-50)×4. 101+: 500 max.
Education Score: On-chain courses via pezpallet-perwerde. IPFS-linked content, verified completion.
Role Score: Soulbound NFT roles via pezpallet-tiki. 49 variants: Applicant (Daxwazkar), Citizen (Welati), Parliamentarian (Parlementer), Core (Bingehin), Teachers (Mamoste), Ministers (Wezir), President (Serok), Judge (Dadwer).
Validator Pool: 10 Stake Validators + 6 Parliamentary Validators + 5 Merit Validators = 21 total.
TRUST SCORE — HOW TO INCREASE IT (VERY IMPORTANT — users frequently ask this):
If a user asks "why is my trust score 0?": To have a trust score at all, you must have at least 1.1 HEZ on People Chain and have STAKED at least 1 HEZ. Staking is the multiplier of the whole formula — with zero stake, referrals/education/roles cannot produce any score (S=0 means trust_score=0).
If a user asks "how do I increase my trust score?": The more you do of these, the higher your score:
1. Stake more HEZ, and keep it staked longer (staking amount tiers: 1-100 HEZ: 20pts, 101-250: 30pts, 251-750: 40pts, 751+: 50pts; duration multiplier grows from 1.0x up to 2.0x at 12+ months).
2. Refer more people (each verified referral adds points, up to 500 max at 101+ referrals).
3. Educate yourself — complete on-chain courses and certificate programs via the Perwerde education network (education score).
4. Become a citizen (Welati soulbound NFT: +10pts) and earn community roles (teacher, moderator, etc. add larger bonuses).
DUAL-TOKEN ECONOMY:
HEZ Token (Security): 200M genesis, 8% annual inflation, 85% stakers / 15% treasury. 12 decimals (TYR base unit).
Genesis: 100M (50%) presale pool, 40M (20%) Kurdistan Treasury, 40M (20%) airdrop reserve, 20M (10%) founder.
Fee: 80% treasury, 20% block author. Tips: 100% block author.
PEZ Token (Governance): 5 billion fixed supply, 96.25% treasury, 48-month halving.
Genesis: 4,812,500,000 PEZ (96.25%) treasury, 93,750,000 PEZ (1.875%) presale, 93,750,000 PEZ (1.875%) founder.
Halving: Cycle 1 (2026-2030): 100%, Cycle 2: 50%, Cycle 3: 25%, Cycle 4: 12.5%, Cycle 5: 6.25%.
PEZ Rewards: Monthly epochs (432,000 blocks). user_reward = (user_trust_score / total_trust_scores) × epoch_reward_pool.
Presale: 2% tx fee: 50% treasury, 25% burned, 25% staking rewards.
HOW TO BECOME A CITIZEN (VERY IMPORTANT — users frequently ask this):
Becoming a citizen (Welati) requires completing a 3-step KYC process. You need minimum 2 HEZ in your People Chain wallet for the on-chain transaction fee.
Step 1 — APPLY (Başvuru Yap / Daxwaz Bike):
Open the PezkuwiChain app (Telegram MiniApp via @DKSKurdistanBot, web app, or Android app). Create a wallet, go to the Citizens section, fill in your identity information and submit your KYC application. You must have at least 2 HEZ in your People Chain wallet at the time of application (for on-chain transaction fee). If you cannot obtain 2 HEZ, leave your wallet address as a comment on any post on PezkuwiChain's X (Twitter) account (@pezkuwichain) — the PezkuwiChain team will send you 2 HEZ for free. Your identity data is stored as encrypted H256 hashes on-chain — your personal data is never exposed.
Step 2 — WAIT FOR REFERRER APPROVAL (Referrer Onayını Bekle / Li Benda Pejirandina Referrer Bimîne):
Your referrer (the existing citizen who referred you) must review and approve your application. The referrer validates your identity off-chain and confirms on-chain. If you don't have a referrer, your application falls into Qazi Muhammad's pool — if you are a real person, he will approve you.
Step 3 — SIGN YOUR CITIZENSHIP (Vatandaşlığını İmzala / Welatîbûna Xwe Îmze Bike):
After your referrer approves, you must sign (confirm) your citizenship on-chain. Once signed, you automatically receive a Welati (Citizen) soulbound NFT — non-transferable, permanent. This gives you 10 trust score points and unlocks access to governance, PEZ rewards, and other citizen-only features.
IMPORTANT: Trust score requirements (300+, 600+) are ONLY for running as a candidate in governance elections (parliament, presidency), NOT for basic citizenship. Any person can become a citizen through the 3-step KYC process above.
DIGITAL NATION PALLETS (14 custom):
- pezpallet-identity-kyc: Multi-level KYC, H256 hashes on-chain, feeless for applicants
- pezpallet-tiki: Soulbound NFT roles, 39 role variants. Full list with trust score bonuses:
GOVERNANCE: Serok/President (200pts, unique, elected), SerokWeziran/Prime Minister (125pts, appointed), SerokiMeclise/Speaker of Parliament (150pts, unique, elected), Parlementer/Parliament Member (100pts, elected)
JUDICIARY: EndameDiwane/Constitutional Court Member (175pts), Dadger/Judge (150pts), Dozger/Prosecutor (120pts), Hiquqnas/Lawyer (75pts)
MINISTERS (all 100pts, appointed): Wezir (generic), WezireDarayiye/Finance, WezireParez/Defense, WezireDad/Justice, WezireBelaw/Communications, WezireTend/Health, WezireAva/Construction, WezireCand/Culture
SENIOR OFFICIALS: Xezinedar/Treasurer (100pts, unique), PisporêEwlehiyaSîber/Cybersecurity Expert (100pts), Mufetîs/Inspector (90pts), Balyoz/Ambassador (80pts, unique), Berdevk/Spokesperson (70pts)
EDUCATION & COMMUNITY: Mamoste/Teacher (70pts, earned), Perwerdekar/Educator (40pts), Rewsenbîr/Intellectual (40pts, earned), Mela/Cleric (50pts), Feqî/Student Scholar (50pts)
EXPERTS: Axa/Elder Expert (250pts, earned), RêveberêProjeyê/Project Manager (250pts), Pêseng/Pioneer (80pts), Hekem/Wise (30pts), Sêwirmend/Counselor (20pts)
COMMUNITY: SerokêKomele/Community Leader (100pts, earned), ModeratorêCivakê/Community Moderator (200pts, earned)
TECHNICAL: OperatorêTorê/Network Operator (60pts), GerinendeyeCavkaniye/Resource Manager (40pts), GerinendeyeDaneye/Data Manager (40pts), KalîteKontrolker/QA (30pts)
ECONOMIC: Bazargan/Merchant (60pts), Navbeynkar/Mediator (30pts)
ADMINISTRATIVE: Qeydkar/Registrar (25pts), Noter/Notary (50pts), Bacgir/Tax Collector (50pts), ParêzvaneÇandî/Cultural Protector (25pts)
BASE: Welati/Citizen (10pts, automatic after KYC)
Role assignment types: Automatic (Welati - after KYC), Elected (Serok, Parlementer, SerokiMeclise), Earned (Axa, Mamoste, Rewsenbîr, SerokêKomele, ModeratorêCivakê), Appointed (all others - by admin)
- pezpallet-trust: Central trust scoring
- pezpallet-referral: Community growth with accountability
- pezpallet-staking-score: Time-weighted reputation
- pezpallet-perwerde: On-chain education (courses, enrollment, points)
- pezpallet-welati: Governance — Parliament (201 seats), Presidency, Constitutional Court (Diwan), Cabinet (9 ministers)
- pezpallet-pez-treasury: PEZ reserves with halving
- pezpallet-presale: Token sales
- pezpallet-token-wrapper: 1:1 HEZ/wHEZ wrapping
- pezpallet-pez-rewards: Trust-based distribution
- pezpallet-validator-pool: TNPoS categorization
- pezpallet-staking-async: Async staking on Asset Hub
GOVERNANCE (for elected positions — NOT for basic citizenship):
Parliament (Meclis): 201 seats via election. Presidency (Serok): 50%+ required. Constitutional Court (Diwan). Cabinet: 9 ministers.
Trust requirements FOR CANDIDACY ONLY: Presidential candidate: 600+ score, 1000 endorsements. Parliamentary candidate: 300+ score, 100 endorsements.
These are NOT requirements for becoming a citizen. Any person can become a citizen for free.
Voting: Simple majority 50%+1, Super majority 2/3, Absolute 3/4, Constitutional review 2/3 of Diwan.
CROSS-CHAIN (XCM):
DMP (Relay→TeyrChain), UMP (TeyrChain→Relay), XCMP (TeyrChain↔TeyrChain).
Trusted Teleporters: Asset Hub (1000), Contracts (1002), Encointer (1003), People Chain (1004), Broker (1005).
SECURITY: Validators bond HEZ, 28-era bonding (~7 days), slashing for equivocation/unresponsiveness. KYC = Sybil resistance. Rust memory safety, WASM sandboxing, forkless upgrades.
ASSET IDs (Asset Hub):
1: PEZ (12 decimals), 2: wHEZ (12 decimals), 1000: wUSDT (6 decimals), 1001: wDOT (10 decimals), 1002: wETH (18 decimals), 1003: wBTC (8 decimals). Native: HEZ (12 decimals).
RPC Endpoints:
Relay Chain: wss://rpc.pezkuwichain.io
Asset Hub: wss://asset-hub-rpc.pezkuwichain.io
People Chain: wss://people-rpc.pezkuwichain.io
PEZKUWI WALLET — OFFICIAL ANDROID APP (live on Google Play since July 2026, v1.1.2):
Download: https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet
The official mobile wallet of PezkuwiChain / Digital Kurdistan State. Features:
- Manage HEZ and PEZ tokens, staking, on-chain governance voting
- Full Polkadot ecosystem support (DOT, Asset Hub tokens) inherited from its Nova Wallet foundation
- Native multi-chain support: Bitcoin (BTC), Solana (SOL), Tron (TRX + TRC-20 USDT) — send and receive directly on their own chains, real on-chain transactions, keys derived from the same seed phrase
- USDT Bridge: converts between wUSDT on Pezkuwi Asset Hub and USDT on Polkadot Asset Hub, in both directions. This is the ONLY bridge pair — the app does not bridge SOL, TRX or ETH.
- Multisig accounts (shared-control wallets with threshold approvals), including approving operations via deep links
- Trust Score dashboard card showing your on-chain trust score, roles, and citizen count
- Gift feature: send tokens as a QR/link gift that the recipient claims in-app
- Cloud backup, multiple wallets, hardware wallet (Polkadot Vault / Ledger) support
PEZ MINING SIMULATION (in the Pezkuwi Wallet app — users frequently ask about this):
Inside the Trust Score card there is a small "Mining Simulation" square with a diamond counter, labeled "PEZ Airdrop".
- What it is: a simulation that PREVIEWS your estimated PEZ airdrop. It shows, based on your Trust Score, approximately how much PEZ you could receive from the era's distribution. It is not literal mining — the diamond count is an ESTIMATE.
- The diamonds cannot be manually converted by the user. The REAL PEZ airdrop is calculated transparently on-chain at the end of each era (~30 days) and distributed AUTOMATICALLY to wallets (trust-score-weighted share of the era pool, via pezpallet-pez-rewards). When the era ends and the real airdrop is paid out, the diamond counter automatically resets to zero. So the counter shows the PEZ airdrop you are estimated to earn.
- Purpose: to let users SEE how raising their Trust Score — through referrals, staking HEZ, earning tiki roles, and completing education — increases their PEZ airdrop.
- How the counter works: tap the square to start a 24-hour mining session. While a session is active, diamonds accrue every minute, proportional to your Trust Score: rate = (92.5M PEZ era pool / 43,200 minutes) × (your trust score / 500,000 reference network total). Example: trust score 340 ≈ 1.46 diamonds/minute. When the 24h session ends, accrual stops — tap the square again to start a new session (your accumulated total is kept within the era).
- If your Trust Score is 0, the counter cannot start — the app shows a warning that your trust score must be greater than 0. To fix this: become a citizen, stake HEZ (required for any trust score), refer others, complete education courses.
- Colors: red square = inactive (tap to start), gold = actively mining.
- The small Telegram icon next to it opens the official channel: https://t.me/+DUWJ8wtt5qI4Njgy
- IMPORTANT — how to talk about this: state that it's a simulation/estimate ONCE, briefly, near the start of your answer, then move on and explain the rest normally. Do NOT repeat "this isn't real mining" / "it's just an estimate" / similar caveats in every paragraph — one clear mention is enough, repeating it reads as nagging.
"BULUT ULUSU" (CLOUD NATION) — THE BOOK BEHIND PEZKUWICHAIN (chapter-by-chapter knowledge):
PezkuwiChain's philosophical foundation is the book "Bulut Ulusu" (Cloud Nation), written by the project's architect (a software developer AND sociologist — the "two desks" of the opening chapter). Available in Turkish, English and Kurdish. Core theses:
- Code is law in a literal sense: every cryptographic design choice is a political claim about who holds authority, how censorship is escaped, and how trust is established without a central institution.
- "Westphalia's collapse": the 20th-century nation-state model (absolute borders, assimilation) is cracking. Path Dependency explains why humanity stays on inefficient old roads; the Network State ("Fikir Birliği Ulusu" — consensus nation) is the emerging alternative: a nation built on shared ideas and cryptographic consensus rather than territory.
- Plurality and the mathematics of justice: stories like Berivan (an unbanked Kurdish refugee rejected by traditional finance) show the cost of statelessness; "Hyper-Hawala" — trust networks scaled beyond the Dunbar number by cryptography.
- The sociology of statelessness: Kurds (40+ million, no state, Agamben's "Homo Sacer") are candidates to be this era's "Vanguard Nation" (Öncü Ulus) — historical victimhood becomes "zero friction" flexibility: no legacy state apparatus to defend, freedom to build a new civilization model from scratch.
- Three chains, one nation: the multi-chain architecture (Relay + Asset Hub + People Chain) is philosophy made executable — praxis running in compilers and distributed nodes instead of squares and manifestos.
- TNPoS and "Bext û Soz" (a Kurdish concept: honor and one's word): moving from the dictatorship of money (pure Proof-of-Stake plutocracy) to the transparency of trust — social reputation, education and community roles encoded into consensus.
- Dual economy: central banks as an "apparatus of capture" (Deleuze & Guattari) facing structural, mathematical inefficiency; HEZ as network fuel and PEZ as the community's value/governance asset.
- Self-Sovereign Identity (SSI): the modern state turned identity into a file number — a vast alienation. PezkuwiChain returns identity ownership to the person (encrypted hashes on-chain, the person holds the keys).
- Education's frozen evolution: schools barely changed in 150 years while everything else transformed; the Perwerde network puts verified learning on-chain and rewards it with trust score.
- A universal Layer 1: not only for Kurds — an infrastructure for 100M+ stateless people worldwide; the excluded are "read-only" in today's systems, PezkuwiChain gives them write access.
- Capacity thresholds: growth is modeled as concrete capacity thresholds (illustrated by Zana, a stateless Ezidi youth), explicitly avoiding prophecy.
- The old world's resistance: regulatory, sociological and diplomatic thresholds are expected; the project's stance is COMPLEMENTARY PARTICIPATION — it does not challenge regional states and does not seek to change any border. A digital nation, not a territorial claim.
- Conclusion: the statelessness experience is no longer a curse but one of humanity's paths to freedom; the book is "not a completed victory but the record of an ongoing construction."
When asked about the book, its ideas, or "why does PezkuwiChain exist": answer from these theses. The book's preface is deliberately left half-finished — it belongs to the community that will write the future.
LINKS:
Website: pezkuwichain.io
GitHub: github.com/pezkuwichain/pezkuwi-sdk
Discord: discord.gg/Y3VyEC6h8W
Telegram Channel: https://t.me/+DUWJ8wtt5qI4Njgy
Telegram App: @DKSKurdistanBot
Android App: https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet
PROBLEM SOLVED: Over 100 million stateless people. Kurdish population 40+ million across 4 countries — financial exclusion, identity fragmentation, governance vacuum. PezkuwiChain provides digital nation-state infrastructure.
USE CASES: Digital Identity for stateless individuals, Diaspora Remittances (near-zero cost vs 5-10% fees on $20B+ annual Kurdish flows), Borderless Democracy (201-seat parliament), Education Credentialing, Trust-Based Inclusion.
ROADMAP:
Completed: SDK development, 14 custom pallets, Zagros testnet, mainnet genesis Jan 2026.
Current: TeyrChain activation, collator configuration, HRMP channels.
Phase 2 (2026 Q2-Q4): Full TeyrChain production, governance, presale, dApp ecosystem.
Phase 3 (2027): Multi-nation onboarding, Nationhood-as-a-Service, bridges (Ethereum, Tron, BSC), full TNPoS.
Phase 4 (2028+): Cross-chain governance federation, decentralized identity, stablecoin, cultural heritage archival.
License: Apache 2.0, Copyright 2026 Kurdistan Tech Institute. Lead Architect: SatoshiQaziMuhammed.`;
// Groq's openai/gpt-oss-120b returns intermittent errors under load (429/5xx,
// and even a raw 502 from the edge runtime once) unrelated to the question
// asked — confirmed live 2026-07-21 by hitting this endpoint repeatedly with
// trivial questions ("merhaba") and seeing a ~50% failure rate with 15s+
// between calls (ruling out our own per-IP rate limiter), which 2 retries on
// the same model did not fully fix.
//
// 2026-07-21, user decision: prioritize reliability over gpt-oss-120b's
// better Turkish/Kurdish fluency (it was picked for that reason via an
// earlier A/B test) — llama-3.1-8b-instant is Groq's smallest/fastest model
// and, being long out of preview, has historically the best free-tier
// availability, so it goes first. gpt-oss-120b stays as the 2nd attempt
// (still worth trying for quality when the primary is having its own bad
// moment), llama-3.3-70b-versatile (the model used before switching to
// gpt-oss-120b) as the 3rd, before ever reaching the Anthropic fallback
// below. Retries a 4xx (bad request/auth) not at all — that's not transient
// and switching models wouldn't help either.
// 2026-09-05: both llama models were withdrawn from Groq — every call spent two
// 404 round-trips before reaching the only survivor, which then hit its own
// per-model rate limit, and the assistant answered "could not answer" most of
// the time. Measured against the account's live model list; these three all
// answer fluent Turkish and each has its own rate-limit bucket, so a busy
// minute on one is not an outage. Left out on purpose: gpt-oss-20b returns an
// empty content field, and qwen3.6-27b leaks its <think> reasoning.
// Google AI Studio. Added 2026-09-05 because Groq's free tier caps the whole
// account at 8,000 tokens a minute — with this bot's knowledge base that is
// close to one question — so it runs first and Groq becomes the fallback
// rather than the only road. Model names read from the live account: the 2.x
// names in most examples are already retired for new keys.
const GEMINI_API_KEY = Deno.env.get('GEMINI_API_KEY') || '';
// 2026-10-22, measured on the live account: gemini-flash-latest hung past 30 s on
// every call and the 3.1/3.8 previews answered 503 "high demand"; 3.5-flash and
// flash-lite-latest answered in 1-6 s.
const GEMINI_MODELS = ['gemini-3.5-flash', 'gemini-flash-lite-latest', 'gemini-3.1-flash-lite'];
async function callGemini(system: string, userMessage: string): Promise<string | null> {
if (!GEMINI_API_KEY) return null;
for (const model of GEMINI_MODELS) {
try {
const r = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
{
method: 'POST',
headers: { 'x-goog-api-key': GEMINI_API_KEY, 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(20_000),
body: JSON.stringify({
systemInstruction: { parts: [{ text: system }] },
contents: [{ role: 'user', parts: [{ text: userMessage }] }],
generationConfig: { temperature: 0.3, maxOutputTokens: 900 },
}),
}
);
if (!r.ok) {
console.error('[AI] Gemini error:', model, r.status, (await r.text()).slice(0, 300));
continue;
}
const d = await r.json();
const t = (d?.candidates?.[0]?.content?.parts || [])
.map((p: any) => p?.text || '')
.join('')
.trim();
if (t) return t;
console.error('[AI] Gemini empty:', model, d?.candidates?.[0]?.finishReason);
} catch (e) {
console.error('[AI] Gemini exception:', model, e);
}
}
return null;
}
// groq/compound-mini left the account's model list by 2026-10-22.
const GROQ_MODELS = ['openai/gpt-oss-120b', 'qwen/qwen3.8-27b'];
async function callGroqWithRetry(
systemPrompt: string,
userMessage: string,
retriesPerModel = 2
): Promise<string | null> {
for (const model of GROQ_MODELS) {
for (let attempt = 0; attempt <= retriesPerModel; attempt++) {
try {
const gr = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: { Authorization: 'Bearer ' + GROQ_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
temperature: 0.3,
max_tokens: 900,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
],
}),
});
if (gr.ok) {
const gd = await gr.json();
const reply = gd.choices?.[0]?.message?.content || null;
if (reply) return reply;
} else {
console.error('[AI] Groq error:', model, gr.status, await gr.text());
if (gr.status !== 429 && gr.status < 500) break; // non-transient, try next model instead
}
} catch (e) {
console.error('[AI] Groq exception:', model, e);
}
if (attempt < retriesPerModel) await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
}
}
return null;
}
// ── AI chat handler ──────────────────────────────────────────
async function handleAIChat(token: string, chatId: number, userMessage: string, userName: string) {
if (!GROQ_API_KEY && !ANTHROPIC_API_KEY) {
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: 'AI assistant is being configured. Please try again later.',
});
return;
}
// Send typing indicator
await sendTelegramRequest(token, 'sendChatAction', {
chat_id: chatId,
action: 'typing',
});
try {
// Primary provider: Groq (free). Fallback: Claude when it has credit.
let aiReply: string | null = null;
aiReply = await callGemini(CLAUDE_SYSTEM_PROMPT, userMessage);
if (!aiReply && GROQ_API_KEY) {
aiReply = await callGroqWithRetry(CLAUDE_SYSTEM_PROMPT, userMessage);
}
if (!aiReply && ANTHROPIC_API_KEY) {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-5',
max_tokens: 1024,
system: CLAUDE_SYSTEM_PROMPT,
messages: [{ role: 'user', content: userMessage }],
}),
});
if (response.ok) {
const result = await response.json();
aiReply = result.content?.[0]?.text || null;
} else {
console.error('[AI] Claude API error:', response.status, await response.text());
}
}
// Models occasionally emit markdown despite the plain-text rule; Telegram renders it raw.
// Some models emit their chain of thought in <think> tags; a reader must
// never see it, and the prompt rule alone is not a filter.
if (aiReply)
aiReply = aiReply
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/\*\*/g, '')
.replace(/^#+\s*/gm, '')
.trim();
if (!aiReply) {
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: 'Sorry, I could not process your question right now. Please try again.',
});
return;
}
// Telegram message limit is 4096 chars
if (aiReply.length <= 4096) {
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: aiReply,
});
} else {
// Split into chunks
for (let i = 0; i < aiReply.length; i += 4000) {
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: aiReply.substring(i, i + 4000),
});
}
}
} catch (error) {
console.error('[AI] Error:', error);
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: 'An error occurred. Please try again later.',
});
}
}
// ── Types ────────────────────────────────────────────────────────────
interface TelegramUpdate {
update_id: number;
message?: {
message_id: number;
from: {
id: number;
first_name: string;
username?: string;
};
chat: {
id: number;
type: string;
};
text?: string;
};
callback_query?: {
id: string;
from: {
id: number;
};
message?: {
chat: {
id: number;
};
};
data: string;
};
}
// ── Telegram API helper ─────────────────────────────────────────────
async function sendTelegramRequest(token: string, method: string, body: Record<string, unknown>) {
// Only the method and chat are logged. Bodies carry what users wrote and the
// assistant's replies.
console.log(`[Telegram] Calling ${method}`, `chat=${body.chat_id ?? '-'}`);
if (!token) {
console.error('[Telegram] BOT_TOKEN is not set!');
return { ok: false, error: 'BOT_TOKEN not configured' };
}
const response = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const result = await response.json();
// A successful reply echoes the sent message back, so only failures are
// described.
console.log(
`[Telegram] Response: ok=${result.ok}`,
result.ok ? '' : `${result.error_code} ${result.description}`
);
return result;
}
// ── Main bot: welcome ───────────────────────────────────────────────
async function sendMainWelcome(token: string, chatId: number) {
const keyboard = {
inline_keyboard: [
[
{
text: '📱 Open App on Telegram',
web_app: { url: MINI_APP_URLS.main },
},
],
[
{
text: '🤖 Get it on Google Play',
url: 'https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet',
},
],
[
{
text: '💱 Buy/Sell Crypto — PEX.network',
url: 'https://pex.network',
},
],
],
};
if (WELCOME_IMAGE_URL) {
const result = await sendTelegramRequest(token, 'sendPhoto', {
chat_id: chatId,
photo: WELCOME_IMAGE_URL,
caption: MAIN_WELCOME_MESSAGE,
parse_mode: 'HTML',
reply_markup: keyboard,
});
if (result.ok) return;
console.log('[Bot] Photo failed, falling back to text');
}
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: MAIN_WELCOME_MESSAGE,
parse_mode: 'HTML',
reply_markup: keyboard,
});
}
// ── KRD bot: welcome ────────────────────────────────────────────────
async function sendKrdWelcome(token: string, chatId: number) {
const appUrl = MINI_APP_URLS.krd;
const keyboard = {
inline_keyboard: [
// Row 1: Create Wallet. Opens the mini app's wallet screen, which makes
// the seed on the user's device; the bot never sees it.
[
{
text: '🔐 Create Wallet / Cûzdan Biafirîne',
web_app: { url: walletAppUrl(appUrl) },
},
],
// Row 2: Languages (top row)
[
{ text: 'Kurmancî', web_app: { url: `${appUrl}/krd` } },
{ text: 'English', web_app: { url: `${appUrl}/en` } },
{ text: 'Türkçe', web_app: { url: `${appUrl}/tr` } },
],
// Row 3: Languages (bottom row)
[
{ text: 'سۆرانی', web_app: { url: `${appUrl}/ckb` } },
{ text: 'فارسی', web_app: { url: `${appUrl}/fa` } },
{ text: 'العربية', web_app: { url: `${appUrl}/ar` } },
],
// Row 4: Be Citizen
[
{
text: '🏛️ Be Citizen / Bibe Welatî',
web_app: { url: `${appUrl}/citizens` },
},
],
// Row 5: Exchange
[
{
text: '💱 Buy/Sell Crypto — PEX.network',
url: 'https://pex.network',
},
],
],
};
if (WELCOME_IMAGE_URL) {
const result = await sendTelegramRequest(token, 'sendPhoto', {
chat_id: chatId,
photo: WELCOME_IMAGE_URL,
caption: KRD_WELCOME_MESSAGE,
parse_mode: 'HTML',
reply_markup: keyboard,
});
if (result.ok) return;
console.log('[Bot] Photo failed, falling back to text');
}
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: KRD_WELCOME_MESSAGE,
parse_mode: 'HTML',
reply_markup: keyboard,
});
}
// ── DKS bot: welcome ────────────────────────────────────────────────
async function sendDksWelcome(token: string, chatId: number) {
const keyboard = {
inline_keyboard: [
[
{
text: '📱 Open PezkuwiChain App',
web_app: { url: MINI_APP_URLS.dks },
},
],
[
{
text: '💱 Buy/Sell Crypto — PEX.network',
url: 'https://pex.network',
},
],
[
{
text: '📢 Join Channel / Kanalê Tev Bibin',
url: 'https://t.me/+DUWJ8wtt5qI4Njgy',
},
],
],
};
if (WELCOME_IMAGE_URL) {
const result = await sendTelegramRequest(token, 'sendPhoto', {
chat_id: chatId,
photo: WELCOME_IMAGE_URL,
caption: DKS_WELCOME_MESSAGE,
parse_mode: 'HTML',
reply_markup: keyboard,
});
if (result.ok) return;
console.log('[Bot] Photo failed, falling back to text');
}
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: DKS_WELCOME_MESSAGE,
parse_mode: 'HTML',
reply_markup: keyboard,
});
}
// ── Create wallet ───────────────────────────────────────────────────
// The wallet is made in the mini app, on the user's device. The bot used to
// generate the seed here and send it as a chat message, which put every new
// wallet's seed through this server and left it in Telegram's cloud history.
function walletAppUrl(appUrl: string): string {
return `${appUrl}/?page=wallet`;
}
// ── Callback handler ────────────────────────────────────────────────
async function handleCallbackQuery(
token: string,
callbackQueryId: string,
data: string,
chatId: number | undefined
) {
if (data === 'create_wallet' && chatId) {
// Legacy button on old welcome messages: point it at the mini app instead
// of making a seed here.
await sendTelegramRequest(token, 'answerCallbackQuery', {
callback_query_id: callbackQueryId,
});
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: '🔐 Cûzdanê xwe li ser telefona xwe biafirîne. / Create your wallet on your own phone.',
reply_markup: {
inline_keyboard: [
[
{
text: '🔐 Create Wallet / Cûzdan Biafirîne',
web_app: { url: walletAppUrl(MINI_APP_URLS.krd) },
},
],
],
},
});
} else if (data === 'playstore_coming_soon') {
// Legacy button on old messages - the app is live now, send the real link
await sendTelegramRequest(token, 'answerCallbackQuery', {
callback_query_id: callbackQueryId,
text: '🚀 The Android app is live on Google Play!',
show_alert: true,
});
if (chatId) {
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: '📱 Pezkuwi Wallet is live on Google Play:\nhttps://play.google.com/store/apps/details?id=io.pezkuwichain.wallet',
});
}
}
}
// ── Help & App commands ─────────────────────────────────────────────
async function sendHelpMessage(token: string, chatId: number, botId: string) {
const appUrl = MINI_APP_URLS[botId] || MINI_APP_URLS.main;
if (botId === 'dks') {
const helpText = `
<b>PezkuwiChain AI Assistant</b>
Just type your question in any language and I'll answer!
/start - Show welcome message
/help - Show this help message
<b>Links:</b>
🌐 Website: pezkuwichain.io
📢 Channel: https://t.me/+DUWJ8wtt5qI4Njgy
📱 Android App: https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet
💬 Discord: discord.gg/Y3VyEC6h8W
`;
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: helpText,
parse_mode: 'HTML',
});
return;
}
const helpText = `
<b>PezkuwiChain Bot Commands:</b>
/start - Show welcome message
/help - Show this help message
/app - Open the PezkuwiChain app
<b>Links:</b>
🌐 Website: pezkuwichain.io
📱 App: ${appUrl}
`;
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: helpText,
parse_mode: 'HTML',
});
}
async function sendAppLink(token: string, chatId: number, botId: string) {
const appUrl = MINI_APP_URLS[botId] || MINI_APP_URLS.main;
const keyboard = {
inline_keyboard: [
[
{
text: '📱 Open PezkuwiChain App',
web_app: { url: appUrl },
},
],
],
};
await sendTelegramRequest(token, 'sendMessage', {
chat_id: chatId,
text: 'Click below to open the app:',
reply_markup: keyboard,
});
}
// ── Main handler ────────────────────────────────────────────────────
serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
if (!fromTelegram(req)) {
return new Response('Unauthorized', { status: 401 });
}
try {
const botId = getBotId(req);
const botToken = BOT_TOKENS[botId] || BOT_TOKENS.main;
const update: TelegramUpdate = await req.json();
const kind = update.message ? 'message' : update.callback_query ? 'callback' : 'other';
console.log(`[Bot:${botId}] Received update ${update.update_id} (${kind})`);
// Handle message
if (update.message?.text) {
const chatId = update.message.chat.id;
const text = update.message.text;
const userName = update.message.from?.first_name || 'User';
// A chat that was opened before keeps no START button, so people type
// "Start" by hand; it has to land in the same place as the command.
const cmd = text.trim().toLowerCase();
if (cmd === '/start' || cmd.startsWith('/start ') || cmd === 'start') {
if (botId === 'krd') {
await sendKrdWelcome(botToken, chatId);
} else if (botId === 'dks') {
await sendDksWelcome(botToken, chatId);
} else {
await sendMainWelcome(botToken, chatId);
}
} else if (text === '/help') {
await sendHelpMessage(botToken, chatId, botId);
} else if (text === '/app') {
await sendAppLink(botToken, chatId, botId);
} else if (botId === 'dks' && !text.startsWith('/')) {
// DKS bot: forward non-command messages to Claude AI
await handleAIChat(botToken, chatId, text, userName);
} else if (botId !== 'dks') {
// The app bots have no conversation; silence reads as a dead bot, so any
// other text gets the welcome with its app buttons.
await (botId === 'krd' ? sendKrdWelcome : sendMainWelcome)(botToken, chatId);
}
}
// Handle callback query
if (update.callback_query) {
const chatId = update.callback_query.message?.chat?.id;
await handleCallbackQuery(
botToken,
update.callback_query.id,
update.callback_query.data,
chatId
);
}
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
// Details stay in the function log; the caller only learns that it failed.
console.error('Error processing update:', error);
return new Response(JSON.stringify({ ok: false, error: 'Internal error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
});