Wallet Safety

How Robbie Gatekeeper verifies wallet ownership safely.

Only trust this domain: https://gatekeeper.robbiesuprameme.xyz

What You Should Approve

Robbie Gatekeeper only asks for a gasless signature. It should not ask you to approve a transaction, transfer funds, approve spending, or reveal a seed phrase.

Safety Checks

No seed phrase

Never enter or share your seed phrase, private key, or recovery phrase.

No transaction

The normal verification flow signs a message only. A transaction approval is not needed.

Unique session

Each verification link has a nonce and expiration, and cannot be reused after completion.

What The Connection Code Does

const accounts = await provider.connect();
const selected = accounts[0];

const nonceData = await requestNonce();
const messageHex = toHexUtf8(nonceData.message);

const signed = await provider.signMessage({ message: messageHex });

await finishLink({
  address: signed.address || selected,
  publicKey: signed.publicKey,
  signature: signed.signature,
  messageHex,
});

What The Server Checks

const proof = verifyWalletSignatureProof({
  walletAddress: wallet,
  publicKey: body.publicKey,
  signature: body.signature,
  message,
  messageHex: body.messageHex,
});

if (!proof.ok) throw new Error("wallet_signature_invalid");

The Message You Sign

This is the kind of message you should see in the wallet signature popup.

Robbie Gatekeeper wallet verification

Sign this gasless message to link your wallet with Discord.
Discord user: ...
Guild: ...
Nonce: ...
Expires: ...

Never share your seed phrase or private key.

Advanced Audit

This section is for experienced users who want to inspect the verification flow. It describes the browser actions and server checks without exposing internal secrets, API keys, or private infrastructure details.

Browser surface

The page uses the wallet provider injected by your wallet. It calls connect, then message signing. It does not import a remote wallet SDK or request a transaction.

Server surface

The server rebuilds the expected message from the stored session. It does not trust a browser-supplied message as the source of truth.

Endpoints Used By Verification

StepEndpoint / callWhat it doesWhat it must not do
1 GET /verify?session=... Loads the verification page for a temporary session. It must not ask for a seed phrase, private key, or transaction.
2 provider.connect() Asks the wallet for the selected public account address. It must not move funds or sign anything.
3 POST /api/verify/nonce Returns the exact message to sign, including Discord user, guild, nonce, and expiration. It must not return transaction calldata.
4 signMessage / SIGN_MESSAGE Asks for a gasless signature proving wallet ownership. It must not call sendTransaction, submitTransaction, transfer, approve, or allowance.
5 POST /api/verify/wallet Sends address, public key, signature, and session token for server-side verification. It must not send seed phrases, private keys, or wallet passwords.

Client Code Path

The important audit point is the absence of transaction methods. The wallet is asked to connect and sign a message only.

// The verification page uses the wallet provider already injected
// by StarKey/Ribbit. It does not import a remote wallet SDK.
const accounts = await provider.connect();
const selected = typeof accounts?.[0] === "string"
  ? accounts[0]
  : String(accounts?.[0]?.address || "");

const nonceData = await requestNonce();
const messageHex = toHexUtf8(nonceData.message);
const signed = await provider.signMessage({ message: messageHex });

await finishLink({
  provider: kind,
  address: signed.address || selected,
  publicKey: signed.publicKey,
  signature: signed.signature,
  messageHex,
});

// There is no sendTransaction, submitTransaction, transfer,
// approve, allowance, private key, or seed phrase request here.

Server Code Path

Gatekeeper verifies the proof, stores the linked wallet, then consumes the session so the same signature cannot be reused.

const message = buildWalletAuthMessage(session);
const proof = verifyWalletSignatureProof({
  walletAddress: wallet,
  publicKey: body.publicKey || body.public_key,
  signature: body.signature,
  message,
  messageHex: body.messageHex,
});

if (!proof.ok) {
  throw new Error(proof.reason || "wallet_signature_invalid");
}

store.upsertWallet({
  guildId: session.guild_id,
  projectId: session.project_id || null,
  discordUserId: session.discord_user_id,
  walletAddress: wallet,
  source: "wallet_connect",
});

store.consumeVerificationSession(token);

Signature Verification

The server derives the SUPRA address from the submitted public key and checks that the signature matches the server-built message.

const derivedAddress = deriveSupraAddressFromPublicKey(publicKeyBytes);
if (normalizeWalletFull(walletAddress) !== derivedAddress) {
  return { ok: false, reason: "public_key_address_mismatch" };
}

const candidates = [
  new TextEncoder().encode(message),
  bytesFromHex(messageHex),
  new TextEncoder().encode(String(messageHex)),
].filter(Boolean);

const valid = candidates.some((candidate) =>
  nacl.sign.detached.verify(candidate, signatureBytes, publicKeyBytes)
);

Data Sent To Gatekeeper

Sent

Session token, selected wallet address, public key, signature, provider name, and message encoding when needed.

Stored

Linked wallet, Discord user, guild/project scope, provider label, signature proof, nonce, and timestamps.

Never sent

Seed phrase, private key, wallet password, spending approval, token allowance, or transaction payload.

What The Bot Collects

This is the transparent record created by wallet verification. It is used to prove wallet ownership, link the wallet to Discord, and prevent replay.

FieldWhere it comes fromWhy it is kept
discord_user_idDiscord button/sessionLinks the wallet proof to the Discord member.
guild_idDiscord server/sessionScopes the proof to the right server.
project_idVerification session, when project-scopedScopes the linked wallet to the right Gatekeeper project.
wallet_addressSelected wallet accountStores the SUPRA wallet being verified.
providerWallet flowRecords StarKey/Ribbit when the wallet sends a provider label.
public_keyWallet signature responseLets the server verify the signature and derive the wallet address.
signatureWallet signature responseProves the wallet owner signed the gasless message.
messageServer-built verification messageStores the complete signed message for auditability.
nonceTemporary verification sessionMakes the signature unique and prevents replay.
created_atServer timestampRecords when the proof was created.
session_tokenTemporary verification URLUsed only to load and complete the session; the session is marked consumed after success.
expires_at / consumed_atTemporary verification sessionShows when the link expires and when it was used.
source / status / updated_atWallet registryTracks whether the link came from wallet verification or manual admin review, and whether it is active.

Replay Protection

Internal Events

RobbieBuyBot wallet activity events are separate from wallet verification. They are accepted only by a protected internal endpoint using a server-side API key. That key is never present in the browser, the Discord panel, or this documentation page.

Red Flags