Abstract#
This article presents the design and implementation of akef-checkin, an automated daily check-in system for Arknights Endfield (SKport platform). The system employs a full OAuth 2.0 authorization flow with HMAC-SHA256 request signing to authenticate against Hypergryph and SKport APIs, eliminating the need for manual token refresh. Deployed as a serverless workflow via GitHub Actions, the system executes daily at 16:00 UTC with multi-account support and Discord webhook notifications. We detail the cryptographic signing mechanism, the four-step authentication pipeline, and the deployment architecture.
1 Introduction#
Daily check-in systems for live-service games typically require authenticated API requests to claim rewards. The Arknights Endfield platform (operated by SKport/Hypergryph)12 implements a multi-layer authentication scheme involving:
- Long-lived account token (
ACCOUNT_TOKEN) — stored by user, valid for weeks to months - Short-lived OAuth code — obtained via
POST /user/oauth2/v2/grant - Session credential (
cred) — generated viaPOST /user/auth/generate_cred_by_code - Signing token (
signToken) — retrieved viaGET /auth/refresh - Game role identifier — resolved via
GET /game/player/binding
Traditional approaches store short-lived tokens, requiring frequent manual updates. Our system regenerates all transient credentials on every execution using only the long-lived ACCOUNT_TOKEN, achieving zero-maintenance operation3.
2 System Architecture#
2.1 High-Level Overview#
The system comprises four modular components:
| Component | File | Responsibility |
|---|---|---|
| Entry Point | src/index.ts | Orchestration, multi-account iteration, result aggregation |
| Authentication | src/auth.ts | Four-step OAuth flow, cryptographic signing |
| Notification | src/discord.ts | Discord webhook delivery |
| Configuration | src/constants.ts | API endpoints, headers, platform constants |
2.2 Execution Pipeline#
flowchart TD
A[Start: Read SKPORT_TOKENS] --> B{For each account}
B --> C[Step 1: Get OAuth Code]
C --> D[Step 2: Generate Cred]
D --> E[Step 3: Get Sign Token]
E --> F[Step 4: Get Game Role]
F --> G[Sign Attendance Request]
G --> H[POST /game/endfield/attendance]
H --> I{Response Code}
I -->|0| J[Success: Parse Rewards]
I -->|1001, 10001| K[Already Signed In]
I -->|10002| L[Token Expired]
I -->|Other| M[Failure: Log Error]
J --> N[Aggregate Result]
K --> N
L --> N
M --> N
N --> O{More Accounts?}
O -->|Yes| B
O -->|No| P[Send Discord Webhook]
P --> Q[Exit]
style A fill:#f97316,color:#fff
style Q fill:#22c55e,color:#fff
style J fill:#3b82f6,color:#fff
style K fill:#3b82f6,color:#fff
style L fill:#f97316,color:#fff
style M fill:#ef4444,color:#fff
3 Authentication Flow#
3.1 Sequence Diagram#
The complete authentication sequence involves two identity providers: Hypergryph (as.gryphline.com) for OAuth grant and SKport (zonai.skport.com) for game-specific credentials.
sequenceDiagram
participant Script
participant Gryphline as as.gryphline.com
participant SKport as zonai.skport.com
Script->>Gryphline: POST /user/oauth2/v2/grant<br/>{token: ACCOUNT_TOKEN, appCode, type: 0}
Gryphline-->>Script: {code: "oauth_code"} (one-time use)
Script->>SKport: POST /web/v1/user/auth/generate_cred_by_code<br/>{code: oauth_code, kind: 1}
SKport-->>Script: {cred: "session_credential", token: "..."}
Script->>SKport: GET /web/v1/auth/refresh<br/>(cred, timestamp, platform, vName)
SKport-->>Script: {token: "sign_token"}
Script->>SKport: GET /api/v1/game/player/binding<br/>(cred + signed request)
SKport-->>Script: {list: [{appCode: "endfield", bindingList: [...]}]}
Script->>SKport: POST /web/v1/game/endfield/attendance<br/>(cred + gameRole + signed request)
SKport-->>Script: {code: 0, data: {signInCount, awardIds, resourceInfoMap}}
3.2 Cryptographic Signing (V2)#
All signed requests use the V2 signature algorithm: HMAC-SHA256 followed by MD5.
export function generateSignV2(
path: string,
timestamp: string,
platform: string,
vName: string,
salt: string
): string {
const headerJson = JSON.stringify({ platform, timestamp, dId: "", vName });
const s = `${path}${timestamp}${headerJson}`;
const hmac = crypto.createHmac("sha256", salt).update(s).digest("hex");
return crypto.createHash("md5").update(hmac).digest("hex");
}typescriptMathematical formulation:
Where:
- denotes string concatenation
- (per-session secret)
- = API endpoint path (e.g.,
/web/v1/game/endfield/attendance) - = Unix epoch seconds as string
Note
The double-hash construction (HMAC-SHA256 → MD5) provides defense-in-depth: even if MD5 collision resistance is compromised, the HMAC-SHA256 layer preserves integrity assuming the salt remains secret.
3.3 Request Structure#
Each signed request includes the following headers:
| Header | Value |
|---|---|
cred | Session credential from Step 2 |
platform | "3" (constant) |
vName | "1.2.0" (client version) |
timestamp | Current Unix timestamp (seconds) |
sign | V2 signature (hex) |
sk-game-role | Game role identifier (format: 3_{roleId}_{serverId}) |
sk-language | Locale code (en, ja, zh_Hans, etc.) |
dId | Empty string (device ID placeholder) |
4 Implementation Details#
4.1 Multi-Account Processing#
The entry point processes accounts sequentially with a 2-second delay between requests to avoid rate limiting:
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
const result = await performCheckin(account);
results.push(result);
if (i < accounts.length - 1) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}typescript4.2 Response Parsing#
On successful check-in (HTTP 200, code === 0), the response contains reward information:
interface AttendanceResponse {
code: number;
message?: string;
data?: {
signInCount?: number;
awardIds?: Array<{ id: string }>;
resourceInfoMap?: Record<string, { name: string; count: number }>;
};
}typescriptRewards are mapped by awardIds → resourceInfoMap and formatted as ItemName xCount.
4.3 Error Handling#
The system categorizes responses into four classes:
| Code / Condition | Category | Action |
|---|---|---|
code === 0 | Success | Parse rewards, report day count |
code ∈ {1001, 10001} or “already” in message | Already Signed In | Report success (idempotent) |
code === 10002 | Token Expired | Alert user to update ACCOUNT_TOKEN |
| Other / Network Error | Failure | Log error, continue to next account |
5 Deployment Architecture#
5.1 GitHub Actions Workflow#
The system runs serverlessly via GitHub Actions4 on ubuntu-latest with Bun runtime:
name: Endfield Auto Check-in
on:
schedule:
- cron: '0 16 * * *' # Daily at 16:00 UTC (00:00 WIB)
workflow_dispatch:
jobs:
checkin:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun run src/index.ts
env:
SKPORT_TOKENS: ${{ secrets.SKPORT_TOKENS }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}yamlSchedule interpretation:
0 16 * * *→ Minute 0, Hour 16 (UTC) → Midnight WIB (UTC+7)workflow_dispatchenables manual triggering via GitHub UI
5.2 Secrets Management#
Two repository secrets are required:
| Secret | Required | Format |
|---|---|---|
SKPORT_TOKENS | Yes | JSON array of account objects |
DISCORD_WEBHOOK_URL | No | Discord webhook URL |
Account object schema:
{
"accountToken": "string (from Network tab)",
"accountName": "string (display name)",
"language": "string (optional, default: en)"
}json6 Token Acquisition Procedure#
The ACCOUNT_TOKEN is not visible in browser cookies. It must be extracted from the OAuth grant request:
- Navigate to
https://game.skport.com/endfield/sign-inwhile logged in - Open DevTools (F12) → Network tab
- Filter for
grant - Reload page (F5)
- Select
POST https://as.gryphline.com/user/oauth2/v2/grant - Open Payload tab (not Response)
- Copy
tokenvalue from JSON body:
json{ "token": "ACCOUNT_TOKEN_HERE", "appCode": "6eb76d4e13aa36e6", "type": 0 }
Important
The ACCOUNT_TOKEN is long-lived (weeks to months). The script regenerates all short-lived credentials (cred, signToken, gameRole) on every run, so token rotation is rarely needed.
7 Local Development#
7.1 Prerequisites#
- Bun ↗ v1.0+
7.2 Configuration#
cp .env.example .env
# Edit .env with your ACCOUNT_TOKENbash7.3 Execution#
bun run startbashExpected output:
Starting Arknights Endfield Auto Check-in...
Processing check-in for account: MyAccount...
[1/4] Getting OAuth code...
[2/4] Generating cred...
[3/4] Getting sign token...
[4/4] Getting player binding (game role)...
Auth complete! Game role: 3_12345678_9
**Endfield:** Check-in for `MyAccount`
✅ Success!
Day: 15
Rewards: Credit x1000, Synthetic Fuel x5
Sending webhook to Discord...
Done!plaintext8 Security Analysis#
8.1 Threat Model#
| Asset | Threat | Mitigation |
|---|---|---|
ACCOUNT_TOKEN | Exfiltration via logs | Never logged; only used in memory |
signToken | Replay attacks | Bound to timestamp; expires quickly |
cred | Session hijacking | Short-lived; regenerated per run |
| Discord webhook | Spam/abuse | Optional; rate-limited by Discord |
8.2 Cryptographic Properties#
The V2 signature5 provides:
- Integrity: Any modification to path, timestamp, or headers invalidates signature
- Replay resistance: Timestamp inclusion limits window to ~1 second (server-side validation)
- Key separation:
signTokenderived per-session, not reusable across accounts
8.3 Operational Security#
- No persistent storage of credentials
- All secrets injected via GitHub Actions encrypted secrets
- Network requests use HTTPS with certificate validation (default in
fetch) - User-Agent mimics legitimate browser (Firefox 147)
9 Related Work#
| Project | Platform | Approach |
|---|---|---|
akef-checkin (this work) | Arknights Endfield (SKport) | Full OAuth flow regeneration |
hoyolab-auto-checkin | Genshin Impact / Star Rail | Cookie-based with manual refresh |
arknights-auto-sign | Arknights (Global/CN) | Token caching with TTL |
Our approach differs by eliminating token caching entirely — every execution performs a fresh OAuth grant, trading ~3 extra HTTP requests for zero-maintenance operation.
10 Conclusion#
The akef-checkin system demonstrates a practical application of OAuth 2.0 and request signing for game automation. Key contributions:
- Zero-maintenance authentication — single long-lived token generates all ephemeral credentials
- Cryptographically sound signing — HMAC-SHA256 + MD5 V2 algorithm with per-session keys
- Serverless deployment — GitHub Actions cron with Bun runtime, no infrastructure management
- Multi-account support — sequential processing with rate-limit awareness
- Observability — structured logging and Discord notifications
Future work includes: exponential backoff for transient failures, Prometheus metrics export, and support for additional SKport game titles.
Appendix A: API Endpoint Reference#
| Constant | URL |
|---|---|
GRANT | https://as.gryphline.com/user/oauth2/v2/grant |
GENERATE_CRED | https://zonai.skport.com/web/v1/user/auth/generate_cred_by_code |
REFRESH_TOKEN | https://zonai.skport.com/web/v1/auth/refresh |
BINDING | https://zonai.skport.com/api/v1/game/player/binding |
ATTENDANCE | https://zonai.skport.com/web/v1/game/endfield/attendance |
Appendix B: Type Definitions#
interface AccountConfig {
accountToken: string;
accountName: string;
language?: string;
}
interface AuthResult {
cred: string;
signToken: string;
gameRole: string;
}
interface ResourceInfo {
name: string;
count: number;
}
interface AttendanceData {
signInCount?: number;
awardIds?: Array<{ id: string }>;
resourceInfoMap?: Record<string, ResourceInfo>;
}typescriptReferences#
This article documents the akef-checkin repository as of version 1.0.0. Source code available at the project repository.
Footnotes#
-
Hypergryph OAuth 2.0 Implementation,
as.gryphline.com(reverse-engineered) ↩ -
SKport Web API v1,
zonai.skport.com(reverse-engineered) ↩ -
RFC 6749 — The OAuth 2.0 Authorization Framework ↩
-
GitHub Actions Documentation — Scheduled Events (
on.schedule) ↩ -
RFC 2104 — HMAC: Keyed-Hashing for Message Authentication ↩