Bot Unknown-User Pairing
Owner-DM inline-button approval for unknown users on Telegram, Discord, and Slack bots
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
- Enable Pairing
python 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.", )The same config from
gateway.yaml— both keys are read per-channel (PR #2856):yaml channels: telegram: token: ${TELEGRAM_BOT_TOKEN} unknown_user_policy: pair owner_user_id: ${TELEGRAM_OWNER_ID} routes: default: assistant - Multi-host deployments (optional)
On a single host, callback signing works out of the box — the signing key is the persisted gateway secret at
~/.praisonai/gateway/.gateway_secret(mode0600, auto-created on first use), so buttons survive restarts with no operator action.Set
PRAISONAI_CALLBACK_SECRETto a shared value only when running the gateway on multiple hosts/replicas so each verifies the same signatures:bash export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
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).
Policy Options
| Policy | Behaviour | When 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 abc12345The owner can then approve manually:
praisonai pairing approve <channel_type> <code>Where <channel_type> is one of: telegram, discord, slack.
Security: HMAC-signed Callbacks
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(callback_secret, payload), - where
callback_secretisPRAISONAI_CALLBACK_SECRETwhen set, otherwise - the persisted gateway secret from
~/.praisonai/gateway/.gateway_secret. - Verification: Tampered
callback_datafails verification and is silently ignored + logged
Platform-specific UI
Telegram
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.
Discord
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).
Slack
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
Semi-public Bot
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.
Internal Bot
from praisonaiagents import Agent
from praisonaiagents import BotConfig
config = BotConfig(
token="your-bot-token",
unknown_user_policy="deny", # Block unknown users
allowed_users=["user123", "user456"], # Explicit allowlist
)
agent = Agent(
name="Internal Assistant",
instructions="Help with internal tasks",
)For team or company-internal bots with a fixed user list.
Public Bot
from praisonaiagents import Agent
from praisonaiagents import BotConfig
config = BotConfig(
token="your-bot-token",
unknown_user_policy="allow", # Open to everyone
auto_approve_tools=False, # Still require tool approval
)
agent = Agent(
name="Public Assistant",
instructions="Help anyone with general questions",
)For fully public bots. Combine with rate limiting and tool approval for safety.
Gateway YAML
channels:
telegram_support:
token: ${TELEGRAM_BOT_TOKEN}
allowed_users: ["123456789"]
unknown_user_policy: "pair"
owner_user_id: "123456789"Gateway mode configuration for the pairing workflow.
Best Practices
Share the callback secret only across multiple hosts
On a single host, inline buttons survive restarts by default — the signing key is the persisted gateway secret at ~/.praisonai/gateway/.gateway_secret (mode 0600, auto-created on first use). No environment variable is needed.
Set PRAISONAI_CALLBACK_SECRET to a shared value only when running the gateway on multiple hosts/replicas so each verifies the same signatures:
# Generate and export the same value on every host
export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
# Or set permanently in your deployment
echo "PRAISONAI_CALLBACK_SECRET=$(openssl rand -hex 32)" >> .envUse platform-native user IDs for owner_user_id
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.
Combine 'allow' policy with rate limiting and tool approval
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.
Treat denied pairings as final
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.