Skip to main content
Bot platform adapters now ship in the praisonai-bot package. praisonai bot serve still works exactly as documented here; for a standalone install see praisonai-bot Migration.
Bot pairing enables owner-approval for unknown users with inline Approve/Deny buttons sent directly to your DM.
from praisonaiagents import Agent, BotConfig

agent = Agent(name="assistant", instructions="You are a helpful assistant.")
config = BotConfig(unknown_user_policy="pair", owner_user_id="123456789")
agent.start("Unknown user requests access — notify the owner.")
The user messages the bot before they are allowlisted; the owner receives Approve/Deny buttons in DM.

Quick Start

1

Enable Pairing

from praisonaiagents import Agent
from praisonaiagents import BotConfig

config = BotConfig(
    token="YOUR_BOT_TOKEN",
    unknown_user_policy="pair",
    owner_user_id="123456789",   # your Telegram/Discord/Slack user ID
)

agent = Agent(
    name="Support",
    instructions="You are a helpful support assistant.",
)
2

Production Environment

export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
Without this, inline-button callbacks stop working across bot restarts.

How It Works

The pairing system intercepts messages from users not in allowed_users and routes them through an approval workflow controlled by the unknown_user_policy, publishing a pairing_approved event on the default EventBus after every successful approval (CLI, inline button, or web UI).
Inline approvals (PR #1892): When the owner taps Approve on the Telegram / Discord / Slack DM, the requester is paired immediately on the platform type (telegram / discord / slack) so all of their later messages flow straight through. If you saw “approve looked successful but the next message was still blocked” on an older release, upgrade to pick up this fix.
Gateway mode (praisonai gateway start): As of PR #1791, the gateway polling path runs the same pairing pipeline as the standalone bot. If pairing.enabled: true is set in your gateway config, unknown users get the same Approve/Deny inline-button flow. No code changes needed — configure unknown_user_policy: "pair" and owner_user_id in your channels: YAML entry.
Behavior change in PR #1885 (Telegram only). Before this fix, Telegram bots with an empty allowed_users list ignored unknown_user_policy and let everyone through — "pair" only worked when allowed_users was non-empty. After the fix, empty allowed_users correctly routes every Telegram user through the pairing flow when the policy is "pair". If you were running with an empty allowlist and "pair" (or default "deny"), upgrading will start enforcing the policy. Set unknown_user_policy: "allow" to restore the old “anyone can message” behavior. Discord and Slack already behaved correctly and are unchanged.

Policy Options

PolicyBehaviourWhen to use
"deny" (default)Silently drops messages from users not in allowed_users.Closed / internal bots.
"pair"Generates a code and DMs the owner an Approve/Deny button. Falls back to CLI if owner_user_id is unset.Semi-public bots where you want owner control.
"allow"Lets every unknown user through (no persistent pair).Fully public bots (combine with rate limits / approval protocol).

Which Policy Should I Choose?


CLI Fallback

When owner_user_id is not set, the bot replies to the requester:
Your pairing code: abc12345. Ask the owner to run: praisonai pairing approve telegram abc12345
The owner can then approve manually:
praisonai pairing approve <channel_type> <code>
Where <channel_type> is one of: telegram, discord, slack.

Security: HMAC-signed Callbacks

Internals (PR #2108): Pairing approve/deny now dispatches through the generic InteractiveRegistry under the reserved pair namespace. The behavior, callback format, and HMAC verification are identical — but you can now register your own handlers under different namespaces (e.g. approval, menu) on the same adapter without conflicting. See Interactive Bot Actions.
All inline-button callbacks are cryptographically signed to prevent tampering:
  • Callback format: pair:{action}:{channel}:{user_id}:{code}:{sig}
  • Signature: First 8 hex chars of HMAC-SHA256(PRAISONAI_CALLBACK_SECRET, payload)
  • Verification: Tampered callback_data fails verification and is silently ignored + logged
Without PRAISONAI_CALLBACK_SECRET set in your environment, a random per-process secret is used and inline buttons stop working after bot restart. Always set this in production:
export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"

Platform-specific UI

Uses InlineKeyboardMarkup with ✅ Approve / ❌ Deny buttons. Callbacks are handled via CallbackQueryHandler that parses the signed callback_data and verifies the HMAC signature.What the owner sees:
User @username wants to chat. Approve access?
[✅ Approve] [❌ Deny]
Implementation: Telegram’s InlineKeyboardButton with callback_data containing the signed pairing payload. The {channel} field in the pair:{action}:{channel}:{user_id}:{code}:{sig} format contains the platform identifier (telegram), not the chat ID.
Uses discord.ui.View with success (green) and danger (red) button styles. Handled via button.callback method that verifies the HMAC signature in the custom_id.What the owner sees:
User username#1234 wants to chat. Approve access?
[✅ Approve] [❌ Deny]
Implementation: Discord’s Button components in an Action Row with HMAC-signed custom_id values. The {channel} field in the callback format is the platform identifier (discord).
Uses Block Kit actions block with primary (blue) and danger (red) button styles. Handled via @app.action("pair_approve") and @app.action("pair_deny") decorators that verify the signature in the button’s value.What the owner sees:
*@username* wants to chat. Approve access?
[✅ Approve] [❌ Deny]
Implementation: Slack Block Kit buttons with HMAC-signed values and dedicated action handlers. The {channel} field in the callback format is the platform identifier (slack).

Configuration Options

For the complete BotConfig options including unknown_user_policy and owner_user_id, see the canonical reference at Messaging Bots Configuration.

Common Patterns

from praisonaiagents import Agent
from praisonaiagents import BotConfig

config = BotConfig(
    token="your-bot-token",
    unknown_user_policy="pair",    # Enable approval workflow
    owner_user_id="123456789",     # Your platform user ID
)

agent = Agent(
    name="Support Bot",
    instructions="Help users with their questions",
)
Perfect for customer support or community bots where you want to vet new users.

Best Practices

Generate a strong secret and set it as an environment variable:
# Generate and export
export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"

# Or set permanently in your deployment
echo "PRAISONAI_CALLBACK_SECRET=$(openssl rand -hex 32)" >> .env
Without this, inline buttons stop working when your bot restarts.
Each platform has its own user ID format:
  • Telegram: Numeric ID (e.g., 123456789)
  • Discord: Snowflake ID (e.g., 123456789012345678)
  • Slack: User ID format (e.g., U1234ABCD)
Find your ID by messaging the bot directly and checking the logs, or use platform-specific methods.
If using unknown_user_policy="allow" for a public bot, protect yourself with:
config = BotConfig(
    unknown_user_policy="allow",
    auto_approve_tools=False,     # Users still need approval for dangerous tools
    debounce_ms=2000,            # Coalesce rapid messages
)
Consider also implementing rate limiting at the platform level.
When you deny a pairing request, the code is consumed and cannot be retried. The user must send a new message to generate a fresh code. This prevents spam and ensures each approval decision is deliberate.

Pair a user with a non-empty --label (the canonical id) and StoreBackedIdentityResolver automatically unifies their session across every channel paired under the same label. No separate praisonai identity link calls needed. See Cross-Platform Sessions › StoreBackedIdentityResolver.

Messaging Bots

Complete bot configuration and setup

Bot Security

Security best practices for bots

Web-UI HTTP API Approval

HTTP API endpoints for web admin UI approval

Cross-Platform Sessions

Unified per-user sessions across platforms