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
Never enter or share your seed phrase, private key, or recovery phrase.
The normal verification flow signs a message only. A transaction approval is not needed.
Each verification link has a nonce and expiration, and cannot be reused after completion.
What The Connection Code Does
- Connects to the wallet to read the selected address.
- Requests a temporary verification message from Gatekeeper.
- Asks the wallet to sign that message.
- Sends the address, public key, and signature back to Gatekeeper for server-side verification.
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
- The signature must match the public key.
- The wallet address must be derived from that public key.
- The session must still be valid and unconsumed.
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.
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.
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
| Step | Endpoint / call | What it does | What 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
Session token, selected wallet address, public key, signature, provider name, and message encoding when needed.
Linked wallet, Discord user, guild/project scope, provider label, signature proof, nonce, and timestamps.
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.
| Field | Where it comes from | Why it is kept |
|---|---|---|
| discord_user_id | Discord button/session | Links the wallet proof to the Discord member. |
| guild_id | Discord server/session | Scopes the proof to the right server. |
| project_id | Verification session, when project-scoped | Scopes the linked wallet to the right Gatekeeper project. |
| wallet_address | Selected wallet account | Stores the SUPRA wallet being verified. |
| provider | Wallet flow | Records StarKey/Ribbit when the wallet sends a provider label. |
| public_key | Wallet signature response | Lets the server verify the signature and derive the wallet address. |
| signature | Wallet signature response | Proves the wallet owner signed the gasless message. |
| message | Server-built verification message | Stores the complete signed message for auditability. |
| nonce | Temporary verification session | Makes the signature unique and prevents replay. |
| created_at | Server timestamp | Records when the proof was created. |
| session_token | Temporary verification URL | Used only to load and complete the session; the session is marked consumed after success. |
| expires_at / consumed_at | Temporary verification session | Shows when the link expires and when it was used. |
| source / status / updated_at | Wallet registry | Tracks whether the link came from wallet verification or manual admin review, and whether it is active. |
Replay Protection
- The verification link is temporary and expires after a short window.
- The signed message includes a nonce and expiration.
- After a successful verification, the session is consumed.
- Trying to reuse the same link or old signature should fail.
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
- Deny if the wallet shows a transaction approval for verification.
- Deny if the domain is not https://gatekeeper.robbiesuprameme.xyz.
- Deny if anyone asks for your seed phrase or private key.