MetalAPI
XAU3.997,89−1,54 %XAG53,936−1,51 %XPT1.554,47−0,83 %XPD1.047,03−0,44 %XCU6,5080+2,29 %WTI103,27+3,82 %BRENT111,70−1,30 %EUR1,120400−0,01 %GBP1,337500−0,01 %JPY0,0063761+0,14 %CHF1,213445−0,25 %TRY0,020125−0,12 %BTC80.329,75−0,55 %ETH2.329,36−1,13 %
Alle Artikel
TutorialE-CommerceTypeScriptPython

Einen Schmuckpreisrechner mit Live-Goldpreisen entwickeln

Serverseitiger Schmuckpreisrechner in TypeScript und Python: Live-Goldpreis pro Gramm, Feingehalt, Arbeitslohn, MwSt. und kontingentschonendes Caching.

Von MetalAPI Team··10 Min. Lesezeit

Was der Rechner leistet

Schmuck wird in der Regel aus vier Größen kalkuliert: dem Gewicht des Stücks, dem Feingehalt des Goldes, dem aktuellen Goldpreis und der Arbeit des Goldschmieds, dazu kommt die Steuer. Ein Rechner, der das für jede Produktseite oder Angebotsanfrage erledigt, braucht einen Goldpreis pro Gramm in der Währung des Shops. Und er braucht diesen Preis schnell, innerhalb des API-Kontingents und ohne den API-Schlüssel im Browser preiszugeben.

Dieses Tutorial baut einen kleinen serverseitigen Dienst in TypeScript (Node.js) und in Python. Der Browser oder Ihr Shop-Backend sendet ein Gewicht und einen Karatwert an Ihren eigenen Endpunkt. Ihr Server verknüpft beides mit einem zwischengespeicherten Goldpreis von MetalAPI und gibt eine vollständige Preisaufschlüsselung zurück. Die Formel:

Preisformel
Metallwert  = Gewicht (g) × Goldpreis pro Gramm (24K) × Feingehalt
Arbeitslohn = pro Gramm, als Prozentsatz des Metallwerts oder pauschal
Nettopreis  = Metallwert + Arbeitslohn
MwSt.       = Nettopreis × MwSt.-Satz
Gesamt      = Nettopreis + MwSt.

Der Spotpreis für Gold ist ein Großhandels-Referenzpreis. Endkundenpreise spiegeln zusätzlich Design, Steine, Materialverlust und Marge wider, und Ankaufspreise liegen meist unter dem Spotpreis. Betrachten Sie diesen Rechner als den metallbasierten Teil Ihrer Preisgestaltung und ergänzen Sie ihn um Ihre eigenen Geschäftsregeln.

Goldpreis pro Gramm in Ihrer Währung

Der Endpunkt /latest liefert Preise in jeder Basiswährung und bei Metallen in jeder Gewichtseinheit. Eine einzige Anfrage ergibt den Preis für ein Gramm Feingold in Euro:

bash
curl "https://metalapi.com/api/v1/latest?base=EUR&currencies=XAU&unit=gram" \
  -H "X-API-Key: YOUR_API_KEY"
Antwort (Beispielwerte)
{
  "success": true,
  "timestamp": 1792834892,
  "date": "2026-10-24",
  "base": "EUR",
  "unit": "gram",
  "rates": {
    "XAU": 0.0090909091,
    "EURXAU": 110
  },
  "rate_limit": { "limit": 10000, "remaining": 9874, "...": "..." }
}

rates.EURXAU ist der Preis einer Einheit XAU, hier eines Gramms, in der Basiswährung. Genau diese Zahl braucht der Rechner. rates.XAU ist der Kehrwert, also Gramm Gold pro Euro. Die Antwort enthält unit, sobald nicht die Standardeinheit Feinunze gilt, und timestamp ist der Zeitpunkt der Preisdaten. Zeigen Sie ihn neben jedem Angebot an.

Basiswährung und Einheit funktionieren in jedem Tarif, auch im Free-Tarif, und dieselbe Anfrage kann XAG, XPT und XPD enthalten, falls Sie auch Silber- oder Platinschmuck verkaufen. Kostenpflichtige Tarife können außerdem den Karat-Endpunkt nutzen, der Karatpreise wie XAU22K direkt liefert oder mit carat=all alle Karatwerte auf einmal. Dieses Tutorial berechnet den Feingehalt selbst, damit der Code in jedem Tarif läuft und Feingehaltswerte aus Punzierungen verwenden kann. Darum geht es im nächsten Abschnitt.

Feingehalt, Arbeitslohn und Mehrwertsteuer

Karat gibt den Goldgehalt in Vierundzwanzigsteln an, der Feingehaltsfaktor ist also karat / 24. Diese Konvention verwendet auch der Endpunkt /carat. Viele Märkte punzieren Gold stattdessen nach dem Feingehalt in Tausendsteln, und bei manchen Karatwerten weichen die beiden leicht voneinander ab:

Karatkarat / 24Übliche Punze
24K1,0000999
22K0,9167916
21K0,8750875
18K0,7500750
14K0,5833585
9K0,3750375

Für einen 5,2 g schweren 14-Karat-Ring bei beispielhaften 110 € pro Gramm ergibt 14/24 einen Metallwert von 333,67 € und 0.585 einen von 334,62 €. Keiner der beiden Werte ist falsch. Verwenden Sie die Konvention, die auch Ihre Punzen und Rechnungen verwenden; deshalb erwartet der Code unten einen Feingehaltsfaktor statt eines Karatwerts. Der Leitfaden zu Karatpreisen erläutert die Hintergründe ausführlicher.

Der Arbeitslohn wird meist in einer von drei Formen berechnet, und die TypeScript-Version unterstützt alle drei:

  • Pro Gramm: ein fester Betrag für jedes Gramm des Stücks, zum Beispiel 12 €/g.
  • Prozentual: ein Anteil am Metallwert, zum Beispiel 8 %. Er steigt und fällt mit dem Goldpreis.
  • Pauschal: ein fester Betrag pro Stück, üblich bei kleinen Artikeln und Einzelanfertigungen.

Die Mehrwert- oder Umsatzsteuer kommt zuletzt. Der Steuersatz und die Bemessungsgrundlage hängen vom Land ab: Manche Länder besteuern den vollen Preis, manche behandeln Anlagegold anders als Schmuck, und manche besteuern nur die Arbeitsleistung. Legen Sie Satz und steuerpflichtigen Betrag in der Konfiguration ab, stimmen Sie beides mit Ihrer Steuerberatung ab und runden Sie jede Position auf Cent, damit die Positionen einer Rechnung genau die Gesamtsumme ergeben.

Caching, um im Kontingent zu bleiben

Jeder Tarif aktualisiert die Preise in einem festen Intervall, und die Aktualisierungen fallen auf Vielfache dieses Intervalls, gezählt ab 00:00 UTC: im Standard-Tarif um :00, :10, :20 usw., im Free-Tarif einmal täglich um Mitternacht UTC. Jede Anfrage zwischen zwei solchen Intervallgrenzen liefert dieselben Preise, der ideale Cache aktualisiert sich also einmal pro Intervallgrenze. Eine Aktualisierung an jeder Grenze, rund um die Uhr, kostet in einem Monat mit 31 Tagen so viel:

Tarif (Monatskontingent)AktualisierungsintervallAnfragen bei Abruf an jeder GrenzeIm Kontingent?
Free (100)Täglich31Ja
Lite (1.000)30 Min.1.488Nein: stattdessen stündlich aktualisieren (744)
Standard (10.000)10 Min.4.464Ja
Advanced (50.000)1 Min.44.640Ja, mit weniger Spielraum für andere Aufrufe

Zwei weitere Regeln machen den Cache robust. Erstens Single Flight: Läuft der Cache unter Last ab, holt die erste Anfrage einen neuen Preis, und gleichzeitige Anfragen warten auf dieses Ergebnis, statt jeweils selbst die API aufzurufen. Zweitens Stale on Error: Ist die API nicht erreichbar, rechnen Sie einige Stunden lang mit dem letzten gültigen Preis weiter und versuchen es einmal pro Minute erneut, statt jeden Bestellvorgang scheitern zu lassen. Anfragen, die mit einem Serverfehler scheitern, werden zwar nicht berechnet, aber es gibt trotzdem keinen Grund, während eines Ausfalls eine Flut davon zu senden.

Der Cache unten liegt im Arbeitsspeicher des Prozesses, was für einen einzelnen Server ideal ist. Auf Serverless-Plattformen oder mit mehreren Instanzen hält jede Instanz ihre eigene Kopie. Legen Sie den Preis dann in einen gemeinsamen Speicher wie Redis oder nutzen Sie den Daten-Cache Ihres Frameworks mit derselben Ablaufzeit.

Die Implementierung in TypeScript

Das folgende Modul läuft ab Node.js 18 und hat keine Abhängigkeiten. Es ruft den Europreis pro Gramm ab, speichert ihn bis zur nächsten Intervallgrenze des Tarifs im Cache und macht aus einem Gewicht, einem Feingehaltsfaktor und einem Arbeitslohn eine Preisaufschlüsselung:

gold-quote.ts
// gold-quote.ts — Node 18+. Der API-Schlüssel bleibt auf dem Server.
const API_KEY = process.env.METALAPI_KEY!;
const CURRENCY = "EUR";
const PLAN_INTERVAL_S = 600; // Standard-Tarif: Preise werden alle 10 Minuten aktualisiert
const MIN_TTL_S = 0;         // Lite-Tarif: 3600 setzen, um unter 1.000 Anfragen im Monat zu bleiben

interface GoldPrice {
  perGram: number;   // Feingold (24K), CURRENCY pro Gramm
  timestamp: number; // Zeitpunkt der Preisdaten (Unix-Sekunden)
  fetchedAt: number;
  expiresAt: number;
}

let cached: GoldPrice | null = null;
let inflight: Promise<GoldPrice> | null = null;

// Preise ändern sich nur zu Vielfachen des Tarifintervalls (gezählt ab 00:00 UTC).
function nextBoundary(ms: number): number {
  const step = PLAN_INTERVAL_S * 1000;
  return Math.floor(ms / step) * step + step;
}

async function fetchGold(): Promise<GoldPrice> {
  const url = `https://metalapi.com/api/v1/latest?base=${CURRENCY}&currencies=XAU&unit=gram`;
  const res = await fetch(url, { headers: { "X-API-Key": API_KEY }, signal: AbortSignal.timeout(10_000) });
  const body = await res.json();
  if (!body.success) throw new Error(`MetalAPI ${body.error.code}: ${body.error.info}`);
  const now = Date.now();
  return {
    perGram: body.rates[`${CURRENCY}XAU`],
    timestamp: body.timestamp,
    fetchedAt: now,
    expiresAt: nextBoundary(now + MIN_TTL_S * 1000) + 5_000, // ein paar Sekunden Puffer
  };
}

export function goldPerGram(): Promise<GoldPrice> {
  if (cached && Date.now() < cached.expiresAt) return Promise.resolve(cached);
  // Single Flight: Gleichzeitige Anfragen teilen sich einen API-Aufruf.
  inflight ??= fetchGold()
    .then((fresh) => (cached = fresh))
    .catch((err) => {
      // Bis zu 6 Stunden mit dem letzten gültigen Preis weiterrechnen; in einer Minute erneut versuchen.
      if (cached && Date.now() - cached.fetchedAt < 6 * 3600_000) {
        cached.expiresAt = Date.now() + 60_000;
        return cached;
      }
      throw err;
    })
    .finally(() => (inflight = null));
  return inflight;
}

export type MakingCharge =
  | { type: "per_gram"; amount: number }
  | { type: "percent"; rate: number } // 0.08 = 8 % des Metallwerts
  | { type: "flat"; amount: number };

const money = (n: number) => Math.round(n * 100 + Number.EPSILON) / 100;

export async function quote(grams: number, purity: number, making: MakingCharge, vatRate: number) {
  const gold = await goldPerGram();
  const metal = money(grams * gold.perGram * purity);
  const labour = money(
    making.type === "per_gram" ? grams * making.amount
      : making.type === "percent" ? metal * making.rate
      : making.amount,
  );
  const net = money(metal + labour);
  const vat = money(net * vatRate);
  return {
    currency: CURRENCY,
    grams,
    purity,
    gold_per_gram_24k: gold.perGram,
    price_time: new Date(gold.timestamp * 1000).toISOString(),
    metal,
    making: labour,
    net,
    vat,
    total: money(net + vat),
  };
}

Einige Zeilen verdienen einen genaueren Blick. nextBoundary() rundet auf das nächste Vielfache des Tarifintervalls auf. Im Standard-Tarif läuft ein um 10:03 Uhr abgerufener Preis daher um 10:10:05 Uhr ab, wenige Sekunden nach der nächsten Aktualisierung, und nicht erst um 10:13 Uhr. Steht MIN_TTL_S im Lite-Tarif auf 3600, wartet die nächste Aktualisierung auf die erste Intervallgrenze, die mindestens eine Stunde entfernt liegt; das begrenzt die Nutzung auf 24 Anfragen pro Tag. Der .catch-Zweig setzt die Stale-on-Error-Regel um: Er gibt den letzten gültigen Preis zurück und plant den nächsten Versuch eine Minute später ein. Ein Ausfall kostet so höchstens eine fehlgeschlagene Anfrage pro Minute statt einer pro Besucher.

Stellen Sie das Modul über einen kleinen HTTP-Endpunkt bereit. Prüfen Sie Eingaben streng: Akzeptieren Sie nur die Feingehalte, die Sie verkaufen, und einen sinnvollen Gewichtsbereich. Arbeitslohn und Mehrwertsteuersatz sind serverseitige Einstellungen; übernehmen Sie sie niemals vom Client.

server.ts
// server.ts — npm install express, dann: METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { quote } from "./gold-quote";

// Feingehaltsfaktoren laut Punze auf Ihren Stücken (siehe Karattabelle oben).
const PURITY: Record<string, number> = { "24": 0.999, "22": 0.916, "21": 0.875, "18": 0.75, "14": 0.585, "9": 0.375 };
const app = express();

app.get("/api/jewelry-quote", async (req, res) => {
  const grams = Number(req.query.grams);
  const purity = PURITY[String(req.query.karat)];
  if (!purity || !(grams > 0 && grams <= 2000)) {
    res.status(400).json({ error: "Use karat 24, 22, 21, 18, 14 or 9 and grams between 0 and 2000." });
    return;
  }
  try {
    // Arbeitslohn und MwSt. sind serverseitige Einstellungen, niemals Client-Eingaben.
    res.json(await quote(grams, purity, { type: "per_gram", amount: 12 }, 0.2));
  } catch (err) {
    console.error(err);
    res.status(503).json({ error: "Gold price temporarily unavailable." });
  }
});

app.listen(3000, () => console.log("Try http://localhost:3000/api/jewelry-quote?karat=18&grams=5.2"));

Eine Anfrage an /api/jewelry-quote?karat=18&grams=5.2 liefert eine Aufschlüsselung wie diese (beispielhafter Goldpreis):

json
{
  "currency": "EUR",
  "grams": 5.2,
  "purity": 0.75,
  "gold_per_gram_24k": 110,
  "price_time": "2026-10-24T09:41:32.000Z",
  "metal": 429,
  "making": 62.4,
  "net": 491.4,
  "vat": 98.28,
  "total": 589.68
}

Die Implementierung in Python

Die Python-Version folgt demselben Aufbau. Sie verwendet Decimal für Geldbeträge, sodass sich nie binäre Gleitkommafehler einschleichen, und eine Sperre (Lock), damit sich gleichzeitige Anfragen in einem Server mit mehreren Threads einen API-Aufruf teilen. Der Kürze halber unterstützt sie nur einen Arbeitslohn pro Gramm:

gold_quote.py
# gold_quote.py — Python 3.9+, pip install requests
import math
import os
import threading
import time
from decimal import ROUND_HALF_UP, Decimal

import requests

API_KEY = os.environ["METALAPI_KEY"]
CURRENCY = "EUR"
PLAN_INTERVAL_S = 600  # Standard-Tarif: Preise werden alle 10 Minuten aktualisiert
MIN_TTL_S = 0          # Lite-Tarif: 3600 setzen, um unter 1.000 Anfragen im Monat zu bleiben

_lock = threading.Lock()
_cache = {"per_gram": None, "timestamp": 0, "fetched": 0.0, "expires": 0.0}


def _next_boundary(t: float) -> float:
    return (math.floor(t / PLAN_INTERVAL_S) + 1) * PLAN_INTERVAL_S


def gold_per_gram() -> tuple[Decimal, int]:
    """Fine (24K) gold in CURRENCY per gram, and the Unix time of that price."""
    with _lock:  # jeweils nur ein API-Aufruf; wartende Threads verwenden dessen Ergebnis
        now = time.time()
        if _cache["per_gram"] is not None and now < _cache["expires"]:
            return _cache["per_gram"], _cache["timestamp"]
        try:
            r = requests.get(
                "https://metalapi.com/api/v1/latest",
                params={"base": CURRENCY, "currencies": "XAU", "unit": "gram"},
                headers={"X-API-Key": API_KEY},
                timeout=10,
            )
            body = r.json()
            if not body.get("success"):
                raise RuntimeError(f"MetalAPI {body['error']['code']}: {body['error']['info']}")
        except Exception:
            if _cache["per_gram"] is not None and now - _cache["fetched"] < 6 * 3600:
                _cache["expires"] = now + 60  # letzten gültigen Preis ausliefern, in einer Minute erneut versuchen
                return _cache["per_gram"], _cache["timestamp"]
            raise
        _cache.update(
            per_gram=Decimal(str(body["rates"][f"{CURRENCY}XAU"])),
            timestamp=body["timestamp"],
            fetched=now,
            expires=_next_boundary(now + MIN_TTL_S) + 5,
        )
        return _cache["per_gram"], _cache["timestamp"]


def money(x: Decimal) -> Decimal:
    return x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def quote(grams: Decimal, purity: Decimal, making_per_gram: Decimal, vat_rate: Decimal) -> dict:
    per_gram, ts = gold_per_gram()
    metal = money(grams * per_gram * purity)
    making = money(grams * making_per_gram)
    net = metal + making
    vat = money(net * vat_rate)
    return {
        "currency": CURRENCY,
        "gold_per_gram_24k": str(per_gram),
        "price_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)),
        "metal": str(metal),
        "making": str(making),
        "net": str(net),
        "vat": str(vat),
        "total": str(net + vat),
    }

Jedes Web-Framework kann den Dienst bereitstellen. In FastAPI laufen einfache def-Endpunkte in einem Thread-Pool, und genau dafür ist die Sperre da:

app.py
# app.py — pip install fastapi uvicorn requests, dann: METALAPI_KEY=mk_live_... uvicorn app:app
from decimal import Decimal

from fastapi import FastAPI, HTTPException, Query

from gold_quote import quote

# Feingehaltsfaktoren laut Punze auf Ihren Stücken.
PURITY = {24: Decimal("0.999"), 22: Decimal("0.916"), 21: Decimal("0.875"),
          18: Decimal("0.750"), 14: Decimal("0.585"), 9: Decimal("0.375")}
app = FastAPI()


@app.get("/api/jewelry-quote")
def jewelry_quote(karat: int, grams: Decimal = Query(gt=0, le=2000)):
    if karat not in PURITY:
        raise HTTPException(400, "Use karat 24, 22, 21, 18, 14 or 9.")
    try:
        return quote(grams, PURITY[karat], making_per_gram=Decimal("12.00"), vat_rate=Decimal("0.20"))
    except Exception:
        raise HTTPException(503, "Gold price temporarily unavailable.")

Die Python-Version gibt Beträge als Strings zurück, die TypeScript-Version als Zahlen. Wenn ein Angebot in ein Bestellsystem übernommen wird, speichern Sie die Beträge als Dezimal-Strings oder als ganzzahlige Cent-Beträge, nicht als Gleitkommazahlen.

Ein Rechenbeispiel

Hier wird der 18-Karat-Ring Schritt für Schritt kalkuliert, mit einem beispielhaften Goldpreis von 110,00 € pro Gramm, einem Arbeitslohn von 12 € pro Gramm und 20 % Mehrwertsteuer:

SchrittBerechnungBetrag
Gold, 24K pro Grammrates.EURXAU110,00 €
Feingehalt18K, Punze 7500,75
Metallwert5,2 g × 110,00 € × 0,75429,00 €
Arbeitslohn5,2 g × 12,00 €62,40 €
Nettopreis429,00 € + 62,40 €491,40 €
MwSt.491,40 € × 20 %98,28 €
Gesamt589,68 €

Nun eine 10 g schwere 22-Karat-Kette mit 8 % Arbeitslohn zum selben Goldpreis. Mit dem Feingehalt 22/24 beträgt der Metallwert 1.008,33 €, der Arbeitslohn 8 % davon (80,67 €), der Nettopreis 1.089,00 € und der Gesamtpreis inklusive Mehrwertsteuer 1.306,80 €. Mit dem Punzierungsfaktor 916 läge der Metallwert stattdessen bei 1.007,60 €.

Nur der Metallwert folgt dem Goldpreis, daher bewegt sich der Gesamtpreis weniger stark als Gold selbst. Steigt Gold um 1 % auf 111,10 € pro Gramm, beträgt der Metallwert des 18-Karat-Rings 433,29 € und sein Gesamtpreis 594,83 €, ein Plus von 5,15 € oder 0,87 %. Mit einem prozentualen Arbeitslohn würde der Gesamtpreis dem Goldpreis enger folgen.

Checkliste für den Produktivbetrieb

  • Bewahren Sie den API-Schlüssel in einer Umgebungsvariablen auf dem Server auf. Der Rechner-Endpunkt gibt Preise zurück, niemals den Schlüssel.
  • Zeigen Sie price_time bei jedem Angebot an, damit Kunden und Mitarbeitende sehen, wie aktuell der Goldpreis ist.
  • Fixieren Sie den Preis bei der Bestellung: Speichern Sie Goldpreis, Zeitstempel, Feingehalt und jede Einzelposition mit der Bestellung, statt später neu zu rechnen.
  • Rechnen Sie am Wochenende, wenn der Goldmarkt geschlossen ist, mit unveränderten Preisen. Das ist kein veralteter Cache.
  • Verfolgen Sie Ihre Nutzung mit dem Usage-Endpunkt, der nicht auf Ihr Kontingent angerechnet wird, und lassen Sie sich warnen, bevor das Monatskontingent aufgebraucht ist.
  • Behandeln Sie Fehler 105 (Monatskontingent aufgebraucht), indem Sie den zwischengespeicherten Preis ausliefern und Ihr Team alarmieren. Das Kontingent wird am 1. um 00:00 UTC zurückgesetzt, und ein Upgrade gilt sofort.
  • Um auf große Preisbewegungen zu reagieren, etwa um Ihren Katalog neu zu bepreisen, nutzen Sie Preisalarme mit Webhooks, statt häufiger abzufragen.

Vergleichen Sie Tarife und Kontingente auf der Preisseite oder erstellen Sie ein kostenloses Konto, um einen Schlüssel zu erhalten und den Rechner mit echten Preisen auszuprobieren.

Probieren Sie es mit Ihren eigenen Daten aus

Erstellen Sie ein kostenloses Konto und senden Sie Ihre erste Anfrage in unter zwei Minuten. 100 Anfragen pro Monat, keine Kreditkarte.

Weiterlesen