> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Telegram wallet bot

Build a Telegram bot where every user gets their own Openfort backend wallet, created on the server the first time they message the bot. Users check balances and send USDC on Base Sepolia from the chat, with no app install, no seed phrase and no ETH for gas.

:::tip
You'll build a Node.js bot with [grammY](https://grammy.dev) and `@openfort/openfort-node` that maps each Telegram user to a backend wallet and sends sponsored USDC transfers.
:::

[View Sample Code](https://github.com/openfort-xyz/recipes-hub/tree/main/telegram-bot) — GitHub Repository. Complete source code for the Telegram wallet bot.

## Build it with your agent

Adding this to an app you already have? Set up your coding agent with the [Openfort docs MCP server and skill](https://www.openfort.io/docs/overview/building-with-ai), then give it this prompt:

```text
Add a backend wallet per chat user with sponsored USDC sends to this app using Openfort. Follow the "Add this to your app" section of
https://github.com/openfort-xyz/recipes-hub/blob/main/telegram-bot/AGENTS.md
```

The same file lists every Openfort primitive the recipe uses and the errors you're likely to hit, with their fixes.

## Getting started

:::steps
### Set up your project

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/telegram-bot openfort-telegram-bot
cd openfort-telegram-bot
pnpm install
```

### Get your Openfort credentials

1. On the **API keys** page of the [dashboard](https://dashboard.openfort.io), copy your **secret key**
2. Under **Backend wallets → Setup**, create your **wallet secret**. It must belong to the same project as the secret key, or every signing call fails with `Authentication failed`
3. In the [gas sponsorships tab](https://dashboard.openfort.io/policies), add a gas sponsorship for Base Sepolia and copy its ID (`pol_...`). The first send delegates each wallet with EIP-7702, so the sponsorship's rules must allow all account functions

### Configure your environment

```bash
cp .env.example .env
```

```bash
OPENFORT_SECRET_KEY=sk_test_...
OPENFORT_WALLET_SECRET=...
OPENFORT_FEE_SPONSORSHIP_ID=pol_...
TELEGRAM_BOT_TOKEN=            # From @BotFather (/newbot); only needed for pnpm start
```

### Run it

Try the flow without Telegram first. `pnpm demo` runs `/start`, `/balance` and `/send` for a simulated user and prints explorer links:

```bash
pnpm demo
```

The demo funds the new wallet from a treasury address set in `src/demo.ts`. Point it at a backend wallet in your own project first.

Then start the real bot:

```bash
pnpm start
```
:::

## How it works

The bot has three commands: `/start` creates or loads the user's wallet, `/balance` shows ETH and USDC, and `/send <address> <amount>` sends USDC with sponsored gas. The server holds the wallet secret and authorizes every signature. Users never handle a key.

### One backend wallet per Telegram user

The server stores only the mapping from Telegram user ID to Openfort account ID and address, never keys:

```typescript
// src/wallets.ts
export async function getOrCreateWallet(telegramUserId: number) {
  const stored = getStoredWallet(telegramUserId)
  if (stored) return openfort.accounts.evm.backend.get({ id: stored.accountId })

  const account = await openfort.accounts.evm.backend.create()
  saveStoredWallet(telegramUserId, { accountId: account.id, address: account.address })
  return account
}
```

`src/store.ts` keeps the mapping in a JSON file. Replace it with your database.

### Sponsored USDC send

`backend.sendTransaction` delegates the wallet with EIP-7702 on first use, then signs and submits the transfer under your gas sponsorship. It resolves once the transaction exists, not once it has landed, so the bot polls `openfort.transactions.get` for the receipt:

```typescript
// src/wallets.ts
const data = encodeFunctionData({
  abi: erc20Abi,
  functionName: 'transfer',
  args: [to, parseUnits(amount, USDC_DECIMALS)],
})

const result = await openfort.accounts.evm.backend.sendTransaction({
  account,
  chainId: CHAIN_ID,
  interactions: [{ to: USDC_ADDRESS, data }],
  policy: config.feeSponsorshipId,
})

const tx = await openfort.transactions.get(result.id)
// Repeat until tx.status is 'succeeded' (read tx.receipt.transactionHash),
// or 'reverted' / 'failed' (read tx.receipt.error.reason).
```

The wallet holds no ETH, so a mined transfer shows the sponsorship worked.

### Platform-independent handlers

`src/commands.ts` holds `handleStart`, `handleBalance` and `handleSend` as plain functions that take a user ID and return reply text. `src/bot.ts` wires them to grammY, and `src/demo.ts` calls them directly. The same handlers work for Discord, Slack or WhatsApp.

## Production notes

* Replace the JSON store with your database.
* Add [policies](https://www.openfort.io/docs/products/server/policies) to limit what each wallet can sign. A signing policy must still allow `signEvmHash`, which the 7702 authorization and the transaction signature use.
* Let users take their wallet with them through key export, described in [backend wallet accounts](https://www.openfort.io/docs/products/server/accounts).

## Next steps

* [Server-side user wallets](https://www.openfort.io/docs/products/server/workflows/server-side-user-wallets): the pattern this bot uses
* [Gasless transactions](https://www.openfort.io/docs/products/server/evm/gasless-transactions): how `sendTransaction` sponsors gas
* [Gas sponsorship](https://www.openfort.io/docs/configuration/gas-sponsorship): set up sponsorship rules
* [Circle faucet](https://faucet.circle.com): Base Sepolia USDC
