LOADING
Roblox stalk account data publik akun Roblox berdasarkan **username**.
#!/usr/bin/env node
/**
* Roblox Account Stalker (Node.js ESM)
*
* Usage:
* node roblox-stalk.js <username>
* node roblox-stalk.js <username> --pretty
*
* As module:
* import { stalk } from './roblox-stalk.js'
* const data = await stalk('builderman')
*/
const BASE = {
users: 'https://users.roblox.com',
avatar: 'https://avatar.roblox.com',
thumbnails: 'https://thumbnails.roblox.com',
friends: 'https://friends.roblox.com',
presence: 'https://presence.roblox.com',
groups: 'https://groups.roblox.com',
badges: 'https://badges.roblox.com',
account: 'https://accountinformation.roblox.com',
games: 'https://games.roblox.com',
};
const UA = 'Mozilla/5.0 (compatible; RobloxStalker/1.3)';
async function request(url, options = {}) {
const res = await fetch(url, {
...options,
headers: {
'User-Agent': UA,
Accept: 'application/json',
...(options.headers || {}),
},
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status} → ${url}\n${text.slice(0, 300)}`);
}
return res.json();
}
async function getUserIdByUsername(username) {
const data = await request(`${BASE.users}/v1/usernames/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
usernames: [username],
excludeBannedUsers: false,
}),
});
if (!data.data?.length) {
throw new Error(`Username "${username}" tidak ditemukan`);
}
return data.data[0];
}
async function getUser(userId) {
return request(`${BASE.users}/v1/users/${userId}`);
}
async function getAvatar(userId) {
return request(`${BASE.avatar}/v1/users/${userId}/avatar`);
}
async function getUserThumbnails(userId, size = '420x420') {
const [full, headshot, bust] = await Promise.all([
request(
`${BASE.thumbnails}/v1/users/avatar?userIds=${userId}&size=${size}&format=Png&isCircular=false`
),
request(
`${BASE.thumbnails}/v1/users/avatar-headshot?userIds=${userId}&size=${size}&format=Png&isCircular=false`
),
request(
`${BASE.thumbnails}/v1/users/avatar-bust?userIds=${userId}&size=${size}&format=Png&isCircular=false`
),
]);
return {
fullBody: full.data?.[0]?.imageUrl || null,
fullBodyState: full.data?.[0]?.state || null,
headshot: headshot.data?.[0]?.imageUrl || null,
headshotState: headshot.data?.[0]?.state || null,
bust: bust.data?.[0]?.imageUrl || null,
bustState: bust.data?.[0]?.state || null,
};
}
async function getAssetThumbnails(assetIds, size = '150x150') {
if (!assetIds?.length) return {};
const unique = [...new Set(assetIds.map(Number).filter(Boolean))];
const map = {};
const chunkSize = 80;
for (let i = 0; i < unique.length; i += chunkSize) {
const chunk = unique.slice(i, i + chunkSize);
try {
const data = await request(
`${BASE.thumbnails}/v1/assets?assetIds=${chunk.join(',')}&size=${size}&format=Png&isCircular=false`
);
for (const item of data.data || []) {
map[item.targetId] = {
imageUrl: item.imageUrl || null,
state: item.state || null,
};
}
} catch {
// skip
}
}
return map;
}
async function getSocial(userId) {
const [friends, followers, followings] = await Promise.all([
request(`${BASE.friends}/v1/users/${userId}/friends/count`).catch(() => ({ count: null })),
request(`${BASE.friends}/v1/users/${userId}/followers/count`).catch(() => ({ count: null })),
request(`${BASE.friends}/v1/users/${userId}/followings/count`).catch(() => ({ count: null })),
]);
return {
friends: friends.count ?? null,
followers: followers.count ?? null,
followings: followings.count ?? null,
};
}
async function getPresence(userId) {
try {
const data = await request(`${BASE.presence}/v1/presence/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIds: [userId] }),
});
return data.userPresences?.[0] || null;
} catch {
return null;
}
}
async function getGroups(userId) {
try {
const data = await request(`${BASE.groups}/v2/users/${userId}/groups/roles`);
return (data.data || []).map((g) => ({
id: g.group.id,
name: g.group.name,
memberCount: g.group.memberCount,
role: g.role.name,
rank: g.role.rank,
isPrimary: g.isPrimaryAffiliation || false,
}));
} catch {
return [];
}
}
async function getRecentBadges(userId, limit = 10) {
try {
const data = await request(
`${BASE.badges}/v1/users/${userId}/badges?limit=${limit}&sortOrder=Desc`
);
return (data.data || []).map((b) => ({
id: b.id,
name: b.name,
description: b.description,
displayName: b.displayName,
awardedDate: b.awardedDate,
iconImageId: b.iconImageId,
}));
} catch {
return [];
}
}
/** Username history (previous usernames) */
async function getUsernameHistory(userId, limit = 50) {
try {
// limit parameter kadang rate-limit, coba tanpa dulu lalu fallback
const data = await request(
`${BASE.users}/v1/users/${userId}/username-history?limit=${limit}&sortOrder=Desc`
).catch(() =>
request(`${BASE.users}/v1/users/${userId}/username-history`)
);
return (data.data || []).map((h) => ({
name: h.name,
// beberapa response punya timestamp, sebagian tidak
}));
} catch {
return [];
}
}
/** Official Roblox system badges */
async function getRobloxBadges(userId) {
try {
const data = await request(
`${BASE.account}/v1/users/${userId}/roblox-badges`
);
// response langsung array
return (Array.isArray(data) ? data : data.data || []).map((b) => ({
id: b.id,
name: b.name,
description: b.description,
imageUrl: b.imageUrl || null,
}));
} catch {
return [];
}
}
/** Favorite games (public) */
async function getFavoriteGames(userId, limit = 10) {
try {
const data = await request(
`${BASE.games}/v2/users/${userId}/favorite/games?limit=${limit}&sortOrder=Desc`
);
return (data.data || []).map((g) => ({
id: g.id,
name: g.name,
description: g.description || '',
rootPlace: g.rootPlace
? { id: g.rootPlace.id, name: g.rootPlace.name }
: null,
placeVisits: g.placeVisits ?? null,
}));
} catch {
return [];
}
}
/**
* Main stalk function
* @param {string} username
* @param {object} [options]
* @param {string} [options.thumbnailSize='420x420']
* @param {string} [options.assetThumbnailSize='150x150']
*/
export async function stalk(username, options = {}) {
if (!username || typeof username !== 'string') {
throw new Error('Username harus diisi');
}
const clean = username.trim().replace(/^@/, '');
const userThumbSize = options.thumbnailSize || '420x420';
const assetThumbSize = options.assetThumbnailSize || '150x150';
const basic = await getUserIdByUsername(clean);
const userId = basic.id;
const [
user,
avatar,
userThumbs,
social,
presence,
groups,
badges,
usernameHistory,
robloxBadges,
favoriteGames,
] = await Promise.all([
getUser(userId),
getAvatar(userId),
getUserThumbnails(userId, userThumbSize),
getSocial(userId),
getPresence(userId),
getGroups(userId),
getRecentBadges(userId),
getUsernameHistory(userId),
getRobloxBadges(userId),
getFavoriteGames(userId),
]);
const equippedRaw = avatar.assets || [];
const emotesRaw = avatar.emotes || [];
const allAssetIds = [
...equippedRaw.map((a) => a.id),
...emotesRaw.map((e) => e.assetId),
];
const assetThumbs = await getAssetThumbnails(allAssetIds, assetThumbSize);
// Urutan: fashion / accessory dulu, baru body part, terakhir animation
const ASSET_ORDER = {
// Fashion & accessories (prioritas tinggi)
Hat: 10,
HairAccessory: 11,
FaceAccessory: 12,
NeckAccessory: 13,
ShoulderAccessory: 14,
FrontAccessory: 15,
BackAccessory: 16,
WaistAccessory: 17,
Shirt: 20,
TShirt: 21,
TShirtAccessory: 22,
Pants: 23,
PantsAccessory: 24,
JacketAccessory: 25,
SweaterAccessory: 26,
ShortsAccessory: 27,
LeftShoeAccessory: 28,
RightShoeAccessory: 29,
DressSkirtAccessory: 30,
Gear: 35,
// Face / head cosmetic
Face: 40,
Head: 41,
DynamicHead: 42,
// Body parts (di belakang)
Torso: 50,
LeftArm: 51,
RightArm: 52,
LeftLeg: 53,
RightLeg: 54,
// Animations (paling belakang)
ClimbAnimation: 60,
FallAnimation: 61,
IdleAnimation: 62,
JumpAnimation: 63,
RunAnimation: 64,
SwimAnimation: 65,
WalkAnimation: 66,
MoodAnimation: 67,
};
const equippedAssets = equippedRaw
.map((a) => {
const typeName = a.assetType?.name || a.assetType || 'Unknown';
const thumb = assetThumbs[a.id] || {};
return {
id: a.id,
name: a.name,
assetType: typeName,
assetTypeId: a.assetType?.id ?? null,
currentVersionId: a.currentVersionId,
imageUrl: thumb.imageUrl || null,
imageState: thumb.state || null,
_sort: ASSET_ORDER[typeName] ?? 99,
};
})
.sort((a, b) => a._sort - b._sort || a.name.localeCompare(b.name))
.map(({ _sort, ...rest }) => rest);
const emotes = emotesRaw.map((e) => {
const thumb = assetThumbs[e.assetId] || {};
return {
assetId: e.assetId,
assetName: e.assetName,
position: e.position,
imageUrl: thumb.imageUrl || null,
imageState: thumb.state || null,
};
});
return {
success: true,
fetchedAt: new Date().toISOString(),
user: {
id: user.id,
name: user.name,
displayName: user.displayName,
description: user.description || '',
created: user.created,
isBanned: user.isBanned,
hasVerifiedBadge: user.hasVerifiedBadge,
externalAppDisplayName: user.externalAppDisplayName || null,
social: {
friends: social.friends,
followers: social.followers,
followings: social.followings,
},
usernameHistory,
},
avatar: {
playerAvatarType: avatar.playerAvatarType,
bodyColors: avatar.bodyColors,
scales: avatar.scales,
defaultShirtApplied: avatar.defaultShirtApplied,
defaultPantsApplied: avatar.defaultPantsApplied,
emotes,
equippedAssets,
},
thumbnails: {
fullBody: userThumbs.fullBody,
headshot: userThumbs.headshot,
bust: userThumbs.bust,
fullBodyState: userThumbs.fullBodyState,
headshotState: userThumbs.headshotState,
bustState: userThumbs.bustState,
},
presence: presence
? {
userPresenceType: presence.userPresenceType,
lastLocation: presence.lastLocation,
placeId: presence.placeId,
rootPlaceId: presence.rootPlaceId,
gameId: presence.gameId,
universeId: presence.universeId,
lastOnline: presence.lastOnline,
}
: null,
groups,
recentBadges: badges,
robloxBadges,
favoriteGames,
profileUrl: `https://www.roblox.com/users/${userId}/profile`,
};
}
// ============ CLI ============
const isMain =
import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}` ||
process.argv[1]?.endsWith('roblox-stalk.js');
if (isMain) {
const args = process.argv.slice(2);
const username = args.find((a) => !a.startsWith('-'));
const pretty = args.includes('--pretty') || args.includes('-p');
if (!username) {
console.error('Usage: node roblox-stalk.js <username> [--pretty]');
process.exit(1);
}
stalk(username)
.then((data) => {
console.log(JSON.stringify(data, null, pretty ? 2 : 0));
})
.catch((err) => {
console.error(
JSON.stringify({ success: false, error: err.message }, null, 2)
);
process.exit(1);
});
}Node.js ESM scraper/wrapper untuk mengambil data publik akun Roblox berdasarkan username.
Output JSON lengkap: profil, avatar (item yang dipakai + gambar), emotes, social stats, presence, groups, badges, username history, favorite games, dll.
equippedAssets) + imageUrl
fetch)bashnode roblox-stalk.js <username> node roblox-stalk.js <username> --pretty # alias node roblox-stalk.js <username> -p
Contoh:
bashnode roblox-stalk.js builderman --pretty node roblox-stalk.js sann289 -p
jsimport { stalk } from './roblox-stalk.js'; const data = await stalk('builderman'); console.log(data.user.displayName); console.log(data.thumbnails.headshot); console.log(data.avatar.equippedAssets[0].imageUrl); console.log(data.avatar.emotes);
Opsi tambahan:
jsconst data = await stalk('builderman', { thumbnailSize: '420x420', // ukuran avatar user assetThumbnailSize: '150x150', // ukuran gambar item/emote });
json{ "success": true, "fetchedAt": "2026-10-28T...", "user": { "id": 156, "name": "builderman", "displayName": "builderman", "description": "...", "created": "2006-...", "isBanned": false, "hasVerifiedBadge": true, "social": { "friends": 12, "followers": 50000, "followings": 10 }, "usernameHistory": [ { "name": "old_name" } ] }, "avatar": { "playerAvatarType": "R15", "bodyColors": { ... }, "scales": { ... }, "emotes": [ { "assetId": 123, "assetName": "Wave", "position": 1, "imageUrl": "https://tr.rbxcdn.com/...", "imageState": "Completed" } ], "equippedAssets": [ { "id": 456, "name": "Classic Fedora", "assetType": "Hat", "assetTypeId": 8, "imageUrl": "https://tr.rbxcdn.com/...", "imageState": "Completed" } ] }, "thumbnails": { "fullBody": "https://tr.rbxcdn.com/...", "headshot": "https://tr.rbxcdn.com/...", "bust": "https://tr.rbxcdn.com/..." }, "presence": { "userPresenceType": 0, "lastLocation": "Website" }, "groups": [ ... ], "recentBadges": [ ... ], "robloxBadges": [ ... ], "favoriteGames": [ ... ], "profileUrl": "https://www.roblox.com/users/156/profile" }
userPresenceType| Value | Meaning |
|---|---|
| 0 | Offline |
| 1 | Online |
| 2 | InGame |
| 3 | InStudio |
username-history). Jangan spam terlalu cepat.null / [] kalau private atau tidak tersedia.bust thumbnail kadang Pending / null — normal dari sisi Roblox.| Data | Endpoint |
|---|---|
| Username → ID | POST users.roblox.com/v1/usernames/users |
| User profile | GET users.roblox.com/v1/users/{id} |
| Username history | GET users.roblox.com/v1/users/{id}/username-history |
| Avatar | GET avatar.roblox.com/v1/users/{id}/avatar |
| User thumbnails | GET thumbnails.roblox.com/v1/users/avatar* |
| Asset thumbnails | GET thumbnails.roblox.com/v1/assets |
| Friends count | GET friends.roblox.com/v1/users/{id}/friends/count |
| Presence | POST presence.roblox.com/v1/presence/users |
| Groups | GET groups.roblox.com/v2/users/{id}/groups/roles |
| Experience badges | GET badges.roblox.com/v1/users/{id}/badges |
| Roblox badges | GET accountinformation.roblox.com/v1/users/{id}/roblox-badges |
| Favorite games | GET games.roblox.com/v2/users/{id}/favorite/games |
Bebas dipakai / dimodifikasi.