-
-
Save cptmacp/1e9a9f20f69c113a0828fea8d13cb34c to your computer and use it in GitHub Desktop.
| /** Config Starts. **/ | |
| const profiles = [ | |
| { | |
| cred: "xxxxxxxxxxxxxxxxxxxxxx", // Replace with your Endfield cred cookie value ( get from cookie ) | |
| skGameRole: "xxxxxxxxxxxx", // Replace with your Endfield skGameRole cookie value ( get from cookie ) | |
| platform: "3", | |
| vName: "1.0.0", | |
| accountName: "acc_name" // Replace with a name to identify this account( a simple identifier ) | |
| } | |
| // Add more profiles if needed | |
| ]; | |
| const discord_notify = true; | |
| const myDiscordID = "xxxxxxxxxxxxxxxxxxx"; // Replace with your Discord ID (optional, for pinging) | |
| const discordWebhook = "https://xxxx.discord.com/api/webhooks/xxxxxxxxxxx"; // Replace with your Discord webhook URL | |
| /** Config ends. **/ | |
| const attendanceUrl = 'https://zonai.skport.com/web/v1/game/endfield/attendance'; | |
| async function main() { | |
| const results = await Promise.all(profiles.map(autoClaimFunction)); | |
| if (discord_notify && discordWebhook) { | |
| postWebhook(results); | |
| } | |
| } | |
| function autoClaimFunction({ cred, skGameRole, platform, vName, accountName }) { | |
| console.log(`[${accountName}] Checking credentials and performing check-in...`); | |
| const timestamp = Math.floor(Date.now() / 1000).toString(); | |
| // Attempt to refresh token used for signing. If refresh fails, token will be empty. | |
| let token = ""; | |
| try { | |
| token = refreshToken(cred, platform, vName); | |
| console.log(`[${accountName}] Token refreshed successfully.`); | |
| } catch (e) { | |
| console.error(`[${accountName}] Token refresh failed: ${e.message}`); | |
| // proceed with empty token; API may reject if token required | |
| } | |
| const sign = generateSign('/web/v1/game/endfield/attendance', '', timestamp, token, platform, vName); | |
| const header = { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0', | |
| 'Accept': '*/*', | |
| 'Accept-Language': 'en-US,en;q=0.9', | |
| 'Accept-Encoding': 'gzip, deflate, br, zstd', | |
| 'Referer': 'https://game.skport.com/', | |
| 'Content-Type': 'application/json', | |
| 'sk-language': 'en', | |
| 'sk-game-role': skGameRole, | |
| 'cred': cred, | |
| 'platform': platform, | |
| 'vName': vName, | |
| 'timestamp': timestamp, | |
| 'sign': sign, | |
| 'Origin': 'https://game.skport.com', | |
| 'Connection': 'keep-alive', | |
| 'Sec-Fetch-Dest': 'empty', | |
| 'Sec-Fetch-Mode': 'cors', | |
| 'Sec-Fetch-Site': 'same-site' | |
| }; | |
| const options = { | |
| method: 'POST', | |
| headers: header, | |
| muteHttpExceptions: true, | |
| }; | |
| let result = { | |
| name: accountName, | |
| success: false, | |
| status: "", | |
| rewards: "" | |
| }; | |
| try { | |
| const endfieldResponse = UrlFetchApp.fetch(attendanceUrl, options); | |
| const responseJson = JSON.parse(endfieldResponse.getContentText()); | |
| console.log(`[${accountName}] API Response Code: ${responseJson.code}`); | |
| if (responseJson.code === 0) { | |
| result.success = true; | |
| result.status = "✅ Check-in Successful"; | |
| if (responseJson.data && responseJson.data.awardIds) { | |
| const awards = responseJson.data.awardIds.map(award => { | |
| const resource = responseJson.data.resourceInfoMap ? responseJson.data.resourceInfoMap[award.id] : null; | |
| return resource ? `${resource.name} x${resource.count}` : (award.id || "Unknown Item"); | |
| }).join('\n'); | |
| result.rewards = awards; | |
| } else { | |
| result.rewards = "No detailed reward info."; | |
| } | |
| } else if (responseJson.code === 10001) { | |
| result.success = true; | |
| result.status = "👌 Already Checked In"; | |
| result.rewards = "Nothing to claim"; | |
| } else { | |
| result.success = false; | |
| result.status = `❌ Error (Code: ${responseJson.code})`; | |
| result.rewards = responseJson.message || "Unknown Error"; | |
| } | |
| } catch (error) { | |
| result.success = false; | |
| result.status = "💥 Exception"; | |
| result.rewards = error.message; | |
| console.error(`[${accountName}] Exception: ${error.message}`); | |
| } | |
| return result; | |
| } | |
| function postWebhook(results) { | |
| console.log('Posting to Discord webhook...'); | |
| const allSuccess = results.every(r => r.success); | |
| const hasError = !allSuccess; | |
| const embedColor = allSuccess ? 5763719 : 15548997; // Green or Red | |
| const fields = results.map(r => { | |
| return { | |
| name: `👤 ${r.name}`, | |
| value: `**Status:** ${r.status}\n**Rewards:**\n${r.rewards ? r.rewards : 'None'}`, | |
| inline: true | |
| }; | |
| }); | |
| const payload = { | |
| username: "Endfield Assistant", | |
| avatar_url: "https://pbs.twimg.com/profile_images/1984225639407529984/2_3-HRTS_400x400.jpg", | |
| embeds: [{ | |
| title: "📡 Endfield Daily Check-in Report", | |
| color: embedColor, | |
| fields: fields, | |
| footer: { | |
| text: `Time: ${new Date().toLocaleString('en-US', { timeZone: 'UTC' })} (UTC)`, | |
| icon_url: "https://assets.skport.com/assets/favicon.ico" | |
| } | |
| }] | |
| }; | |
| if (hasError && myDiscordID) { | |
| payload.content = `<@${myDiscordID}> Script encountered an error, please check logs!`; | |
| } | |
| const options = { | |
| method: 'POST', | |
| contentType: 'application/json', | |
| payload: JSON.stringify(payload), | |
| muteHttpExceptions: true | |
| }; | |
| try { | |
| UrlFetchApp.fetch(discordWebhook, options); | |
| } catch (e) { | |
| console.error("Failed to send Discord webhook: " + e.message); | |
| } | |
| } | |
| /** Helper: Refresh token used for signing **/ | |
| function refreshToken(cred, platform, vName) { | |
| const refreshUrl = 'https://zonai.skport.com/web/v1/auth/refresh'; | |
| const header = { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', | |
| 'Accept': 'application/json, text/plain, */*', | |
| 'cred': cred, | |
| 'platform': platform, | |
| 'vName': vName, | |
| 'Origin': 'https://game.skport.com', | |
| 'Referer': 'https://game.skport.com/' | |
| }; | |
| const options = { | |
| method: 'GET', | |
| headers: header, | |
| muteHttpExceptions: true | |
| }; | |
| const response = UrlFetchApp.fetch(refreshUrl, options); | |
| const json = JSON.parse(response.getContentText()); | |
| if (json.code === 0 && json.data && json.data.token) { | |
| return json.data.token; | |
| } else { | |
| throw new Error(`Refresh Failed (Code: ${json.code}, Msg: ${json.message})`); | |
| } | |
| } | |
| /** Signature generation (HMAC-SHA256 then MD5) **/ | |
| function generateSign(path, body, timestamp, token, platform, vName) { | |
| let str = path + body + timestamp; | |
| const headerJson = `{"platform":"${platform}","timestamp":"${timestamp}","dId":"","vName":"${vName}"}`; | |
| str += headerJson; | |
| const hmacBytes = Utilities.computeHmacSha256Signature(str, token || ''); | |
| const hmacHex = bytesToHex(hmacBytes); | |
| const md5Bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, hmacHex); | |
| return bytesToHex(md5Bytes); | |
| } | |
| function bytesToHex(bytes) { | |
| return bytes.map(function(byte) { | |
| return ('0' + (byte & 0xFF).toString(16)).slice(-2); | |
| }).join(''); | |
| } |
It worked before and also stopped working for me now with code=10000 as response. I tried to see if anything is different but could not get the request to work even with the exact same headers. We have to assume they changed something to make auto claiming harder.
Hi All
Looks like something got changed in their API checking.
Earlier, they were not validating timestamp but now if you resend the same API request via curl, you will get an error saying the timestamp is old.
So they definitely changed the API validation and something in the backend.
I checked on multiple accounts via web-login and there seems to be no difference in the curl request.
I suspect they are doing sign validation in the backend now. (This is just a wild guess )
because I didn't see any difference in the API call apar from that param getting regenerated on each call.
It is probably a hash that they are now enforcing strict validation upon.
Feel free to reverse engineer the parms and test with different params.
Will update once I find something.
// code snippet for js to test headers
const profiles = [
{
cred: "xx",
skGameRole: "xx",
platform: "3",
vName: "1.0.0",
accountName: "test"
}
];
const telegram_notify = true;
const myTelegramID = "xx";
const telegramBotToken = "xx:xx";
const attendanceUrl =
"https://zonai.skport.com/web/v1/game/endfield/attendance";
async function main() {
const messages = await Promise.all(profiles.map(autoClaimFunction));
const endfieldResp = messages.join("\n\n");
// if (telegram_notify) {
// await postWebhook(endfieldResp);
// }
}
async function autoClaimFunction({
cred,
skGameRole,
platform,
vName,
accountName
}) {
const headers = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0",
Accept: "*/*",
"Content-Type": "application/json",
"Priority": "u=0",
"Content-Length": "0",
"TE": "trailers",
"sk-language": "en",
"sk-game-role": skGameRole,
cred,
platform,
vName,
timestamp: Math.floor(Date.now() / 1000).toString(),
sign: "4e3d341085e083abea105df7c51e9f0a",
Origin: "https://game.skport.com",
Referer: "https://game.skport.com/"
};
let response = `Daily reward claim for ${accountName}`;
try {
const res = await fetch(attendanceUrl, {
method: "POST",
headers
});
const responseJson = await res.json();
console.log("DEBUG RESPONSE:", responseJson);
if (responseJson.code === 0) {
response += "\nClaim successful!";
} else if (responseJson.code === 10001) {
response += "\nAlready claimed today.";
} else {
response += `\nError: ${responseJson.message}`;
}
} catch (err) {
response += `\nFailed: ${err.message}`;
}
return response;
}
async function postWebhook(data) {
await fetch(
`https://api.telegram.org/bot${telegramBotToken}/sendMessage`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: myTelegramID,
text: data
})
}
);
}
main();
node --inspect endfield_test.js
Thanks
/** --- CONFIGURATION START --- **/
let profiles = [
{
// [REQUIRED] Security Credentials
// You must obtain these from your browser's Network tab (F12) after logging in.
cred: "YOUR_CRED_STRING_HERE",
// The script will automatically refresh this.
token: "",
// [REQUIRED] Game Profile Details
// 'skGameRole' format is usually: "Platform_UserID_Server" (e.g., "3_4760396803_2")
skGameRole: "YOUR_ROLE_ID_HERE",
platform: "3", // Platform ID (3 usually stands for Android/PC)
vName: "1.0.0", // Game Version
accountName: "Account_1" // Nickname for Discord notifications
}
// You can duplicate the block above to add multiple accounts.
];
// Discord Notification Settings
const ENABLE_DISCORD_NOTIFY = true;
const DISCORD_USER_ID = ""; // Paste your User ID here to get pinged on error (e.g., "123456789")
const DISCORD_WEBHOOK_URL = ""; // (Required) Paste your Discord Webhook URL here
/** --- CONFIGURATION END --- **/
// API Endpoints
const URLS = {
refresh: 'https://zonai.skport.com/web/v1/auth/refresh',
attendance: 'https://zonai.skport.com/web/v1/game/endfield/attendance'
};
async function main() {
let results = [];
// Process each profile sequentially
for (let i = 0; i < profiles.length; i++) {
let profile = profiles[i];
console.log(`[${profile.accountName}] Checking credentials and performing check-in...`);
try {
// 1. Attempt to refresh the Token
// This ensures we have a valid key for signing the request and bypasses login CAPTCHA.
const newToken = refreshToken(profile);
profile.token = newToken;
console.log(`[${profile.accountName}] Token refreshed successfully.`);
// 2. Perform Check-in
let claimResult = autoClaimFunction(profile);
results.push(claimResult);
} catch (e) {
console.error(`[${profile.accountName}] Error: ${e.message}`);
results.push({
name: profile.accountName,
success: false,
status: "⛔ Auth/Refresh Failed",
rewards: "Please update your 'cred': " + e.message
});
}
// Sleep for 1 second to avoid rate limiting
Utilities.sleep(1000);
}
if (ENABLE_DISCORD_NOTIFY && DISCORD_WEBHOOK_URL) {
sendDiscordEmbed(results);
}
}
function refreshToken(profile) {
const { cred, platform, vName } = profile;
const header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'cred': cred,
'platform': platform,
'vName': vName,
'Origin': 'https://game.skport.com',
'Referer': 'https://game.skport.com/'
};
const options = {
method: 'GET',
headers: header,
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(URLS.refresh, options);
const json = JSON.parse(response.getContentText());
if (json.code === 0 && json.data && json.data.token) {
return json.data.token;
} else {
// If code is not 0, the cred might be expired.
if (json.code !== 0) {
throw new Error(`Refresh Failed (Code: ${json.code}, Msg: ${json.message})`);
}
return null;
}
}
/**
* Main Check-in Function
*/
function autoClaimFunction(profile) {
const { cred, token, skGameRole, platform, vName, accountName } = profile;
// 1. Prepare Parameters
const timestamp = Math.floor(Date.now() / 1000).toString();
const path = "/web/v1/game/endfield/attendance";
const body = "";
// 2. Generate Signature
const sign = generateSign(path, body, timestamp, token, platform, vName);
const header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'sk-language': 'en_US', // Changed to en_US for English response (if supported), or keep zh_Hant
'sk-game-role': skGameRole,
'cred': cred,
'platform': platform,
'vName': vName,
'timestamp': timestamp,
'sign': sign,
'Origin': 'https://game.skport.com',
'Referer': 'https://game.skport.com/'
};
const options = {
method: 'POST',
headers: header,
muteHttpExceptions: true,
payload: body
};
let result = {
name: accountName,
success: false,
status: "",
rewards: ""
};
try {
const response = UrlFetchApp.fetch(URLS.attendance, options);
const json = JSON.parse(response.getContentText());
console.log(`[${accountName}] API Response Code: ${json.code}`);
if (json.code === 0) {
result.success = true;
result.status = "✅ Check-in Successful";
if (json.data && json.data.awardIds) {
const awards = json.data.awardIds.map(award => {
const resource = json.data.resourceInfoMap ? json.data.resourceInfoMap[award.id] : null;
return resource ? `${resource.name} x${resource.count}` : (award.id || "Unknown Item");
}).join('\n');
result.rewards = awards;
} else {
result.rewards = "No detailed reward info.";
}
} else if (json.code === 10001) {
result.success = true;
result.status = "👌 Already Checked In";
result.rewards = "Nothing to claim";
} else {
result.success = false;
result.status = `❌ Error (Code: ${json.code})`;
result.rewards = json.message || "Unknown Error";
}
} catch (error) {
result.success = false;
result.status = "💥 Exception";
result.rewards = error.message;
console.error(error);
}
return result;
}
/**
* Endfield Signature Algorithm (HMAC-SHA256 -> MD5)
*/
function generateSign(path, body, timestamp, token, platform, vName) {
let str = path + body + timestamp;
const headerJson = `{"platform":"${platform}","timestamp":"${timestamp}","dId":"","vName":"${vName}"}`;
str += headerJson;
// Sign using the token obtained from refresh
const hmacBytes = Utilities.computeHmacSha256Signature(str, token);
const hmacHex = bytesToHex(hmacBytes);
const md5Bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, hmacHex);
return bytesToHex(md5Bytes);
}
/**
* Helper: Convert Byte Array to Hex String
*/
function bytesToHex(bytes) {
return bytes.map(function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
/**
* Send Report to Discord
*/
function sendDiscordEmbed(results) {
const allSuccess = results.every(r => r.success);
const hasError = !allSuccess;
const embedColor = allSuccess ? 5763719 : 15548997; // Green or Red
const fields = results.map(r => {
return {
name: `👤 ${r.name}`,
value: `**Status:** ${r.status}\n**Rewards:**\n${r.rewards ? r.rewards : 'None'}`,
inline: true
};
});
const payload = {
username: "Endfield Assistant",
avatar_url: "https://pbs.twimg.com/profile_images/1984225639407529984/2_3-HRTS_400x400.jpg", // Optional: Add an image URL here
embeds: [{
title: "📡 Endfield Daily Check-in Report",
color: embedColor,
fields: fields,
footer: {
text: `Time: ${new Date().toLocaleString('en-US', { timeZone: 'UTC' })} (UTC)`,
icon_url: "https://assets.skport.com/assets/favicon.ico"
}
}]
};
if (hasError && DISCORD_USER_ID) {
payload.content = `<@${DISCORD_USER_ID}> Script encountered an error, please check logs!`;
}
const options = {
method: 'POST',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
} catch (e) {
console.error("Failed to send Discord webhook: " + e.message);
}
}I've already claimed today's rewards, so I couldn't test the unclaimed scenario. I have only tested the script within script google.
I've already claimed today's rewards, so I couldn't test the unclaimed scenario. I have only tested the script within script google.
I can verify this did indeed work, looks like they started requiring a proper sign header. Thanks!
I've already claimed today's rewards, so I couldn't test the unclaimed scenario. I have only tested the script within script google.
This helps a lot, I appreciate it.
Thanks for the info, Wondering how you figured out the combination for the sign hashing logic used by the website.
Anyways thanks for your effort.
I've updated the script from using your code snippet as a reference, and also took some nice points related to Discord webhook and message layout.
Tested on both Telegram + Discord with multiple accounts in profiles array.
@cptmacp thanks for the script. <3
Do you happen to know what to change so the name in the discord message would be a user ping?
Before I could just change the variable of username to "<@" + myDiscordID + ">.
But I cant figure it out where/how to change it in the updated version. :) As it just gets out as plain text instead of the ping in Discord.
But it should be like this,, if possible. :D
* You will see `cred` and `skGameRole`![]()
I can't get skGameRole , in fact I don't even have any cookie that follows the "Platform_UserID_Server" format. That tampersmonkey script also doesn't work for me @cptmacp .
* You will see `cred` and `skGameRole`![]()
I can't get
skGameRole, in fact I don't even have any cookie that follows the "Platform_UserID_Server" format. That tampersmonkey script also doesn't work for me @cptmacp .
did you enable, "Allow userscript"
After installing just open the link "https://game.skport.com/endfield/sign-in?header=0&hg_media=skport&hg_link_campaign=tools"
and you should see the popup on top right side
did you enable, "Allow userscript"
After installing just open the link "https://game.skport.com/endfield/sign-in?header=0&hg_media=skport&hg_link_campaign=tools"
and you should see the popup on top right side
Seems like it fixed itself after reinstalling the script.
@cptmacp thanks for the script. <3 Do you happen to know what to change so the name in the discord message would be a user ping? Before I could just change the variable of username to
"<@" + myDiscordID + ">. But I cant figure it out where/how to change it in the updated version. :) As it just gets out as plain text instead of the ping in Discord.
Hey, the ping will only happen in case of
https://gist.github.com/cptmacp/1e9a9f20f69c113a0828fea8d13cb34c#file-endfield_discord-js-L149
hasError is populated, it wont ping in normal scenario.
@cptmacp thanks for the script. <3 Do you happen to know what to change so the name in the discord message would be a user ping? Before I could just change the variable of username to
"<@" + myDiscordID + ">. But I cant figure it out where/how to change it in the updated version. :) As it just gets out as plain text instead of the ping in Discord.
I can send what I looked up to change the message back to the old written text instead of the new layout since I preferred the old one.







hello, I just want to ask. I havent get a successful request since the reward is manually claimed however I only got
Error: Request exception (code=10000) as response which isnt exactly the code of Already claimed as you specified as 10001. I wonder what's wrong with the script or else. Here is a little bit modified version I used