Multi-currency crypto payment bot for Telegram with Privy wallet integration.
- Multi-currency support: USDT, USDC, ETH, BTC, TON, BNB, SOL, TRX, LTC
- Invoice creation: Create payment requests with any supported asset
- Direct transfers: Send crypto to users via Telegram
- Crypto checks: Create vouchers/gift cards that can be claimed
- Non-custodial: Users connect their own Privy wallets
- Create and manage party rooms on dial.wtf
- Search parties by name, room code, or address
- List all active party rooms
/invoice <amount> <asset> [description]- Create crypto invoice/send <user> <amount> <asset> [comment]- Send crypto/check <amount> <asset> [pin_to_user]- Create voucher/balance- View wallet balance/startparty [wallet]- Create party room/listparty- List parties/findparty <keyword>- Search parties
Dial-themed Telegram Mini App to create Request Network invoices with a Cash-App style UI. Runs inside Telegram’s webview, validates Telegram initData server-side, optionally uses Privy embedded wallets as payee, creates a Request invoice, and exposes a /pay/[id] page with QR + live status polling.
- Next.js App Router (Node runtime for API routes)
- Telegram SDK:
@twa-dev/sdk(dynamic import in client only) - Privy:
@privy-io/react-auth(embedded wallets) - Request Network:
@requestnetwork/request-client.js
Copy .env.example → .env.local and fill in values:
PUBLIC_BASE_URL= # e.g. https://your-tunnel.ngrok.app (dev) or prod domain
REQUEST_NODE_URL=https://main.gateway.request.network
REQUEST_CHAIN=polygon # e.g. polygon, base, etc.
ERC20_TOKEN_ADDRESS= # optional; if empty uses USDC symbol
BOT_TOKEN= # BotFather token for initData validation
ALLOW_UNVERIFIED_INITDATA=1 # dev bypass; omit in prod
PAYEE_ADDR= # your org/payee EVM address
FEE_ADDR= # optional; defaults to PAYEE_ADDR
NEXT_PUBLIC_PRIVY_APP_ID= # Privy App ID
NEXT_PUBLIC_ONRAMP=COINBASE # or MOONPAY
NEXT_PUBLIC_COINBASE_APP_ID= # Coinbase Onramp App ID (NOT API key)
COINBASE_DEFAULT_ASSET=USDC
COINBASE_DEFAULT_FIAT=USD
COINBASE_DEFAULT_FIAT_AMOUNT=20
# If your Coinbase Onramp app requires a session token, set this (temporary dev only):
# COINBASE_SESSION_TOKEN= # server-generated session token per Coinbase docs
NEXT_PUBLIC_MOONPAY_KEY= # MoonPay public key (if using MOONPAY)
MOONPAY_SECRET_KEY= # MoonPay secret for URL signature
MOONPAY_DEFAULT_CURRENCY_CODE=usdc
MOONPAY_DEFAULT_BASE_CURRENCY=usd
- Install deps and run dev server
pnpm install
pnpm dev- Start a tunnel and set
PUBLIC_BASE_URLto the HTTPS URL
npx ngrok http 3000
# or cloudflared tunnel --url http://localhost:3000- Set Telegram bot Web App URL to
PUBLIC_BASE_URLwith BotFather.
-
/api/invoice(POST)- Validates
initDataunlessALLOW_UNVERIFIED_INITDATA=1in non-prod - Creates Request invoice with ERC20 Fee Proxy on the configured chain/token
- Responds
{ requestId, payUrl }wherepayUrl = PUBLIC_BASE_URL + /pay/[id]
- Validates
-
/api/status(GET)?id=<requestId>→ rehydrates the request and returns{ status: 'pending'|'paid', balance }
-
/api/onramp/coinbase(GET)- Redirects to Coinbase Hosted Onramp:
https://pay.coinbase.com/buy - Params:
appId, optionaladdresses=[{address,blockchains:['base']}],amount,fiatCurrency, optionalassets(USDC/ETH/...) - If your Onramp app enforces secure initialization, a
sessionTokenis required. ProvideCOINBASE_SESSION_TOKEN(dev) or implement a server endpoint to mint tokens per Coinbase security requirements.
- Redirects to Coinbase Hosted Onramp:
-
/api/onramp/moonpay(GET)- Redirects to MoonPay
https://buy.moonpay.comwith a signed query - Requires
NEXT_PUBLIC_MOONPAY_KEYandMOONPAY_SECRET_KEY
- Redirects to MoonPay
All API routes export runtime = 'nodejs' to avoid Edge limitations.
app/page.tsxdynamically imports@twa-dev/sdkinuseEffect- Privy
ensureWallet()prompts login if not authenticated, then readswallets[0].address - POST to
/api/invoicewith{ kind, amount:Number, note, initData, payee } - On success, opens
payUrlinside Telegram webview
Funding (Add funds)
- Tries Privy
fundWalletwithcard.preferredProviderfromNEXT_PUBLIC_ONRAMP - If the widget fails in Telegram IAB, we auto-fallback to an iframe overlay and an “Open in browser” button
- Known limitation: many onramps restrict flows inside Telegram’s webview; “Open in browser” is the reliable path
curl -X POST "$PUBLIC_BASE_URL/api/invoice" \
-H "Content-Type: application/json" \
-d '{"amount":1.23,"note":"Test","kind":"request","initData":"","payee":"0x..."}'
curl "$PUBLIC_BASE_URL/api/status?id=<requestId>"- Set the same env vars (omit
ALLOW_UNVERIFIED_INITDATA) - Update BotFather Web App URL to the prod domain
- Test from phone;
/api/invoicemust validate TelegraminitData
- If you see a Coinbase page saying "Missing or invalid parameters: requires sessionToken" your Onramp app is configured to require secure initialization.
- For dev:
- Option A: Temporarily disable session token requirement in the Coinbase portal
- Option B: Provide
COINBASE_SESSION_TOKEN(server-generated) and restart
- For prod:
- Implement a backend endpoint to mint session tokens after authenticating the user (see Coinbase docs) and pass it through the
/api/onramp/coinbaseredirect.
- Implement a backend endpoint to mint session tokens after authenticating the user (see Coinbase docs) and pass it through the
Many providers block unknown origins and IAB contexts. Any time your ngrok URL changes, update all allowlists:
- Privy (Allowed origins)
- Coinbase Onramp (Allowed origins / app settings)
- MoonPay (Allowed origins)
- Google OAuth (Authorized JavaScript origins and redirect URIs)
- Telegram (Bot Web App URL)
Recommended: Always use HTTPS ngrok domain (e.g., https://dial.ngrok.app). If the subdomain rotates, revisit each dashboard and re-add it.
All bots are owned by Adam. Current linkage:
- Dial Betabot →
https://dev.tgbot.dial.wtf(development) - Alpha Dialbot →
https://dial.ngrok.app(local dev tunnel) - Dial Official Bot →
https://tgbot.dial.wtf(production) - Dial WTF Bot →
https://tgbot.dial.wtf(production)
Each bot has its own token and must be configured with the correct Web App URL and webhook.
Use scripts/sync-env-and-webhook.sh to push env vars to Vercel and set the webhook for the current .env.local:
scripts/sync-env-and-webhook.sh development .env.local
# or preview/productionEnv must include:
PUBLIC_BASE_URL(matches the bot’s Web App URL)BOT_TOKEN(for the specific bot you’re linking)
This script sets the webhook to ${PUBLIC_BASE_URL}/api/bot for the provided BOT_TOKEN.
Each bot must have its own webhook and (optionally) commands and inline mode.
Per-bot env file (example .env.alpha):
PUBLIC_BASE_URL=https://dial.ngrok.app
BOT_TOKEN=123456:ABCDEF
Set webhook and verify:
scripts/set-bot-webhook.sh "$BOT_TOKEN" "$PUBLIC_BASE_URL"
# or
scripts/sync-env-and-webhook.sh development .env.alphaSet commands (private chats or group scope):
scripts/set-bot-commands.sh "$BOT_TOKEN" all_private_chats
scripts/set-bot-commands.sh "$BOT_TOKEN" all_group_chatsEnable inline mode (BotFather): see scripts/enable-inline-mode.md.
Notes:
- Any time your domain/tunnel changes, re-run the webhook script for each bot.
- Group privacy mode affects non-command messages; keep it ON and use slash commands, or OFF if you want the bot to read all messages.
We use @telegram-apps/mate to upload static assets to Telegram Mini Apps CDN.
Install & usage (via pnpm dlx):
# Print CDN base path for a project name
pnpm dlx @telegram-apps/mate@latest deploy info --project dial-telegram-mini
# Upload a directory (e.g., public/) to CDN under the project name
pnpm dlx @telegram-apps/mate@latest deploy upload --project dial-telegram-mini --dir ./publicNotes:
- The base path from
deploy infois used by Telegram as the root for uploaded files for the given project. - Re-run
uploadwhen you change static files used by your Mini App (icons, images, etc.). - You can parameterize the project name per environment, e.g.,
dial-telegram-mini-devvsdial-telegram-mini-prod.