Senpi Agent Onboarding
Onboard an AI agent into the Senpi trading platform. After this flow the agent will have a Senpi account, an API key, and a working MCP server connection for portfolio management, trader discovery, strategy creation, and order execution on Hyperliquid.
Prerequisites: Shell/code execution access and Node.js (required for npx).
Defaults
SENPI_MCP_ENDPOINT: https://mcp.prod.senpi.ai (use this if {{SENPI_MCP_ENDPOINT}} is a literal unsubstituted placeholder)
REFERRAL_CODE: empty/optional (use this if {{REFERRAL_CODE}} is a literal unsubstituted placeholder)
Arena intent hard-gate: If the user's message contains "arena", "agents arena", "competition", "prize pool", "qualify", "qualification", "weekly cycle", or "weekly competition" β call read_senpi_guide(uri="senpi://guides/arena") before composing any answer. Do not use web search or leaderboard data for Arena questions. Full routing rules in references/post-onboarding.md.
Onboarding Flow
Follow every step in order. Do not skip steps.
Before you begin: State initialization
Per the state lifecycle, ensure state.json exists so routing and transitions are well-defined. If it does not exist, create it with initial FRESH state:
if [ ! -f ~/.config/senpi/state.json ]; then
mkdir -p ~/.config/senpi
cat > ~/.config/senpi/state.json << 'STATEEOF'
{
"version": "1.0.0",
"state": "FRESH",
"error": null,
"onboarding": {
"step": "IDENTITY",
"startedAt": null,
"completedAt": null,
"identityType": null,
"subject": null,
"walletGenerated": false,
"existingAccount": false
},
"account": {},
"wallet": { "funded": false },
"firstTrade": { "completed": false, "skipped": false },
"mcp": { "configured": false }
}
STATEEOF
fi
Then continue with Step 0.
Transition to ONBOARDING: Before running Step 0, if state is FRESH, update state.json so the state machine and resume behavior work. Set state to ONBOARDING, set onboarding.startedAt to current ISO 8601 UTC, and keep onboarding.step as IDENTITY. Use a read-modify-write (merge) so other fields are preserved:
node -e "
const fs = require('fs');
const p = require('os').homedir() + '/.config/senpi/state.json';
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
if (s.state === 'FRESH') {
s.state = 'ONBOARDING';
s.onboarding = s.onboarding || {};
s.onboarding.startedAt = new Date().toISOString();
s.onboarding.step = s.onboarding.step || 'IDENTITY';
fs.writeFileSync(p, JSON.stringify(s, null, 2));
}
"
If state is already ONBOARDING, read onboarding.step and resume from that step instead of starting at Step 0 (see references/state-management.md).
Step 0: Verify mcporter (OpenClaw only)
Check if mcporter CLI is available:
if command -v mcporter &> /dev/null; then
MCPORTER_AVAILABLE=true
else
MCPORTER_AVAILABLE=false
fi
If unavailable and on OpenClaw, install it:
npm i -g mcporter
mcporter --version
Set MCPORTER_AVAILABLE=true once installed and proceed.
Step 1: Collect Identity
Present all three options to the user and wait for them to choose:
- Option A -- Telegram user ID: The skill will read your Telegram identity from USER.md automatically.
- Option B -- User-provided wallet: Must be
0x-prefixed, exactly 42 hex characters. Validate before proceeding.
- Option C -- Agent-generated wallet (only if you have neither).
Option A: Collect Telegram user ID
When the user chooses Option A, first attempt to read from ${OPENCLAW_WORKSPACE_DIR}/USER.md:
USER_MD="${OPENCLAW_WORKSPACE_DIR}/USER.md"
if [ -f "$USER_MD" ]; then
TELEGRAM_USER_ID=$(awk '/^## Telegram/{f=1; next} f && /^## /{f=0} f && /- Chat ID:/{print $NF; exit}' "$USER_MD")
TELEGRAM_USERNAME=$(awk '/^## Telegram/{f=1; next} f && /^## /{f=0} f && /- Username:/{print $NF; exit}' "$USER_MD")
TELEGRAM_USERNAME="${TELEGRAM_USERNAME#@}"
fi
If both TELEGRAM_USER_ID (digits-only, non-empty) and TELEGRAM_USERNAME (non-empty) are found, set the variables automatically without prompting the user:
IDENTITY_TYPE="TELEGRAM"
IDENTITY_VALUE="$TELEGRAM_USER_ID"
If USER.md is missing or either field is absent/invalid, fall back to the manual prompt: see references/telegram-identity.md for user-facing instructions and validation rules. Also set TELEGRAM_USERNAME from the user's input if prompted manually, then normalize it before API use with TELEGRAM_USERNAME="${TELEGRAM_USERNAME#@}".
Option B: Set variables
IDENTITY_TYPE="WALLET"
IDENTITY_VALUE="0x..."
Option C: Generate EVM wallet
Use only when the user confirms they have neither wallet nor Telegram. Inform the user before proceeding.
Run the bundled script to generate a wallet:
WALLET_DATA=$(npx -y -p ethers@6 node scripts/generate_wallet.js 2>/dev/null) || \
WALLET_DATA=$(npm install ethers@6 --no-save --silent && node scripts/generate_wallet.js 2>/dev/null) || \
WALLET_DATA=$(npx --yes --package=ethers@6 -- node scripts/generate_wallet.js)
If the script is not available at scripts/generate_wallet.js, generate inline:
WALLET_DATA=$(npx -y -p ethers@6 node -e "
const { ethers } = require('ethers');
const w = ethers.Wallet.createRandom();
console.log(JSON.stringify({
address: w.address,
privateKey: w.privateKey,
mnemonic: w.mnemonic.phrase
}));
")
Do not prompt the user on failure -- try fallbacks silently. Only report if all methods fail. See references/error-handling.md for wallet generation failure handling.
Parse WALLET_DATA JSON to extract address, privateKey, and mnemonic. Validate the address is not empty or null. If invalid, stop and see error handling reference.
Persist the wallet immediately (before continuing) using the parsed values:
mkdir -p ~/.config/senpi
chmod 600 ~/.config/senpi/wallet.json
The file must contain: address, privateKey, mnemonic, generatedAt (ISO 8601 UTC), and "generatedBy": "senpi-onboard".
CRITICAL:
- Do not log or display the private key or mnemonic.
- Do not proceed until
wallet.json is written and permissions set.
Set the identity variables using the parsed address:
WALLET_GENERATED=true
IDENTITY_TYPE="WALLET"
IDENTITY_VALUE="<address from WALLET_DATA>"
Notify the user that a wallet was generated and saved to ~/.config/senpi/wallet.json with restricted permissions. Instruct them to back up this file immediately.
Verify before proceeding
Before Step 2, confirm these are set:
IDENTITY_TYPE -- "WALLET" or "TELEGRAM"
IDENTITY_VALUE -- wallet address (with 0x) or Telegram numeric user ID (digits only)
WALLET_GENERATED -- true if Option C was used, unset otherwise
TELEGRAM_USERNAME -- required and non-empty when IDENTITY_TYPE="TELEGRAM" (unset otherwise)
Persist progress for resume: Update ~/.config/senpi/state.json: set onboarding.step to REFERRAL, and if available set onboarding.identityType, onboarding.subject, onboarding.walletGenerated from current variables. Use read-modify-write so other fields are preserved.
Step 2: Set Referral Code
REFERRAL_CODE="{{REFERRAL_CODE}}"
If empty and user hasn't provided one, that's fine -- it's optional. Do not prompt unless the user mentions having one.
Persist progress for resume: Update ~/.config/senpi/state.json: set onboarding.step to API_CALL. Use read-modify-write.
Step 3: Call Onboarding API
Execute the CreateAgentStubAccount GraphQL mutation. This is a public endpoint -- no auth required.
if [ "$IDENTITY_TYPE" = "TELEGRAM" ] && [ -z "${TELEGRAM_USERNAME:-}" ]; then
echo "TELEGRAM_USERNAME must be set when IDENTITY_TYPE=TELEGRAM" >&2
exit 1
fi
RESPONSE=$(curl -s -X POST https://moxie-backend.prod.senpi.ai/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CreateAgentStubAccount($input: CreateAgentStubAccountInput!) { CreateAgentStubAccount(input: $input) { user { id privyId userName name referralCode referrerId } apiKey apiKeyExpiresIn apiKeyTokenType referralCode agentWalletAddress } }",
"variables": {
"input": {
"from": "'"${IDENTITY_TYPE}"'",
"subject": "'"${IDENTITY_VALUE}"'",
'"$([ "$IDENTITY_TYPE" = "TELEGRAM" ] && echo "\"userName\": \"${TELEGRAM_USERNAME}\",")"'
"referralCode": "'"${REFERRAL_CODE}"'",
"apiKeyName": "agent-'"$(date +%s