cardmarket package - github.com/mtgban/go-cardmarket - Go Packages

cardmarket

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 18 Imported by: 0

README

go-cardmarket

A small Go client for the Cardmarket API (catalog & pricing), plus the two tools that read its published catalog files. It handles OAuth 1.0a request signing, rate-limit backoff, and resilient HTTP via retryablehttp.

Entities are named as Cardmarket's own documentation names them — Product, Expansion, Article — so a field can be looked up in one place rather than translated first.


Install

go get github.com/mtgban/go-cardmarket

Quick start

package main

import (
    "context"
    "fmt"

    "github.com/mtgban/go-cardmarket"
)

func main() {
    c := cardmarket.NewClient("<APP_TOKEN>", "<APP_SECRET>")

    expansions, err := c.Expansions(context.TODO(), cardmarket.GamePokemon)
    if err != nil {
        // <handle err>
    }
    fmt.Println("expansions:", len(expansions))
}

Two transports, two naming conventions

Cardmarket publishes its catalog twice over, and the method name says which one you are reading:

  • Bare entity names (Product, Expansions, Articles, …) are the signed API at apiv2.cardmarket.com. They need app credentials and count against a daily request allowance.
  • Download* (DownloadPriceGuide, DownloadProductListSingles, …) are the static files at downloads.s3.cardmarket.com. No credentials, no allowance, a whole game per request.

Prefer the downloads wherever they answer the question. Walking a game through the API is thousands of requests; the same data as a file is one.


Authentication

NewClient(appToken, appSecret) wires an http.RoundTripper that signs every request with OAuth 1.0a (HMAC-SHA1), as Cardmarket's auth documentation describes. Only the app token and secret are needed for the public catalog requests; the access-token pair stays empty.

RequestNo() reports how many requests the client has made, which is what to watch against the daily allowance.


API helpers

  • Expansions
    • Expansions(ctx, gameID) ([]Expansion, error) — every expansion of a game
    • ExpansionSingles(ctx, expansionID) ([]Product, error) — every single card in one
  • Products
    • Product(ctx, productID) (*Product, error)
  • Articles (listings)
    • Articles(ctx, productID, options, page, maxResults) ([]Article, error)
    • DefaultArticleFilter(onlyEnglish) — played-or-better, from a seller with a record, neither signed nor altered

Pages start at zero and the API requires both bounds — asking for a page size without a start is refused. MaxEntities (100) is the largest page it serves.

Two shapes of one Product

The marketplace answers the same entity two ways, and which request built a Product decides which half of it is filled:

filled by fields
ExpansionSingles ExpansionName, ExpansionIcon
Product Expansion (nested), PriceGuide

A product read one way carries nothing of the other's.

Finish flags are per-game

A Magic Article carries IsFoil and neither of the others. A Pokémon Article carries IsFirstEd and IsReverseHolo and no IsFoil at all. A flag absent from a game's listings decodes false, so read the one its game uses.


Published catalog files

  • DownloadPriceGuide(ctx, gameID) ([]PriceGuide, error)
  • DownloadProductListSingles(ctx, gameID) ([]ProductList, error)
  • DownloadProductListSealed(ctx, gameID) ([]ProductList, error)

PriceGuide.SecondPrinting(gameID) reads the printing sold beside the default one under whichever heading the game's guide publishes it — most games publish a foil, Pokémon publishes a reverse holo.


The catalog file

Catalog is the id map a price run reads instead of walking the API. Two programs write it and they fill different halves:

field MTGJSON (Magic) mkmcatalog (everything else)
expansionId, name yes yes
number mostly yes
uuids yes never
rarity, version never yes
expansion code never yes

A field absent from one producer is absent from every product it writes, not missing from a particular row.

catalog, err := cardmarket.LoadCatalog(reader)
product := catalog.Data.Products[571798]

meta.date and meta.version are carried through and neither is enforced: the two producers version themselves differently, and a reader refusing an unfamiliar string would refuse a file it can read perfectly well. A file naming no products is refused — that is the shape a truncated download takes.


Commands

mkmcatalog

Walks one game's catalog and writes the file above.

go build ./cmd/mkmcatalog

MKM_APP_TOKEN=... MKM_APP_SECRET=... \
  ./mkmcatalog -game pokemon -output cardmarket_catalog.json

Flags:

  • -gamelorcana, riftbound, onepiece, pokemon, yugioh, fleshandblood
  • -output — a path, or a b2://bucket/object one; an .xz suffix compresses it

A b2:// output reads B2_APPLICATION_KEY_ID and B2_APPLICATION_KEY — the same names the b2 command line uses, so one pair of credentials works for both.

Any expansion failing fails the run. The client already retries the transient errors, and a partial catalog uploaded on schedule would silently unprice whatever it dropped.

mkmpriceguide

Downloads a published file to stdout. Needs no credentials.

go build ./cmd/mkmpriceguide
./mkmpriceguide -game 6 -mode prices > pokemon-prices.json

Flags:

  • -game — the game id (see the Game* constants)
  • -modeprices (default), singles, sealed

License

MIT

Documentation

Overview

Package cardmarket reads Cardmarket's marketplace: the signed API that answers for products, expansions and listings, and the catalog files the site publishes for anyone to download. The entities are named as https://apiv2.cardmarket.com/ws/documentation names them, so a field can be looked up in one place rather than translated first.

Index

Constants

View Source
const (
	GameMagic = iota + 1
	GameWorldOfWarcraft
	GameYuGiOh

	GameTheSpoils
	GamePokemon
	GameForceOfWill
	GameCardfightVanguard
	GameFinalFantasy
	GameWeissSchwarz
	GameDragoborne
	GameMyLittlePony
	GameDragonBallSuper

	GameStarWarsDestiny
	GameFleshAndBlood
	GameDigimon
	GameOnePiece
	GameLorcana
	GameBattleSpiritsSaga
	GameStarWarsUnlimited
	GameRiftbound
)

The games Cardmarket carries, as their API numbers them.

View Source
const (

	// MaxEntities is how many results one request may ask for
	MaxEntities = 100
)

Variables

This section is empty.

Functions

func BuildURL

func BuildURL(idProduct, idGame int, affiliate string, foil bool) string

BuildURL builds the storefront link for a product, carrying an affiliate tag when one is given.

func DefaultArticleFilter

func DefaultArticleFilter(onlyEnglish bool) map[string]string

DefaultArticleFilter is the filter a price should be read through: played or better, from a seller with a record, and neither signed nor altered. Anything looser prices a card off a listing nobody would buy.

func GameFromName

func GameFromName(name string) int

GameFromName is the inverse, matching case-insensitively so a caller can hand over the name it already knows a game by ("lorcana") instead of translating to an id first; an unnamed game is Magic. Unknown games answer 0, which the URL builders reject: a game Cardmarket does not carry yields no link at all rather than one pointing at a path it does not serve.

func GameName

func GameName(idGame int) string

GameName returns the game as Cardmarket spells it, or "" for a game whose catalog is not covered.

func ProductVersion

func ProductVersion(product *Product) int

ProductVersion reads that index off a product, or zero where it carries none. The marketplace publishes it in the name for some games and only in the address for others - of the catalogs we walk, 31,523 of Yu-Gi-Oh's 86,628 products name it and not one of Pokemon's 72,752 does, though its shelves are full of it - so both are read and the name wins.

func SearchURL

func SearchURL(name string, idGame int, affiliate string) string

SearchURL returns the catalog search for a product name, the fallback for a card whose Cardmarket product id is not known. Empty for an uncovered game, like BuildURL.

Types

type APIError

type APIError struct {
	Description       string `json:"mkm_error_description"`
	InternalCode      any    `json:"internal_error_code"`
	PostDataField     any    `json:"post_data_field"`
	StatusCode        int    `json:"http_status_code"`
	StatusDescription string `json:"http_status_code_description"`
}

APIError is the body Cardmarket answers a rejected request with. The description is the only part meant for a person; the rest says which of the request it objected to.

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface.

type Article

type Article struct {
	IDArticle int `json:"idArticle"`
	IDProduct int `json:"idProduct"`
	Language  struct {
		IDLanguage   int    `json:"idLanguage"`
		LanguageName string `json:"languageName"`
	} `json:"language"`
	Comments       string         `json:"comments"`
	Price          float64        `json:"price"`
	IDCurrency     int            `json:"idCurrency"`
	CurrencyCode   string         `json:"currencyCode"`
	Prices         []ArticlePrice `json:"prices,omitempty"`
	Count          int            `json:"count"`
	InShoppingCart bool           `json:"inShoppingCart"`
	Condition      string         `json:"condition"`
	Product        struct {
		Name        string `json:"enName"`
		LocName     string `json:"locName"`
		Expansion   string `json:"expansion"`
		IDExpansion int    `json:"idExpansion"`
		SetCode     string `json:"abbreviation"`
		Number      string `json:"nr"`
		Rarity      string `json:"rarity"`
		IDGame      int    `json:"idGame"`
		ExpIcon     int    `json:"expIcon"`
	} `json:"product"`
	Seller ArticleSeller `json:"seller"`

	IsFoil        bool `json:"isFoil"`
	IsFirstEd     bool `json:"isFirstEd"`
	IsReverseHolo bool `json:"isReverseHolo"`
	IsSigned      bool `json:"isSigned"`
	IsAltered     bool `json:"isAltered"`

	Links []Link `json:"links,omitempty"`
}

Article is one seller's listing on a product.

The finish flags are the game's own and not one vocabulary: a Magic listing carries IsFoil and neither of the others, a Pokemon listing carries IsFirstEd and IsReverseHolo and no IsFoil at all. A flag absent from a game's listings decodes false, so read the one its game uses.

type ArticlePrice

type ArticlePrice struct {
	Price        float64 `json:"price"`
	IDCurrency   int     `json:"idCurrency"`
	CurrencyCode string  `json:"currencyCode"`
}

ArticlePrice is one listing's price in one currency. A listing is quoted in the seller's currency and in every currency the marketplace converts it to.

type ArticleSeller

type ArticleSeller struct {
	IDUser       int    `json:"idUser"`
	Username     string `json:"username"`
	IsCommercial int    `json:"isCommercial"`
	IsSeller     bool   `json:"isSeller"`
	Reputation   int    `json:"reputation"`
	SellCount    int    `json:"sellCount"`
	SoldItems    int    `json:"soldItems"`
	OnVacation   bool   `json:"onVacation"`
	// AvgShippingTime is in days, and RiskGroup and LossPercentage are the
	// marketplace's own read on whether an order will arrive.
	AvgShippingTime int    `json:"avgShippingTime"`
	RiskGroup       int    `json:"riskGroup"`
	LossPercentage  string `json:"lossPercentage"`
	Address         struct {
		Country string `json:"country"`
	} `json:"address"`
}

ArticleSeller is who is selling, minus the fields naming the person. The marketplace answers a seller's first name, email, phone, VAT number and legal information beside their trading record; none of that is needed to judge a price, so none of it is decoded and none of it can leak into a log of an article.

type Catalog

type Catalog struct {
	Meta CatalogMeta `json:"meta"`
	Data CatalogData `json:"data"`
}

Catalog is a published Cardmarket catalog: every singles product mapped to the printings it sells, which is what a price run reads instead of walking the API.

Two programs write this file and they fill different halves of it. MTGJSON publishes Magic's, with the uuids of the printings each product stands for and no rarity, version or expansion code. cmd/mkmcatalog walks the API for every other game, which answers the rarity and the version and the code but knows nothing of any datastore's uuids. A field absent from one producer is absent from every product it writes, not missing from a particular row.

func LoadCatalog

func LoadCatalog(reader io.Reader) (*Catalog, error)

LoadCatalog reads a published catalog. A file naming no products is refused: it is the shape a truncated download and an unrelated JSON file both take, and pricing from it would quietly unprice a whole game.

type CatalogData

type CatalogData struct {
	Expansions map[int]CatalogExpansion `json:"expansions"`
	Products   map[int]CatalogProduct   `json:"products"`
}

CatalogData is the file's two tables. JSON has only string keys, so the ids are written as strings and read back as the numbers they are, which encoding/json does on its own for an integer-keyed map.

type CatalogExpansion

type CatalogExpansion struct {
	Name string `json:"name"`
	Code string `json:"code,omitempty"`
}

CatalogExpansion names one expansion of the catalog. Code is the marketplace's own abbreviation, which is where the foreign catalogs say what they are; MTGJSON's file does not carry it.

type CatalogMeta

type CatalogMeta struct {
	Date    string `json:"date"`
	Version string `json:"version"`
}

CatalogMeta says when the file was built and by which version of whatever built it. Both are carried through and neither is enforced: the two producers version themselves differently and a reader that refused an unfamiliar string would refuse a file it can read perfectly well.

type CatalogProduct

type CatalogProduct struct {
	ExpansionID int    `json:"expansionId"`
	Name        string `json:"name"`
	Number      string `json:"number,omitempty"`
	Rarity      string `json:"rarity,omitempty"`
	// Version is the index the marketplace counts a card's printings with
	// where one shelf sells several - the second Budew of a stamp
	// programme, the master-ball pattern beside the poke-ball one. It is
	// the only thing telling two products of one name and number apart, and
	// zero where the product carries none. See ProductVersion for where it
	// is read from, which is not the same field in every game.
	Version int `json:"version,omitempty"`
	// UUIDs are the printings the product sells, as MTGJSON links them.
	// Only Magic's file carries any.
	UUIDs []string `json:"uuids,omitempty"`
}

CatalogProduct is one product of the catalog: what the marketplace calls it, where it files it, and the printings it stands for.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client reads Cardmarket's API, which signs every request with an app token and secret.

func NewClient

func NewClient(appToken, appSecret string) *Client

NewClient returns a client signing with the given app credentials.

The API is very sensitive to concurrent requests and answers a burst with 429s, so the retry is deliberately patient: up to 20 attempts, each waiting a jittered 2 to 10 seconds multiplied by the attempt number. That is up to half an hour of backoff for one unlucky request, which is the intended behaviour against a rate limiter but is not a bound anyone should rely on - give long walks a context deadline.

func (*Client) Articles

func (mkm *Client) Articles(ctx context.Context, id int, options map[string]string, page, maxResults int) ([]Article, error)

Articles returns the listings on a product, one page at a time. Pages start at zero, and the API requires both bounds: asking for a page size without a start is refused.

func (*Client) ExpansionSingles

func (mkm *Client) ExpansionSingles(ctx context.Context, id int) ([]Product, error)

ExpansionSingles returns every single card in one expansion.

func (*Client) Expansions

func (mkm *Client) Expansions(ctx context.Context, gameID int) ([]Expansion, error)

Expansions returns every expansion of one game.

func (*Client) Product

func (mkm *Client) Product(ctx context.Context, id int) (*Product, error)

Product returns one catalog entry by id.

func (*Client) RequestNo

func (mkm *Client) RequestNo() int

RequestNo returns how many requests the client has made, which matters against Cardmarket's daily allowance.

type Expansion

type Expansion struct {
	IDExpansion  int            `json:"idExpansion"`
	Name         string         `json:"enName"`
	Localization []Localization `json:"localization,omitempty"`
	// SetCode is the marketplace's own abbreviation, which is the only
	// field telling a foreign catalog from the English one it shadows.
	SetCode     string `json:"abbreviation"`
	Icon        int    `json:"icon"`
	ReleaseDate string `json:"releaseDate"`
	IsReleased  bool   `json:"isReleased"`
	IDGame      int    `json:"idGame"`
	Links       []Link `json:"links,omitempty"`
}

Expansion is a set as Cardmarket files it.

type Link struct {
	Rel    string `json:"rel"`
	Href   string `json:"href"`
	Method string `json:"method"`
}

Link is one HATEOAS entry, which every entity carries: what the link is for, where it points, and the method that follows it.

type Localization

type Localization struct {
	Name         string `json:"name"`
	IDLanguage   int    `json:"idLanguage"`
	LanguageName string `json:"languageName"`
}

Localization is a name as one language spells it. Every named entity carries the five languages the marketplace trades in.

type PriceGuide

type PriceGuide struct {
	IDProduct        int     `json:"idProduct"`
	AvgSellPrice     float64 `json:"avg"`
	LowPrice         float64 `json:"low"`
	TrendPrice       float64 `json:"trend"`
	FoilAvgSellPrice float64 `json:"avg-foil"`
	FoilLowPrice     float64 `json:"low-foil"`
	FoilTrendPrice   float64 `json:"trend-foil"`
	// Pokemon's guide names the second printing's prices "holo" rather
	// than "foil", and publishes them for the cards sold in a reverse
	// holo and for no others; see SecondPrinting.
	HoloAvgSellPrice float64 `json:"avg-holo"`
	HoloLowPrice     float64 `json:"low-holo"`
	HoloTrendPrice   float64 `json:"trend-holo"`
	AvgDay1          float64 `json:"avg1"`
	AvgDay7          float64 `json:"avg7"`
	AvgDay30         float64 `json:"avg30"`
	FoilAvgDay1      float64 `json:"avg1-foil"`
	FoilAvgDay7      float64 `json:"avg7-foil"`
	FoilAvgDay30     float64 `json:"avg30-foil"`
}

PriceGuide is one product's published prices: the low, the trend, and the averages Cardmarket derives rather than any single listing.

func DownloadPriceGuide

func DownloadPriceGuide(ctx context.Context, gameID int) ([]PriceGuide, error)

DownloadPriceGuide downloads the published price guide for one game.

func (PriceGuide) SecondPrinting

func (pg PriceGuide) SecondPrinting(gameID int) (low, trend float64)

SecondPrinting names the prices of the printing sold beside the product's default one, under whichever heading the game's guide publishes them. Most games sell a foil beside a plain card; Pokemon sells a reverse holo, and its guide says so.

type Product

type Product struct {
	IDProduct     int `json:"idProduct"`
	IDMetaproduct int `json:"idMetaproduct"`
	// CountReprints is how many products the metaproduct bundles, which is
	// how many times the card has been printed across every expansion.
	CountReprints int            `json:"countReprints"`
	Name          string         `json:"enName"`
	LocName       string         `json:"locName"`
	Localization  []Localization `json:"localization,omitempty"`
	Website       string         `json:"website"`
	// Image is a pre-signed address that expires an hour after the request
	// that returned it. It is worth reading and never worth storing.
	Image        string `json:"image"`
	GameName     string `json:"gameName"`
	CategoryName string `json:"categoryName"`
	IDGame       int    `json:"idGame"`
	Number       string `json:"number"`
	// Rarity is what the marketplace calls the printing's rarity. It is the
	// game's own vocabulary where the game has one - Common through Secret
	// Rare for Pokemon - and "Promo" for a whole promotional shelf.
	Rarity string `json:"rarity"`
	// ExpansionName and ExpansionIcon are the ExpansionSingles form.
	ExpansionName string `json:"expansionName"`
	ExpansionIcon int    `json:"expansionIcon"`
	// ExpansionCode is the marketplace's own abbreviation of the
	// expansion, read off the catalog rather than the product
	ExpansionCode string `json:"-"`
	// Expansion and PriceGuide are the single-product form.
	Expansion struct {
		IDExpansion   int    `json:"idExpansion"`
		Name          string `json:"enName"`
		ExpansionIcon int    `json:"expansionIcon"`
	} `json:"expansion"`
	// PriceGuide is keyed by the marketplace's own headings - SELL, LOW,
	// LOWEX, LOWFOIL, AVG, TREND, TRENDFOIL. The documentation spells the
	// third LOWEX+; the API sends LOWEX.
	PriceGuide    map[string]float64 `json:"priceGuide"`
	CountArticles int                `json:"countArticles"`
	CountFoils    int                `json:"countFoils"`
	Links         []Link             `json:"links,omitempty"`
}

Product is one catalog entry, a card or a sealed item.

Two of its fields depend on which request built it, the marketplace answering the same entity two ways: ExpansionName and ExpansionIcon are filled by ExpansionSingles, and Expansion and PriceGuide by Product. A product read one way carries nothing of the other's.

type ProductList

type ProductList struct {
	IDProduct    int    `json:"idProduct"`
	Name         string `json:"name"`
	CategoryID   int    `json:"idCategory"`
	CategoryName string `json:"categoryName"`
	ExpansionID  int    `json:"idExpansion"`
	MetacardID   int    `json:"idMetacard"`
	DateAdded    string `json:"dateAdded"`
}

ProductList is one entry of the catalog dump, which names products without pricing them.

func DownloadProductListSealed

func DownloadProductListSealed(ctx context.Context, gameID int) ([]ProductList, error)

DownloadProductListSealed downloads the catalog of one game's sealed product.

func DownloadProductListSingles

func DownloadProductListSingles(ctx context.Context, gameID int) ([]ProductList, error)

DownloadProductListSingles downloads the catalog of one game's singles.

Directories

Path Synopsis
cmd
mkmcatalog command
Command mkmcatalog walks one game's Cardmarket catalog - every expansion and the products it shelves - and writes it in the shape a price run reads, so the twice-daily runs stop crawling the API.
Command mkmcatalog walks one game's Cardmarket catalog - every expansion and the products it shelves - and writes it in the shape a price run reads, so the twice-daily runs stop crawling the API.
mkmpriceguide command
Command mkmpriceguide downloads Cardmarket's published catalog files for a game: the price guide, or the product list for singles or sealed product.
Command mkmpriceguide downloads Cardmarket's published catalog files for a game: the price guide, or the product list for singles or sealed product.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL