Fairy is a Go library for fetching, enriching, and calculating Zenless Zone Zero player game profiles via the EnkaNetwork API. Just like the AI assistant from New Eridu, it handles all the heavy lifting — mapping raw game IDs to localized names, building asset URLs, and computing final combat stats from scratch.
The EnkaNetwork API returns player profiles as raw data — agents, W-Engines, and Drive Discs are represented by internal numeric IDs, stat values have no names, and there are no image URLs. To build anything user-facing, you'd need to maintain your own mapping tables, host localization files, implement stat calculations, and keep up with every game patch.
Fairy eliminates this entire layer. It takes a single UID, fetches the raw profile from Enka, and returns a fully enriched model — human-readable names in 13 languages, ready-to-use asset URLs, computed final stats, and Drive Disc roll analysis. One function call, zero boilerplate.
The comparison below shows what this looks like in practice: a raw API response on the left versus the enriched output Fairy produces on the right.
| ENKANETWORK API RESPONSE | FAIRY ENRICHED OUTPUT |
|---|---|
{
"Id": 1511,
"Level": 60,
"Exp": 0,
"PromotionLevel": 6,
"TalentLevel": 0,
"SkinId": 3115111,
"UpgradeId": 0,
"CoreSkillEnhancement": 6,
"Weapon": {
"Id": 15388,
"Level": 60,
"StarMark": 1,
"BreakLevel": 6
},
"EquippedList": [{
"Slot": 1,
"Equipment": {
"Id": 33041,
"Level": 15,
"MainPropertyList": [{
"PropertyId": 11103,
"PropertyValue": 550
}],
"RandomPropertyList": [
{"PropertyId": 12103, "PropertyValue": 19},
{"PropertyId": 31203, "PropertyValue": 9},
{"PropertyId": 11102, "PropertyValue": 300},
{"PropertyId": 12102, "PropertyValue": 300}
]
}
}]
} |
{
"name": "Nangong Yu",
"level": 60,
"rarity": "S",
"attribute_name": "Ether",
"specialty_name": "Stun",
"w_engine": {
"name": "Neon Fantasies",
"level": 60,
"modification": 1,
"rarity": "S",
"main_stat": {
"name": "Base ATK",
"value": 713
}
},
"drive_discs": [{
"slot": 1,
"set_name": "Phaethon's Melody",
"level": 15,
"main_stat": {"name": "HP", "value": 2200},
"sub_stats": [
{"name": "ATK", "value": 38, "rolls": 2},
{"name": "Anomaly Prof","value": 27, "rolls": 3},
{"name": "Percent ATK", "value": 0.09, "rolls": 3}
]
}],
"stats": {
"hp": 11188, "atk": 2866,
"crit_rate": 0.074, "crit_dmg": 0.548,
"pen_ratio": 0.24, "energy_regen": 1.2
}
} |
Note
The JSON examples above are simplified and shortened to highlight the key differences. The actual API responses and Fairy's models contain significantly more data.
-
🧮 Stat Calculation — Computes final combat stats (HP, ATK, DEF, CRIT, PEN, Energy Regen, and more) by combining agent base values, W-Engine scaling, Drive Disc main/sub stats, and set bonuses. All percentage stats are stored as decimals internally and can be formatted for display with a single call.
-
🎨 UI-Ready Stat Breakdown —
FormattedUIStats()splits every stat into Base, Added, and Total components, pre-formatted as strings — matching exactly what players see in the in-game stat panel.Stats.Formatted()gives you a simpler flat view when you don't need the breakdown. -
🔍 Drive Disc Analysis —
SubStatTotals()aggregates sub-stats across all six discs, grouping by property and summing values and rolls.CountEffectiveRolls()counts how many rolls landed on the stats you care about — available on both the agent (all discs) and individual disc level. -
🌍 13 Languages — Every string in the output — agent names, skill descriptions, stat labels, W-Engine passives, set bonus text, titles, and badges — is fully localized. Fetch raw data once, then call
Localize()to produce the same profile in any supported language without extra network calls. -
🖼️ Asset URLs — Generates ready-to-use image URLs for agent splash arts, skins, W-Engine icons, Drive Disc icons, profile avatars, namecards, and badges. No manual URL construction needed.
-
📦 Zero-Config Data — All game metadata (stat scaling tables, localization strings, item definitions) is embedded in the binary via
go:embed. No external files, no database, no CDN — justgo getand start building. -
🧩 Flexible Client — Functional options let you configure the default language, swap in a custom
MetadataStoreimplementation, and pass through HTTP settings (timeouts, retries, User-Agent, caching) to the underlyingenkanetwork-goclient.
Requires Go 1.22+
go get github.com/kirinyoku/fairyThe easiest way to get started is with the GetProfile function. It uses a default client with English localization and built-in game data.
package main
import (
"context"
"fmt"
"log"
"github.com/kirinyoku/fairy"
)
func main() {
profile, err := fairy.GetProfile(context.Background(), "1504687050")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Player: %s (Inter-Knot Level: %d)\n", profile.Nickname, profile.InterknotLevel)
for _, agent := range profile.Agents {
fmt.Printf(" • %s Lv.%d %s %s\n", agent.Name, agent.Level, agent.AttributeName, agent.SpecialtyName)
}
}Use NewClient to set a different default language, configure HTTP settings, retries, caching, or a custom User-Agent header (required by Enka.Network).
client, err := fairy.NewClient(
// Change default language for all requests
fairy.WithDefaultLang(fairy.LangJA),
// Configure the underlying `github.com/kirinyoku/enkanetwork-go/client/zzz` client
fairy.WithEnkaOptions(zzz.Options{
UserAgent: "MyApp/1.0 (github.com/you/myapp)",
HTTPClient: &http.Client{Timeout: 10 * time.Second},
Retry: &zzz.RetryOptions{MaxAttempts: 2, Delay: 2 * time.Second},
Cache: myCacheInstance,
}),
)You can also override the language on a per-request basis without recreating the client:
// Use the shared global client, but respond in Korean for this call
profile, err := fairy.GetProfileWithLang(ctx, "1504687050", fairy.LangKO)FormattedUIStats() returns every stat split into Base, Added, and Total, formatted exactly as they appear in the in-game stat panel.
agent := profile.Agents[0]
ui := agent.FormattedUIStats()
fmt.Printf("HP: %s (base %s + %s)\n", ui.HP.Total, ui.HP.Base, ui.HP.Added)
fmt.Printf("ATK: %s (base %s + %s)\n", ui.ATK.Total, ui.ATK.Base, ui.ATK.Added)
fmt.Printf("CRIT Rate: %s (base %s + %s)\n", ui.CritRate.Total, ui.CritRate.Base, ui.CritRate.Added)
fmt.Printf("CRIT DMG: %s (base %s + %s)\n", ui.CritDMG.Total, ui.CritDMG.Base, ui.CritDMG.Added)
fmt.Printf("PEN Ratio: %s (base %s + %s)\n", ui.PenRatio.Total, ui.PenRatio.Base, ui.PenRatio.Added)Measure how many sub-stat rolls landed on stats that actually matter for your agent.
agent := profile.Agents[0]
// Count effective rolls for an Attack agent:
usefulRolls := agent.CountEffectiveRolls(
fairy.PropCritRate,
fairy.PropCritDMG,
fairy.PropATKPercent,
)
fmt.Printf("Effective rolls: %d\n", usefulRolls)
// Full sub-stat breakdown across all 6 discs, grouped and summed:
for _, stat := range agent.SubStatTotals() {
fmt.Printf(" %-20s %s (×%d rolls)\n", stat.Name, stat.DisplayValue(), stat.Rolls)
}To display the same profile in multiple languages, fetch the raw data once and localize it in memory — no extra network calls needed:
// 1. Fetch the raw data from the API just once
rawProfile, err := client.GetRawProfile(ctx, "1504687050")
if err != nil {
log.Fatal(err)
}
// 2. Localize the same raw data into different languages without extra network calls
enProfile, _ := client.Localize(rawProfile, fairy.LangEN)
jaProfile, _ := client.Localize(rawProfile, fairy.LangJA)| Language | Language |
|---|---|
| 🇬🇧 English | 🇰🇷 Korean |
| 🇷🇺 Russian | 🇵🇹 Portuguese |
| 🇩🇪 German | 🇹🇭 Thai |
| 🇪🇸 Spanish | 🇻🇳 Vietnamese |
| 🇫🇷 French | 🇨🇳 Chinese (Simplified) |
| 🇮🇩 Indonesian | 🇹🇼 Chinese (Traditional) |
| 🇯🇵 Japanese |
Licensed under the MIT License.
