Skip to content

Using custom scripts to dynamically customize account data

You can use arbitrary JavaScript to choose the user name, display name, summary, avatar, and password for the accounts you create. This can be useful if you want to create accounts in bulk.

Custom JavaScript

You can run any JavaScript code you want. Your code needs to return a function that will be called to generate the value. That function can be asynchronous.

Context & Environment

Your callback function will be called with the following arguments:

  • ctx: The context object passed to the script.
  • env: The environment object passed to the script.

Context

The context object allows you to access the current account being created (eg the previously generated username or steam id).

PropertyDescriptionAvailability Example
typeFor which field the script is being run.All"username"
user.usernameThe username of the account being created.display name, real name, summary"aciuqwnf8izwgqevbf8qhuf"
user.passwordThe username of the account being created.display name, real name, summary"PIn0UHBWE=dfhue9fzguFEO"
idThe steam ID of the account being created.display name, real name, summary"76561198000000000"
accountThe steam acocount that was created.postAccount
proxyThe proxy that was used to create the accountposthttp://127.0.0.1
cookiesSession cookies for the account.postCookie Jar
headersBase headers used for creating the account.post{ "User-Agent": "Mozilla/5.0 ..." }
accountsThe steam accounts being exported.exportArray of Accounts

The cookies object contains a top-level property of the domain, and each domain has a list of cookies. Each cookie has the following properties:

  • name - The name of the cookie.
  • value - The value of the cookie.
{
"store.steampowered.com": [
{
"name": "steamLoginSecure",
"value": "1234567890|eyMDWODNUWPDMO"
}
]
}

The following cookies are extracted from the following domains:

  • steamLoginSecure
  • sessionid

sessionid may not be present depending on the domain and features enabled. (Eg there wont be a sessionid for steam.tv)

Domains:

  • store.steampowered.com
  • checkout.steampowered.com
  • steamcommunity.com
  • help.steampowered.com
  • steam.tv

Environment

The environment object allows you to access some useful functions and data.

  • counter(name) - Increments a counter with the given name and returns the new value. The counter is stored on disk.
  • fs.readFile(path: string): Promise<string> - Reads a file from the special directory. If the content is not string safe, it will be a base64 encoded data URI.
  • fs.writeFile(path: string, content: string): Promise<void> - Writes a file to the special directory. If the content is a data URI, it will be base64 decoded.
  • fs.deleteFile(path: string): Promise<void> - Deletes a file from the special directory.
  • fs.listFiles(): Promise<Array<string>> - Lists all files in the special directory.
  • fs.mkdir(path: string): Promise<void> - Creates a directory in the special directory.
  • http(proxy?: string): Promise<HTTPClient> - Creates a new HTTP client, with an optional proxy.

File system access is limited to a special directory in the SAGE data directory.

HTTPClient

The HTTP client has the following methods:

  • request(url: string, options?: RequestInit): Promise<Response> - Makes a request to the given URL with the given options.
  • setCookie(domain: string, cookie: string, value: string) - Sets a cookie for the given domain.
  • getCookie(domain: string, cookie: string): string - Gets a cookie for the given domain.
  • close() - Closes the HTTP client.

RequestInit

The RequestInit object looks like:

  • method - The HTTP method to use.
  • headers - An object with the headers to send. Object of key-value pairs.
  • body - The body of the request. Must be a string. For binary data, use data:;base64, URIs.

Response

The Response object has the following properties:

  • status - The HTTP status code.
  • headers - An object with the headers of the response.
  • body - The response body as a string. Uses data: URIs for binary data.

Phases

There are multiple phases during account generation where you can run your custom scripts. The current phase is passed as the type property in the context object.

Generation phases

These phases are run when generating the account.

  • username & password run concurrently. They can run before or after the CAPTCHA is submitted, this depends on your “match email” setting.
  • display name, real name, summary & avatar run concurrently. They run after the accoutn is already created.
  • post runs after the account is created and the full account object is available (including steam guard data).

You can see a mermaid diagram of the generation process here.

Export phase

This phase is run when exporting accounts.

Examples

Check our Discord server for more examples and community made scripts.

Sequential usernames

return async (_, env) => {
const counter = await env.counter("username");
return `user${counter.toString().padStart(10, "0")}`;
};

Random names

return async () => {
const response = await fetch("https://randomuser.me/api/?inc=login&noinfo");
const data = await response.json();
return data.results[0].login.username;
};

Using faker.js:

let faker = null;
return async (ctx, env) => {
if (!faker)
// cache imports
faker = await import(
"https://cdn.jsdelivr.net/npm/@faker-js/[email protected]/+esm"
);
return faker.faker.internet.userName().replace(/[^a-z\d]/gi, "");
};

SteamID as name

This already exists as a built-in option, but here’s how you could do it in a custom script:

return (ctx) => ctx.id;

Random anime pfp

Uses the https://www.thiswaifudoesnotexist.net/ API to choose a random anime waifu.

return () =>
`https://www.thiswaifudoesnotexist.net/example-${Math.floor(
Math.random() * 100000,
)}.jpg`;

Random pfp from a directory

This chooses a random image from the files/avatars directory inside the SAGE data directory.

return async (_, env) => {
const files = await env.fs.listFiles("avatars");
const avatars = files.filter((f) => f.endsWith(".png"));
const avatar = avatars[Math.floor(Math.random() * avatars.length)];
// thanks to not being valid UTF-8 text, it's already encoded as a data URI
return await env.fs.readFile(`avatars/${avatar}`);
};

Exporting Accounts

return async (ctx) => {
return ctx.accounts.map((account) => account.user);
};

Sending custom Discord webhook

return async (ctx, env) => {
const payload = {
// https://discord.com/developers/docs/resources/channel#embed-object
embeds: [
{
title: "Hello world!",
description: "Hey! New SAGE account just dropped!",
fields: [
{ name: "Username", value: ctx.account.user.username },
{ name: "Password", value: `||${ctx.account.user.password}||` },
],
},
],
};
const webhookUrl = "https://discord.com/api/webhooks/....";
await fetch(webhookUrl, {
method: "POST",
body: JSON.stringify(payload),
headers: { "content-type": "application/json" },
});
};

Creating a Quick Friend Invite

return async (ctx, env) => {
// create a new http client with the same proxy
const http = await env.http(ctx.proxy);
// load our cookie jar so we are logged in
for (const [domain, cookies] of Object.entries(ctx.cookies)) {
for (const cookie of cookies) {
await http.setCookie(domain, cookie.name, cookie.value);
}
}
const response = await http.request(
"https://steamcommunity.com/invites/ajaxcreate",
{
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
...ctx.headers,
},
body: new URLSearchParams({
sessionid: ctx.cookies["steamcommunity.com"].find(
(c) => c.name === "sessionid",
).value,
steamid_user: ctx.account.id,
duration: "2592000",
}).toString(), // remember to call toString() so its an string! this isnt the fetch API
},
);
await http.close(); // we are done with the http client
const body = JSON.parse(response.body);
const inviteCode = body.token;
// now we do a bunch of math and conversions to get the account id that is in the invite link
const accountId = Number(BigInt(ctx.account.id) & 0xffffffffn);
const alphabet = "bcdfghjkmnpqrtvw";
const bytes = [
(accountId >> 24) & 0xff,
(accountId >> 16) & 0xff,
(accountId >> 8) & 0xff,
accountId & 0xff,
];
const code = bytes.map((b) => alphabet[b >> 4] + alphabet[b & 0xf]).join("");
// the invite link, finally!
const invite = `https://s.team/p/${code.slice(0, 4)}-${code.slice(
4,
)}/${inviteCode}`;
// lets send it to our discord webhook
const webhookUrl = "https://discord.com/api/webhooks/....";
await fetch(webhookUrl, {
method: "POST",
body: JSON.stringify({
content: `Invite link for ${ctx.account.user.username}: ${invite}`,
}),
headers: { "content-type": "application/json" },
});
};