Claude-skill-registry eve-esi-api
EVE Online ESI API patterns and zkillboard integration. Covers ESI endpoints, authentication, data models, zkillboard RedisQ listener, caching strategies, and EVE-specific IDs (characters, corporations, alliances, ships, systems, regions). Use when working with killmail data, fetching EVE universe info, or implementing ESI calls.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/eve-esi-api" ~/.claude/skills/majiayu000-claude-skill-registry-eve-esi-api && rm -rf "$T"
manifest:
skills/data/eve-esi-api/SKILL.mdsource content
EVE Online ESI API Guidelines
Purpose
Patterns for interacting with the EVE Online ESI API and zkillboard data feeds in this project.
When to Use
- Fetching data from ESI (characters, systems, ships, etc.)
- Processing killmail data from zkillboard
- Implementing ESI authentication (SSO)
- Caching EVE universe data
- Working with EVE-specific IDs and data structures
Key URLs and Endpoints
| Service | Base URL | Purpose |
|---|---|---|
| ESI | | Official EVE API |
| zkillboard RedisQ | | Real-time killmail feed |
| zkillboard | | Killmail browser |
| Fuzzwork | | Third-party celestial data |
| EVE SSO | | OAuth authentication |
| EVE Images | | Character/ship/alliance icons |
ESI Client Pattern
Basic Structure
pub struct EsiClient { client: Client, // reqwest::Client } impl EsiClient { pub fn new() -> Self { EsiClient { client: Client::new(), } } async fn fetch<T: for<'de> Deserialize<'de>>( &self, path: &str, ) -> Result<T, Box<dyn Error + Send + Sync>> { let url = format!("{}{}", ESI_URL, path); let response = self.client.get(&url).send().await?; if !response.status().is_success() { return Err(format!("ESI API returned status: {}", response.status()).into()); } let data: T = response.json().await?; Ok(data) } }
Common ESI Endpoints
// Get system info pub async fn get_system(&self, system_id: u32) -> Result<System, Error> { self.fetch(&format!("universe/systems/{}/", system_id)).await } // Get ship/item group pub async fn get_ship_group_id(&self, type_id: u32) -> Result<u32, Error> { #[derive(Deserialize)] struct EsiType { group_id: u32 } let type_info: EsiType = self.fetch(&format!("universe/types/{}/", type_id)).await?; Ok(type_info.group_id) } // Get names (POST endpoint) pub async fn get_name(&self, id: u64) -> Result<String, Error> { let names: Vec<EsiName> = self.client .post(format!("{}universe/names/", ESI_URL)) .json(&[id]) // Array of IDs .send() .await? .json() .await?; Ok(names.into_iter().next()?.name) } // Get character affiliation (POST endpoint) pub async fn get_character_affiliation(&self, character_id: u64) -> Result<(u64, Option<u64>), Error> // (corp_id, alliance_id) { let affiliations: Vec<Affiliation> = self.client .post(format!("{}characters/affiliation/", ESI_URL)) .json(&[character_id]) .send() .await? .json() .await?; let a = affiliations.into_iter().next()?; Ok((a.corporation_id, a.alliance_id)) }
zkillboard RedisQ
Listener Pattern
const REDISQ_URL: &str = "https://zkillredisq.stream/listen.php"; pub struct RedisQListener { client: Client, url: String, } impl RedisQListener { pub fn new(queue_id: &str) -> Self { // Each listener needs a unique queue ID let url = format!("{}?queueID={}", REDISQ_URL, queue_id); RedisQListener { client: Client::new(), url, } } pub async fn listen(&self) -> Result<Option<ZkData>, Error> { let response = self.client.get(&self.url) .timeout(Duration::from_secs(60)) // Long-polling .send() .await?; let wrapper: RedisQResponse = response.json().await?; Ok(wrapper.package) // None if no new killmails } }
Main Loop Pattern
loop { match listener.listen().await { Ok(Some(zk_data)) => { info!("[Kill: {}] Received", zk_data.kill_id); process_killmail(&zk_data).await; sleep(Duration::from_secs(1)).await; } Ok(None) => { // No new data, RedisQ timed out sleep(Duration::from_secs(1)).await; } Err(e) => { error!("Error: {}", e); sleep(Duration::from_secs(5)).await; // Backoff on error } } }
Data Models
Killmail Structure
#[derive(Debug, Deserialize)] pub struct ZkData { pub kill_id: u64, pub killmail: KillmailData, pub zkb: ZkbMeta, } #[derive(Debug, Deserialize)] pub struct KillmailData { pub killmail_id: u64, pub killmail_time: String, // RFC3339 format pub solar_system_id: u32, pub victim: Victim, pub attackers: Vec<Attacker>, } #[derive(Debug, Deserialize)] pub struct Victim { pub ship_type_id: u32, pub character_id: Option<u64>, pub corporation_id: Option<u64>, pub alliance_id: Option<u64>, pub position: Option<Position>, } #[derive(Debug, Deserialize)] pub struct Attacker { pub ship_type_id: Option<u32>, pub weapon_type_id: Option<u32>, pub character_id: Option<u64>, pub corporation_id: Option<u64>, pub alliance_id: Option<u64>, pub final_blow: bool, } #[derive(Debug, Deserialize)] pub struct ZkbMeta { pub total_value: f64, pub location_id: Option<u64>, pub esi: String, // URL to full ESI killmail }
System Data
#[derive(Debug, Clone, Serialize, Deserialize)] pub struct System { pub id: u32, pub name: String, pub security_status: f64, pub region_id: u32, pub region: String, pub x: f64, pub y: f64, pub z: f64, }
Caching Strategy
Cache ESI Data Locally
// Check cache first { let systems = app_state.systems.read().unwrap(); if let Some(system) = systems.get(&system_id) { return Some(system.clone()); } } // Fetch and cache match esi_client.get_system(system_id).await { Ok(system) => { let mut systems = app_state.systems.write().unwrap(); systems.insert(system_id, system.clone()); save_systems(&systems); // Persist to disk Some(system) } Err(e) => { warn!("Failed to fetch system {}: {}", system_id, e); None } }
Using Moka for In-Memory Cache
use moka::future::Cache; pub struct AppState { // Time-based expiry cache pub celestial_cache: Cache<u32, Arc<Celestial>>, } // Check cache if let Some(celestial) = app_state.celestial_cache.get(&system_id) { return Some(celestial); } // Fetch and cache let celestial = Arc::new(fetch_celestial().await?); app_state.celestial_cache.insert(system_id, celestial.clone()).await;
EVE-Specific Knowledge
Important IDs
| Type | Example IDs | Notes |
|---|---|---|
| Ship Groups | 485 (Dread), 659 (Super), 30 (Titan) | Used for filtering |
| Regions | 10000030 (Devoid), 10000012 (Curse) | Universe IDs |
| Systems | 30000142 (Jita), 30002086 (Turnur) | Solar system IDs |
Ship Group Priorities
const SHIP_GROUP_PRIORITY: &[u32] = &[ 30, // Titan 659, // Supercarrier 4594, // Lancer 485, // Dreadnought 1538, // FAX 547, // Carrier 883, // Capital Industrial Ship 902, // Jump Freighter 513, // Freighter ];
Image URLs
// Alliance logo fn alliance_icon(id: u64) -> String { format!("https://images.evetech.net/alliances/{}/logo?size=64", id) } // Corporation logo fn corp_icon(id: u64) -> String { format!("https://images.evetech.net/corporations/{}/logo?size=64", id) } // Ship/item icon fn ship_icon(id: u32) -> String { format!("https://images.evetech.net/types/{}/icon?size=64", id) } // Character portrait fn character_portrait(id: u64) -> String { format!("https://images.evetech.net/characters/{}/portrait?size=64", id) }
External Links
// zkillboard fn zkb_kill(id: u64) -> String { format!("https://zkillboard.com/kill/{}/", id) } fn zkb_character(id: u64) -> String { format!("https://zkillboard.com/character/{}/", id) } fn zkb_corp(id: u64) -> String { format!("https://zkillboard.com/corporation/{}/", id) } // Dotlan fn dotlan_system(id: u32) -> String { format!("http://evemaps.dotlan.net/system/{}", id) } fn dotlan_region(id: u32) -> String { format!("http://evemaps.dotlan.net/region/{}", id) } // EVE Tools battle report fn br_link(system_id: u32, timestamp: &str) -> String { format!("https://br.evetools.org/related/{}/{}", system_id, timestamp) }
SSO Authentication
OAuth2 Flow
const ESI_AUTH_URL: &str = "https://login.eveonline.com/v2/oauth/token"; const ESI_VERIFY_URL: &str = "https://login.eveonline.com/oauth/verify"; pub async fn exchange_code_for_token( &self, code: &str, client_id: &str, client_secret: &str, ) -> Result<EveAuthToken, Error> { // 1. Exchange authorization code for tokens let params = [("grant_type", "authorization_code"), ("code", code)]; let auth_response: Value = self.client .post(ESI_AUTH_URL) .basic_auth(client_id, Some(client_secret)) .form(¶ms) .send() .await? .json() .await?; let access_token = auth_response["access_token"].as_str()?.to_string(); let refresh_token = auth_response["refresh_token"].as_str()?.to_string(); // 2. Verify token and get character info let verify_response: Value = self.client .get(ESI_VERIFY_URL) .bearer_auth(&access_token) .send() .await? .json() .await?; let character_id = verify_response["CharacterID"].as_u64()?; let character_name = verify_response["CharacterName"].as_str()?.to_string(); Ok(EveAuthToken { character_id, character_name, access_token, refresh_token, ... }) }
Authenticated Requests
pub async fn get_contacts( &self, entity_id: u64, token: &str, endpoint: &str, // "characters", "corporations", or "alliances" ) -> Result<Vec<StandingContact>, Error> { let url = format!("{}{}/{}/contacts/", ESI_URL, endpoint, entity_id); let contacts: Vec<StandingContact> = self.client .get(&url) .bearer_auth(token) // Include access token .send() .await? .json() .await?; Ok(contacts) }
Reference Files
- resources/endpoints.md - Complete ESI endpoint reference
- resources/ids.md - Common EVE IDs lookup table