Overview
Your Monkeytype profile is your public presence on the platform. Customize it to showcase your typing setup, achievements, and social links. Control what information is visible to other users.
Accessing Your Profile
Profiles can be accessed via:
// By username
GET / users / profile / : username
// By UID (with query parameter)
GET / users / profile / : uid ? isUid = true
From backend/src/api/controllers/user.ts:912-919:
export async function getProfile (
req : MonkeyRequest < GetProfileQuery , undefined , GetProfilePathParams >,
) : Promise < GetProfileResponse > {
const { uidOrName } = req . params ;
const user = req . query . isUid
? await UserDAL . getUser ( uidOrName , "get user profile" )
: await UserDAL . getUserByName ( uidOrName , "get user profile" );
Usernames are case-insensitive and can be changed once every 30 days.
Public Profile Data
All profiles display:
Typing Stats
Tests completed
Tests started
Total time typing
Personal Bests
15s, 30s, 60s, 120s (time modes)
10, 25, 50, 100 (word modes)
Progression
Current XP
Current streak
Max streak achieved
Identity
Username
Discord avatar (if linked)
Selected badge
Premium status
From backend/src/api/controllers/user.ts:950-987:
const validTimePbs = extractValid ( personalBests . time , [
"15" ,
"30" ,
"60" ,
"120" ,
]);
const validWordsPbs = extractValid ( personalBests . words , [
"10" ,
"25" ,
"50" ,
"100" ,
]);
const typingStats = {
completedTests ,
startedTests ,
timeTyping ,
};
const relevantPersonalBests = {
time: validTimePbs ,
words: validWordsPbs ,
};
const baseProfile = {
name ,
banned ,
addedAt ,
typingStats ,
personalBests: relevantPersonalBests ,
discordId ,
discordAvatar ,
xp ,
streak: streak ?. length ?? 0 ,
maxStreak: streak ?. maxLength ?? 0 ,
lbOptOut ,
isPremium: await UserDAL . checkIfUserIsPremium ( user . uid , user ),
};
Profile Details (Optional)
Additional customizable profile information:
interface UserProfileDetails {
bio ?: string ;
keyboard ?: string ;
socialProfiles ?: {
twitter ?: string ;
github ?: string ;
website ?: string ;
// ... other platforms
};
showActivityOnPublicProfile ?: boolean ;
}
Customizing Your Profile
Bio
Add a personal bio to introduce yourself:
PATCH / users / profile
{
"bio" : "Coffee-fueled typist | 150+ WPM | Mechanical keyboard enthusiast"
}
Bios are sanitized to prevent XSS attacks. HTML and scripts are stripped automatically.
From backend/src/api/controllers/user.ts:1040-1054:
const profileDetailsUpdates : Partial < UserProfileDetails > = {
bio: sanitizeString ( bio ),
keyboard: sanitizeString ( keyboard ),
socialProfiles: Object . fromEntries (
Object . entries ( socialProfiles ?? {}). map (([ key , value ]) => [
key ,
sanitizeString ( value ),
]),
),
showActivityOnPublicProfile ,
};
await UserDAL . updateProfile ( uid , profileDetailsUpdates , user . inventory );
Share your typing setup:
PATCH / users / profile
{
"keyboard" : "Keychron Q1 | Gateron Yellow Pro | GMK Laser keycaps"
}
Social Profiles
Link your social media and professional profiles:
PATCH / users / profile
{
"socialProfiles" : {
"twitter" : "myhandle" ,
"github" : "myusername" ,
"website" : "https://example.com"
}
}
All social profile links are sanitized. Only provide usernames or handles, not full URLs (except for website field).
Badge Selection
Selecting a Display Badge
Choose which badge appears on your profile and leaderboard entries:
PATCH / users / profile
{
"selectedBadgeId" : 14 // 365-day streak badge
}
From backend/src/api/controllers/user.ts:1032-1038:
user . inventory ?. badges . forEach (( badge ) => {
if ( badge . id === selectedBadgeId ) {
badge . selected = true ;
} else {
delete badge . selected ;
}
});
Only one badge can be selected at a time. Selecting a new badge automatically deselects the previous one.
Badge Inventory
Badges are stored in your user inventory:
interface UserInventory {
badges : Badge [];
}
interface Badge {
id : number ;
selected ?: boolean ;
}
Your badge inventory is returned with user data:
GET / users
// Response includes:
{
"inventory" : {
"badges" : [
{ "id" : 14 , "selected" : true },
{ "id" : 5 },
{ "id" : 12 }
]
}
}
Privacy Settings
Test Activity Visibility
Control whether your test activity appears on your public profile:
PATCH / users / profile
{
"showActivityOnPublicProfile" : true
}
From backend/src/api/controllers/user.ts:1003-1007:
if ( user . profileDetails ?. showActivityOnPublicProfile ) {
profileData . testActivity = generateCurrentTestActivity ( user . testActivity );
} else {
delete profileData . testActivity ;
}
When enabled, your profile includes:
{
"testActivity" : {
"testsByDays" : [ 1 , 3 , 0 , 5 , 2 , ... ], // Last 372 days
"lastDay" : 1735689600000 // Unix timestamp
}
}
Test activity shows a heatmap of your daily test counts for the past year. This is different from the premium test activity history feature which includes all historical data.
Leaderboard Opt-Out
Completely remove yourself from all leaderboards:
POST / users / optOutOfLeaderboards
From backend/src/api/controllers/user.ts:414-430:
export async function optOutOfLeaderboards (
req : MonkeyRequest ,
) : Promise < MonkeyResponse > {
const { uid } = req . ctx . decodedToken ;
await UserDAL . optOutOfLeaderboards ( uid );
await purgeUserFromDailyLeaderboards (
uid ,
req . ctx . configuration . dailyLeaderboards ,
);
await purgeUserFromXpLeaderboards (
uid ,
req . ctx . configuration . leaderboards . weeklyXp ,
);
void addImportantLog ( "user_opted_out_of_leaderboards" , "" , uid );
return new MonkeyResponse ( "User opted out of leaderboards" , null );
}
Opting out of leaderboards:
Removes you from daily and weekly leaderboards
Hides your rank from friends leaderboards
Cannot be easily reversed (requires contacting support)
You can still earn XP and maintain streaks
Viewing Other Profiles
Public Profile View
When viewing another user’s profile, you see:
GET / users / profile / someusername
// Response:
{
"name" : "someusername" ,
"banned" : false ,
"addedAt" : 1640995200000 ,
"typingStats" : {
"completedTests" : 1500 ,
"startedTests" : 1650 ,
"timeTyping" : 180000
},
"personalBests" : {
"time" : {
"60" : [
{
"wpm" : 120 ,
"acc" : 98.5 ,
"consistency" : 85 ,
"timestamp" : 1735600000000
}
]
}
},
"discordId" : "123456789" ,
"discordAvatar" : "abc123" ,
"xp" : 50000 ,
"streak" : 45 ,
"maxStreak" : 125 ,
"isPremium" : true ,
"details" : {
"bio" : "Love typing!" ,
"keyboard" : "Custom build" ,
"socialProfiles" : {
"github" : "someuser"
}
},
"allTimeLbs" : {
"time" : {
"15" : {
"english" : { "rank" : 250 , "count" : 10000 }
},
"60" : {
"english" : { "rank" : 500 , "count" : 15000 }
}
}
},
"testActivity" : {
"testsByDays" : [ ... ],
"lastDay" : 1735689600000
},
"uid" : "abc123def456"
}
Banned User Profiles
Banned users have limited profile information:
if ( banned ) {
return new MonkeyResponse ( "Profile retrived: banned user" , baseProfile );
}
Banned profiles show only:
Name
Banned status
Join date
Basic typing stats
Personal bests
No detailed information, inventory, or leaderboard ranks are displayed.
All-Time Leaderboard Ranks
Profiles display your all-time leaderboard rankings:
// From backend/src/api/controllers/user.ts:1146-1199
async function getAllTimeLbs ( uid : string ) : Promise < AllTimeLbs > {
const allTime15English = await LeaderboardsDAL . getRank (
"time" ,
"15" ,
"english" ,
uid ,
);
const allTime15EnglishCount = await LeaderboardsDAL . getCount (
"time" ,
"15" ,
"english" ,
);
const allTime60English = await LeaderboardsDAL . getRank (
"time" ,
"60" ,
"english" ,
uid ,
);
const allTime60EnglishCount = await LeaderboardsDAL . getCount (
"time" ,
"60" ,
"english" ,
);
// ...
return {
time: {
"15" : {
english: english15 ,
},
"60" : {
english: english60 ,
},
},
};
}
This shows your rank and the total number of ranked users for:
15-second English
60-second English
All-time leaderboard ranks are only shown for users who meet the minimum typing time requirement and haven’t opted out.
Profiles can be accessed via multiple URL patterns:
/profile/:username - Most common, case-insensitive
/profile/:uid?isUid=true - Using user ID
From backend/src/api/controllers/user.ts:916-919:
const user = req . query . isUid
? await UserDAL . getUser ( uidOrName , "get user profile" )
: await UserDAL . getUserByName ( uidOrName , "get user profile" );
Updating Your Profile
Complete Update Example
PATCH / users / profile
{
"bio" : "Mechanical keyboard enthusiast and competitive typist" ,
"keyboard" : "Keychron Q1 Pro | Gateron Oil King | GMK Laser" ,
"socialProfiles" : {
"twitter" : "mytwitterhandle" ,
"github" : "mygithubuser" ,
"website" : "https://myblog.com"
},
"selectedBadgeId" : 14 ,
"showActivityOnPublicProfile" : true
}
All fields are optional. Only include the fields you want to update.
All text inputs are sanitized using sanitizeString() from backend/src/utils/misc.ts to prevent:
XSS attacks
HTML injection
Script execution
SQL injection
Friends Integration
Your profile is visible to your friends via:
GET / users / friends
// Returns array of friend profiles:
[
{
"uid" : "..." ,
"name" : "friendname" ,
"xp" : 25000 ,
"streak" : 30 ,
"isPremium" : false ,
// ... other profile data
}
]
From backend/src/api/controllers/user.ts:1296-1310:
export async function getFriends (
req : MonkeyRequest ,
) : Promise < GetFriendsResponse > {
const { uid } = req . ctx . decodedToken ;
const premiumEnabled = req . ctx . configuration . users . premium . enabled ;
const data = await UserDAL . getFriends ( uid );
if ( ! premiumEnabled ) {
for ( const friend of data ) {
delete friend . isPremium ;
}
}
return new MonkeyResponse ( "Friends retrieved" , data );
}
Friends see additional context:
Last activity timestamp
Connection metadata (when friendship was established)
Premium status (if enabled)
Profile Stats Calculation
Typing statistics are calculated from all your completed tests:
Completed Tests : Tests finished without bailing out
Started Tests : All test attempts (including incomplete)
Time Typing : Total seconds spent in tests (excluding AFK time)
These values are updated after each test:
// From backend/src/api/controllers/result.ts:471-482
const afk = completedEvent . afkDuration ?? 0 ;
const totalDurationTypedSeconds =
completedEvent . testDuration + completedEvent . incompleteTestSeconds - afk ;
void UserDAL . updateTypingStats (
uid ,
completedEvent . restartCount ,
totalDurationTypedSeconds ,
);
Frequently Asked Questions
Can I change my username?
Yes, but only once every 30 days. To change your username: PATCH / users / name
{
"name" : "newusername"
}
Your old username is preserved in your name history (not publicly visible).
What happens to my profile if I'm banned?
Banned profiles show limited information:
Basic stats remain visible
Detailed profile info is hidden
You’re removed from leaderboards
Friends can still see your basic profile
Can I hide my profile completely?
No, profiles are always visible. However, you can:
Opt out of leaderboards
Hide test activity
Leave bio and social profiles blank
Not select a badge
Why doesn't my badge show up?
Ensure that:
The badge is in your inventory
You’ve selected it via the profile update endpoint
You haven’t selected a different badge since
Check your inventory: GET / users
// Look for inventory.badges[].selected = true
How do I link my Discord account?
Discord linking is separate from profile settings:
Get the OAuth link: GET /users/discord/oauth
Authorize via Discord
Complete the linking: POST /users/discord/link
Your Discord avatar will automatically appear on your profile.