Skip to main content
Telegram bots are a great use case for Action Codes — users can interact with blockchain actions directly in chat.

The flow

  1. User starts chat with your bot
  2. Bot asks for an action code
  3. User gets code from actioncode.app and sends it
  4. Bot attaches an action (transaction or message)
  5. User approves in actioncode.app
  6. Bot confirms completion

Example bot

Using grammY (works similarly with other libraries):
import { Bot, Context } from 'grammy'
import { ActionCodesClient } from '@actioncodes/sdk'

const bot = new Bot(process.env.BOT_TOKEN!)
const client = new ActionCodesClient({
  authToken: process.env.ACTION_CODES_TOKEN
})

// /start command
bot.command('start', (ctx) => {
  ctx.reply(
    'Welcome! I can help you sign messages with your Solana wallet.\n\n' +
    'Commands:\n' +
    '/sign - Sign a message\n' +
    '/verify - Verify wallet ownership'
  )
})

// /sign command
bot.command('sign', async (ctx) => {
  await ctx.reply(
    'To sign a message:\n\n' +
    '1. Open actioncode.app in your Solana wallet\n' +
    '2. Connect and get your code\n' +
    '3. Send me the 8-digit code'
  )

  // Store state that we're expecting a code for signing
  await setUserState(ctx.from!.id, { action: 'sign' })
})

// Handle code input
bot.on('message:text', async (ctx) => {
  const text = ctx.message.text

  // Check if it looks like an action code (8 digits)
  if (!/^\d{8}$/.test(text)) {
    return // Not a code, ignore or handle other messages
  }

  const userState = await getUserState(ctx.from!.id)
  if (!userState?.action) {
    await ctx.reply('Send /sign first to start a signing session.')
    return
  }

  await ctx.reply('Verifying code...')

  try {
    // Resolve the code
    const actionCode = await client.resolve(text)

    await ctx.reply(
      `Code valid!\n` +
      `Wallet: ${actionCode.pubkey.slice(0, 8)}...\n\n` +
      `Attaching message for you to sign...`
    )

    // Create and attach message
    const message = `Signed via Telegram bot\nUser: @${ctx.from!.username}\nTime: ${new Date().toISOString()}`

    await client.attachMessage(text, message, {
      description: 'Telegram signature request'
    })

    await ctx.reply(
      'Message attached!\n\n' +
      'Now go to actioncode.app and approve the signing request.'
    )

    // Wait for signature
    for await (const status of client.observeStatus(text, { timeout: 120000 })) {
      if (status.signedMessage) {
        await ctx.reply(
          `Message signed!\n\n` +
          `Signature: ${status.signedMessage.slice(0, 20)}...`
        )
        await clearUserState(ctx.from!.id)
        return
      }
    }

    // If we get here, it timed out
    await ctx.reply('Code expired. Use /sign to try again.')

  } catch (error: any) {
    await ctx.reply(`Error: ${error.message}\n\nUse /sign to try again.`)
  }

  await clearUserState(ctx.from!.id)
})

// Start bot
bot.start()

State management helpers

// Simple in-memory state (use Redis or DB in production)
const userStates = new Map<number, { action: string }>()

async function setUserState(userId: number, state: { action: string }) {
  userStates.set(userId, state)
}

async function getUserState(userId: number) {
  return userStates.get(userId)
}

async function clearUserState(userId: number) {
  userStates.delete(userId)
}

Transaction example

For sending SOL:
bot.command('send', async (ctx) => {
  await ctx.reply(
    'To send SOL:\n\n' +
    '1. Reply with: <amount> <recipient>\n' +
    '   Example: 0.1 ABC123...\n\n' +
    '2. Then send your action code from actioncode.app'
  )
  await setUserState(ctx.from!.id, { action: 'send' })
})

// Handle "0.1 ABC123..." format
bot.hears(/^(\d+\.?\d*)\s+(\w{32,44})$/, async (ctx) => {
  const amount = parseFloat(ctx.match[1])
  const recipient = ctx.match[2]

  await setUserState(ctx.from!.id, {
    action: 'send',
    amount,
    recipient
  })

  await ctx.reply(
    `Ready to send ${amount} SOL to ${recipient.slice(0, 8)}...\n\n` +
    `Now send your 8-digit code from actioncode.app`
  )
})

User experience tips

  1. Guide users clearly — Not everyone knows what actioncode.app is
  2. Show progress — Send messages at each step
  3. Handle timeouts gracefully — Codes expire in ~2 minutes
  4. Confirm success — Show the result clearly
// Good UX: Clear instructions
await ctx.reply(
  'Getting ready to sign!\n\n' +
  '1. Open your Solana wallet (Phantom, Solflare, etc.)\n' +
  '2. In the wallet browser, go to: actioncode.app\n' +
  '3. Tap "Get Code" and copy the 8 digits\n' +
  '4. Paste the code here\n\n' +
  'The code expires in 2 minutes, so do this quickly!'
)

Live example

Try the official example bot: @action_codes_bot Source code: github.com/otaprotocol/telegram-bot-example