Skip to Content

Multi-Sided Base Card API Matching

Date2026-04-14
StatusAccepted
Decision Makers@benjaminW78

Context and Problem Statement

Starting from Spark of Rebellion, SWU Base cards come with associated token cards (Experience, Shield, Credit, etc.). TCGPlayer lists each base+token combination as a separate product using a // TokenName suffix:

  • Imperial Command Complex // Credit
  • Imperial Command Complex // Experience
  • Imperial Command Complex // Shield

The official SWU API (admin.starwarsunlimited.com) names the same card without any suffix — just Imperial Command Complex. The importer’s findApiCardMatch method looks up TCG CSV product names in the API index after normalization. Because "imperial command complex // credit" does not equal "imperial command complex", the lookup fails and the product gets no translations.

Additionally, the hand-rolled normalizeCardName (lowercase + strip diacritics) was too narrow — it missed curly apostrophes (U+2019), smart quotes (U+201C/U+201D), and Unicode ellipsis (U+2026), causing ~92 additional matching failures for cards like Grievous\u2019s Secret Weapon and Jar Jar Binks - Mesa Propose\u2026.

295 // products across 8 expansions and 9 distinct token suffixes (Experience, Shield, Credit, X-Wing, TIE Fighter, Clone Trooper, Battle Droid, Force, Spy) were affected, plus ~92 products from unicode punctuation mismatches.

Decision Drivers

  • The // suffix is a TCGPlayer naming convention, not part of the official card name — it must not block API matching
  • Each token variant must remain a separate product (they are separate TCGPlayer products with distinct IDs and images)
  • The base card’s attributes (type, aspects, hp, description, rarity, artist, images) don’t apply to token products — only translations should come from the API match
  • A broader card-number-based fallback was investigated but rejected due to card number collisions in the SWU API (e.g., sor:1 maps to both “Director Krennic” and “Experience” token — 7,980 API cards collapse to 6,450 unique expansion:number pairs)

Considered Options

  1. // suffix stripping + baseSlugify normalization + apiEnrichment scoping — Strip // for lookup, use baseSlugify for normalization, scope API data to translations-only for // matches
  2. Card-number-based fallback — Build a secondary API index by expansion:cardNumber and fall back to it on name miss
  3. Normalize // out of all names globally — Strip // ... from card names in normalizeCardName() so the index key never includes it

Decision Outcome

Chosen option: Option 1, because:

  • baseSlugify is already used everywhere in the importer (via this.slugify()), strips all non-alphanumeric chars, and handles unicode punctuation in one shot. Verified zero collisions across all 2,320 distinct SWU product names.
  • The // fallback only activates for products that already failed initial lookup AND contain //
  • The apiEnrichment scoping ensures // cards only get translations from the API match, keeping all other attributes from TCG CSV — preventing the base card’s type/hp/description from overwriting token-specific data

Option 2 was rejected because the SWU API assigns the same card number to different cards (leaders and their tokens share numbers), making card-number matching unreliable.

Option 3 was rejected because the // is semantically meaningful in the product display name and shouldn’t be stripped from index keys globally.

Implementation

Logic Flow

Impacted Files

FileChange
packages/games/game-importer/src/swu-importer.tsAdded stripDoubleFaceSuffix(), // fallback lookup, apiEnrichment scoping, replaced normalizeCardName with baseSlugify
packages/games/game-importer/src/swu/tcg-powertools/tcg-powertools-match.utils.tsAdded LAWP, TS26/XTS26, TWIN-SUNS, A-LAWLESS-TIME-WEEKLY-PLAY-PROMOS, ASHES-OF-THE-EMPIRE expansion mappings

Key Code

// Fallback: strip "// TokenName" suffix for multi-sided Base cards. const isDoubleFaceFallback = !apiMatch && baseName.includes(" // ") if (isDoubleFaceFallback) { const strippedName = StarWarsUnlimitedImporter.normalizeCardName( StarWarsUnlimitedImporter.stripDoubleFaceSuffix(baseName), ) apiMatch = this.findApiCardMatch(apiCardIndex, strippedName, variant, finish) } // For // fallback matches, prevent the base card's name/description/images // from overwriting token-specific data const apiEnrichment = apiMatch && !isDoubleFaceFallback ? apiMatch : undefined

Two variables control what data flows from the API match:

  • apiMatch — used for translations and shared card attributes (aspects, hp, rarity, artist, orientation, traits, keywords, cost, power, unique). Token products ARE sides of the base card, so these attributes apply to all // variants.
  • apiEnrichment — used for token-specific fields that differ between the base card and its tokens: name (must keep // Credit suffix), description/epicAction (base card’s game text doesn’t apply to tokens), images (each token has its own art), and Cardmarket ID (separate TCGPowertools product). For // fallback matches this is undefined, falling back to TCG CSV data.

Consequences

Positive

  • 295 // products gain translations (FR, DE, IT, ES) and shared base card attributes (aspects, hp, artist, rarity) that were previously missing
  • ~92 additional products gain translations via baseSlugify normalization (curly quotes, ellipsis, diacritics)
  • Zero impact on existing non-// card matching — verified no slug collisions across all 2,320 distinct names
  • Product display names, slugs, card keys, Cardmarket IDs, and images remain unchanged for // cards

Negative

  • The translations applied to // Credit, // Experience, // Shield variants are the base card’s translations (e.g., “Complexe de Commandement Imperial”) — the token-specific names are not translated because the SWU API doesn’t provide per-token translations
  • TCG CSV typos (Frieghter vs Freighter, Counci vs Council) and missing subtitles (Rex's DC-17s vs Rex's DC-17s - This Ship is Going Down) are not fixed — these require upstream corrections or fuzzy matching

More Information

  • PR: #2850 
  • Related ADR: TCGCSV-first architecture — explains why TCG CSV names are the primary source of truth
  • Related ADR: TCGPlayer suffix-based parsing — explains the parseTcgProductName logic that handles (Foil), (Hyperspace) suffixes but not //
  • SWU Official API: https://admin.starwarsunlimited.com/api/cards
Last updated on