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‎%‎
جميع المقالات
درس تطبيقيالتجارة الإلكترونيةTypeScriptPython

أنشئ حاسبة لأسعار المجوهرات بأسعار الذهب اللحظية

أنشئ حاسبة لأسعار المجوهرات من جهة الخادم بلغتي TypeScript وPython، بسعر غرام الذهب اللحظي ونقاء العيار وأجرة المصنعية والضريبة وتخزين مؤقت يحفظ حصتك.

بقلم MetalAPI Team··وقت القراءة: 10 دقيقة

ما الذي تفعله الحاسبة

تُسعَّر المجوهرات عادةً انطلاقًا من أربعة مدخلات: وزن القطعة، ونقاء ذهبها، وسعر الذهب الحالي، وأجرة الصائغ، ثم تُضاف الضريبة. والحاسبة التي تؤدي ذلك لكل صفحة منتج أو طلب عرض سعر تحتاج إلى سعر غرام الذهب بعملة المتجر. وتحتاج إليه بسرعة، وضمن حصة API، ودون كشف مفتاح API للمتصفح.

يبني هذا الدرس خدمة صغيرة من جهة الخادم بلغة TypeScript (Node.js) وبلغة Python. يرسل المتصفح، أو الواجهة الخلفية لمتجرك، وزنًا وعيارًا إلى نقطة النهاية الخاصة بك، فيدمجهما خادمك مع سعر ذهب مخزَّن مؤقتًا من MetalAPI ويُرجع تفصيلًا كاملًا للسعر. المعادلة:

معادلة التسعير
metal value   = weight (g) × gold price per gram (24K) × purity
making charge = per gram, a percentage of the metal value, or a flat fee
net price     = metal value + making charge
VAT           = net price × VAT rate
total         = net price + VAT

سعر الذهب الفوري سعر مرجعي لسوق الجملة. أما أسعار التجزئة فتعكس أيضًا التصميم والأحجار والهالك وهامش الربح، وعادةً ما تكون أسعار إعادة الشراء أدنى من السعر الفوري. تعامل مع هذه الحاسبة على أنها الجزء المرتبط بالمعدن من تسعيرك، وأضف قواعد عملك الخاصة فوقها.

سعر غرام الذهب بعملتك

تُرجع نقطة النهاية /latest الأسعار بأي عملة أساس، وبالنسبة إلى المعادن بأي وحدة وزن. ويمنحك طلب واحد سعر غرام واحد من الذهب الخالص باليورو:

bash
curl "https://metalapi.com/api/v1/latest?base=EUR&currencies=XAU&unit=gram" \
  -H "X-API-Key: YOUR_API_KEY"
الاستجابة (قيم توضيحية)
{
  "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 هو سعر وحدة واحدة من XAU، وهي هنا غرام واحد، بعملة الأساس، وهو الرقم الذي تحتاج إليه الحاسبة. أما rates.XAU فهو المقلوب: عدد غرامات الذهب لكل يورو. تتضمن الاستجابة unit كلما كانت الوحدة غير أونصة التروي الافتراضية، وtimestamp هو وقت بيانات السعر، فاعرضه بجوار كل عرض سعر.

تعمل عملة الأساس والوحدة في جميع الخطط، بما فيها Free، ويمكن أن يتضمن الطلب نفسه XAG وXPT وXPD إذا كنت تبيع أيضًا قطعًا من الفضة أو البلاتين. ويمكن للخطط المدفوعة كذلك استخدام نقطة نهاية العيار التي تُرجع أسعار العيارات مثل XAU22K مباشرةً، أو جميع العيارات دفعة واحدة باستخدام carat=all. أما هذا الدرس فيطبّق النقاء بنفسه حتى تعمل الشيفرة في أي خطة وتستطيع استخدام قيم النقاء المدموغة، وهذا هو الموضوع التالي.

النقاء وأجرة المصنعية وضريبة القيمة المضافة

يعبّر العيار عن محتوى الذهب بأجزاء من 24، لذا فإن معامل النقاء هو karat / 24، وهو الاصطلاح الذي تعتمده نقطة النهاية /carat. لكن أسواقًا كثيرة تدمغ الذهب بدرجة نقائه بالأجزاء من الألف بدلًا من ذلك، وفي بعض العيارات يختلف الرقمان اختلافًا طفيفًا:

العيارkarat / 24الدمغة الشائعة
عيار 241.0000999
عيار 220.9167916
عيار 210.8750875
عيار 180.7500750
عيار 140.5833585
عيار 90.3750375

لخاتم عيار 14 وزنه 5.2 غرام بسعر توضيحي قدره 110 يورو للغرام، يعطي 14/24 قيمة معدن قدرها 333.67 يورو، ويعطي 0.585 قيمة قدرها 334.62 يورو. ولا أحد منهما خاطئ. استخدم الاصطلاح الذي تعتمده دمغاتك وفواتيرك، ولهذا تأخذ الشيفرة أدناه معامل نقاء بدلًا من العيار. ويتناول دليل أسعار العيارات الخلفية بمزيد من التفصيل.

تأخذ أجرة المصنعية عادةً أحد ثلاثة أشكال، وتدعمها نسخة TypeScript جميعًا:

  • لكل غرام: مبلغ ثابت عن كل غرام من القطعة، مثل 12 يورو للغرام.
  • نسبة مئوية: حصة من قيمة المعدن، مثل 8%، فترتفع وتنخفض مع سعر الذهب.
  • مبلغ مقطوع: رسم ثابت لكل قطعة، وهو شائع في القطع الصغيرة والأعمال المصنوعة حسب الطلب.

تأتي ضريبة القيمة المضافة أو ضريبة المبيعات في النهاية. ويعتمد معدلها، وما تُطبَّق عليه، على البلد: فبعض البلدان تفرضها على السعر كاملًا، وبعضها يعامل ذهب الاستثمار معاملة مختلفة عن المجوهرات، وبعضها لا يفرضها إلا على أجرة الصنعة. احتفظ بالمعدل والمبلغ الخاضع للضريبة في الإعدادات، وتأكد منهما مع محاسبك، وقرّب كل بند إلى السنت حتى يساوي مجموع بنود الفاتورة إجماليها.

التخزين المؤقت للبقاء ضمن حصتك

تحدّث كل خطة الأسعار على فترات ثابتة، وتقع التحديثات عند مضاعفات تلك الفترة محسوبةً من الساعة 00:00 UTC: عند الدقائق :00 و:10 و:20 وهكذا في خطة Standard، ومرة واحدة يوميًا عند منتصف الليل بتوقيت UTC في خطة Free. وكل طلب بين حدّين من هذه الحدود يُرجع الأسعار نفسها، لذا فإن التخزين المؤقت المثالي يتجدد مرة واحدة عند كل حد. والتجديد عند كل حد على مدار الساعة يكلّف ما يلي في شهر من 31 يومًا:

الخطة (الحصة الشهرية)فترة التحديثالطلبات عند كل حدضمن الحصة؟
Free (100)يوميًا31نعم
Lite (1,000)30 دقيقة1,488لا: جدّد كل ساعة بدلًا من ذلك (744)
Standard (10,000)10 دقائق4,464نعم
Advanced (50,000)دقيقة واحدة44,640نعم، مع هامش أقل للاستدعاءات الأخرى

قاعدتان إضافيتان تجعلان التخزين المؤقت متينًا. الأولى الطلب الواحد (single flight): عندما تنتهي صلاحية ذاكرة التخزين المؤقت تحت الضغط، يجلب الطلب الأول سعرًا جديدًا وتنتظر الطلبات المتزامنة تلك النتيجة بدلًا من أن يستدعي كل منها الواجهة. والثانية تقديم القيمة القديمة عند الخطأ (serve stale on error): إذا تعذّر الوصول إلى الواجهة، فواصل التسعير بآخر سعر صالح لبضع ساعات وأعد المحاولة مرة كل دقيقة، بدلًا من إفشال كل عملية دفع. صحيح أن الطلبات التي تفشل بخطأ في الخادم لا تُحتسب، لكن لا داعي مع ذلك لإرسال دفعة منها أثناء الانقطاع.

تعيش ذاكرة التخزين المؤقت أدناه في ذاكرة العملية، وهذا مثالي لخادم واحد. أما على المنصات عديمة الخوادم (serverless) أو مع عدة مثيلات، فيحتفظ كل مثيل بنسخته الخاصة. ضع السعر في مخزن مشترك مثل Redis، أو استخدم ذاكرة البيانات المؤقتة في إطار عملك بمدة الصلاحية نفسها.

التنفيذ بلغة TypeScript

تعمل الوحدة أدناه على Node.js 18 أو أحدث ولا تعتمد على أي حزم. فهي تجلب سعر الغرام باليورو، وتخزّنه مؤقتًا حتى الحد التالي للخطة، وتحوّل الوزن ومعامل النقاء وأجرة المصنعية إلى تفصيل مُسعَّر:

gold-quote.ts
// gold-quote.ts — Node 18+. The API key stays on the server.
const API_KEY = process.env.METALAPI_KEY!;
const CURRENCY = "EUR";
const PLAN_INTERVAL_S = 600; // Standard plan: prices update every 10 minutes
const MIN_TTL_S = 0;         // Lite plan: set 3600 to stay under 1,000 requests a month

interface GoldPrice {
  perGram: number;   // fine (24K) gold, CURRENCY per gram
  timestamp: number; // time of the price data (Unix seconds)
  fetchedAt: number;
  expiresAt: number;
}

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

// Prices only change at multiples of the plan interval (counted from 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, // a few seconds of slack
  };
}

export function goldPerGram(): Promise<GoldPrice> {
  if (cached && Date.now() < cached.expiresAt) return Promise.resolve(cached);
  // Single flight: concurrent requests share one API call.
  inflight ??= fetchGold()
    .then((fresh) => (cached = fresh))
    .catch((err) => {
      // Keep quoting the last good price for up to 6 hours; try again in a minute.
      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% of the metal value
  | { 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),
  };
}

تستحق بعض الأسطر نظرة أقرب. تقرّب الدالة nextBoundary() الوقت صعودًا إلى المضاعف التالي لفترة الخطة، لذا في خطة Standard تنتهي صلاحية سعر جُلب الساعة 10:03 عند 10:10:05، أي بعد التحديث التالي ببضع ثوانٍ، لا عند 10:13. ومع ضبط MIN_TTL_S على 3600 في خطة Lite، ينتظر التجديد التالي أول حد يبعد ساعة على الأقل، ما يحصر الاستهلاك في 24 طلبًا يوميًا. أما الفرع .catch فهو قاعدة تقديم القيمة القديمة: يُرجع آخر سعر صالح ويجدول المحاولة التالية بعد دقيقة، فلا يكلّف الانقطاع أكثر من طلب فاشل واحد في الدقيقة بدلًا من طلب فاشل لكل زائر.

اعرض الوحدة عبر نقطة نهاية HTTP صغيرة. وتحقّق من المدخلات بصرامة: لا تقبل إلا درجات النقاء التي تبيعها ونطاق وزن معقولًا. أجرة المصنعية ومعدل الضريبة إعدادات من جهة الخادم، فلا تقبلهما من العميل أبدًا.

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

// Purity factors as stamped on your pieces (see the karat table above).
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 {
    // Making charge and VAT are server-side settings, never client input.
    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"));

يُرجع الطلب /api/jewelry-quote?karat=18&grams=5.2 تفصيلًا كالتالي (بسعر ذهب توضيحي):

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
}

التنفيذ بلغة Python

تتبع نسخة Python التصميم نفسه. فهي تستخدم Decimal للمبالغ المالية حتى لا تتسرب إليها أخطاء الفاصلة العائمة الثنائية، وتستخدم قفلًا (lock) حتى تتشارك الطلبات المتزامنة في خادم متعدد الخيوط استدعاءً واحدًا للواجهة. وللاختصار، لا تدعم إلا أجرة مصنعية لكل غرام:

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 plan: prices update every 10 minutes
MIN_TTL_S = 0          # Lite plan: set 3600 to stay under 1,000 requests a month

_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:  # one API call at a time; waiting threads reuse its result
        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  # serve the last good price, retry in a minute
                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),
    }

يمكن لأي إطار عمل ويب تقديم هذه الوحدة. وفي FastAPI تعمل نقاط النهاية المعرَّفة بـ def العادية ضمن مجمّع خيوط (thread pool)، وهذا بالضبط ما وُجد القفل من أجله:

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

from fastapi import FastAPI, HTTPException, Query

from gold_quote import quote

# Purity factors as stamped on your pieces.
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.")

تُرجع نسخة Python المبالغ كسلاسل نصية، بينما تُرجعها نسخة TypeScript كأرقام. وإذا كان عرض السعر سيدخل نظام طلبات، فخزّن المبالغ كسلاسل عشرية أو كسنتات صحيحة، لا كأرقام فاصلة عائمة.

مثال تطبيقي

إليك تسعير خاتم عيار 18 خطوة بخطوة، بسعر ذهب توضيحي قدره 110.00 يورو للغرام، وأجرة مصنعية قدرها 12 يورو للغرام، وضريبة قيمة مضافة بنسبة 20%:

الخطوةالحسابالمبلغ
الذهب عيار 24 للغرامrates.EURXAU€110.00
النقاءعيار 18، دمغة 7500.75
قيمة المعدن5.2 g × €110.00 × 0.75€429.00
أجرة المصنعية5.2 g × €12.00€62.40
السعر الصافي€429.00 + €62.40€491.40
ضريبة القيمة المضافة€491.40 × 20%€98.28
الإجمالي€589.68

والآن سلسلة عيار 22 وزنها 10 غرامات بأجرة مصنعية 8% وبسعر الذهب نفسه. مع نقاء 22/24 تبلغ قيمة المعدن 1,008.33 يورو، وأجرة المصنعية 8% منها (80.67 يورو)، والسعر الصافي 1,089.00 يورو، والإجمالي مع الضريبة 1,306.80 يورو. أما مع معامل الدمغة 916 فتصبح قيمة المعدن 1,007.60 يورو.

قيمة المعدن وحدها هي التي تتبع سعر الذهب، لذا يتحرك الإجمالي بنسبة أقل من حركة الذهب نفسه. فإذا ارتفع الذهب 1% إلى 111.10 يورو للغرام، تصبح قيمة المعدن في خاتم عيار 18 مساوية لـ 433.29 يورو وإجماليه 594.83 يورو، بزيادة قدرها 5.15 يورو أو 0.87%. أما أجرة المصنعية بالنسبة المئوية فتجعل الإجمالي يتبع سعر الذهب عن كثب أكثر.

قائمة التحقق قبل الإطلاق

  • احتفظ بمفتاح API في متغير بيئة على الخادم. تُرجع نقطة نهاية الحاسبة الأسعار، ولا تُرجع المفتاح أبدًا.
  • اعرض price_time مع كل عرض سعر، ليرى العملاء والموظفون مدى حداثة سعر الذهب.
  • ثبّت السعر عند تقديم الطلب: خزّن سعر الذهب وطابعه الزمني والنقاء ومبلغ كل بند مع الطلب بدلًا من إعادة الحساب لاحقًا.
  • توقّع ثبات الأسعار خلال عطلة نهاية الأسبوع حين تكون سوق الذهب مغلقة، فهذا ليس تخزينًا مؤقتًا قديمًا.
  • تابع الاستهلاك عبر نقطة نهاية الاستهلاك، التي لا تُحتسب من حصتك، ونبّه نفسك قبل نفاد حصة الشهر.
  • عالج الخطأ 105 (استنفاد الحصة الشهرية) بتقديم السعر المخزَّن مؤقتًا وتنبيه فريقك. تُعاد تعيين الحصة في اليوم الأول من الشهر الساعة 00:00 UTC، وتسري الترقية فورًا.
  • للتفاعل مع تحركات الأسعار الكبيرة، كإعادة تسعير الكتالوج، استخدم تنبيهات الأسعار عبر خطافات الويب بدلًا من الاستطلاع بوتيرة أعلى.

قارن الخطط والحصص في صفحة الأسعار، أو أنشئ حسابًا مجانيًا لتحصل على مفتاح وتجرّب الحاسبة بأسعار حقيقية.

جرّبه مع بياناتك الخاصة

أنشئ حسابًا مجانيًا وأرسل طلبك الأول في أقل من دقيقتين. 100 طلب شهريًا، دون بطاقة ائتمان.

تابع القراءة