MetalAPI
XAU3.997,89−%1,54XAG53,936−%1,51XPT1.554,47−%0,83XPD1.047,03−%0,44XCU6,5080+%2,29WTI103,27+%3,82BRENT111,70−%1,30EUR1,120400−%0,01GBP1,337500−%0,01JPY0,0063761+%0,14CHF1,213445−%0,25TRY0,020125−%0,12BTC80.329,75−%0,55ETH2.329,36−%1,13
Tüm yazılar
AkışJavaScriptEğitim

Server-Sent Events ile Gerçek Zamanlı Metal Fiyatı Akışı

Altın ve gümüş fiyatlarını Server-Sent Events ile anlık alın: /stream endpoint'i, olaylar, EventSource, Node.js, yeniden bağlanma, kota ve canlı fiyat şeridi.

Yazar: MetalAPI Team··10 dk okuma

Akış mı, sorgulama mı?

Sorgulama (polling), fiyatları belirli aralıklarla ister. Akış (streaming) ise tek bir HTTP bağlantısını açık tutar ve yeni fiyatlar yayımlandıkça sunucunun bunları size göndermesini sağlar. MetalAPI'nin akışı Server-Sent Events (SSE) kullanır: text/event-stream içerik türüne sahip, açık kalan ve adlandırılmış olaylar ileten sıradan bir HTTP yanıtı. WebSocket kütüphanesi gerektirmez, çoğu proxy'den sorunsuz geçer ve tarayıcılar EventSource ile bunu yerleşik olarak destekler.

Akışın avantaj sağlayıp sağlamayacağı planınızın güncelleme aralığına bağlıdır. Bir akış, bağlandığında bir istek ve açık kaldığı sürece dakikada bir istek harcar; fiyatların ne sıklıkla değiştiği fark etmez. Sorgulamada ise her çağrı bir istektir. 31 günlük bir ayda fiyatları günün her saati izleyen tek bir istemci için:

PlanGüncelleme aralığıBu aralıkla sorgulamaSürekli açık tek akış
Pro1 dk44.640 istekyaklaşık 45.400 istek
Ultra, Business15 sn178.560 istekyaklaşık 45.400 istek
Enterprise5 sn535.680 istekyaklaşık 45.400 istek

Akış rakamına, aşağıda açıklanan saatlik yeniden bağlanmaların her biri için bir istek dahildir. Pro planda akış ile dakikada bir sorgulama aşağı yukarı aynı maliyettedir; akışın avantajı, ince ayar gerektiren bir sorgulama döngüsü olmadan yeni fiyatların yayımlanır yayımlanmaz gelmesidir. Ultra, Business ve Enterprise planlarında ise akış, tam hızda sorgulamadan çok daha ucuzdur.

Fiyatın sayfa görüntüleme başına yalnızca bir kez doğru olmasının yettiği durumlarda, gece çalışan işlerde ve eşik kontrollerinde akış doğru araç değildir. İlk ikisi için önbelleğe alınmış bir /latest çağrısı, üçüncüsü için ise webhook'larla fiyat alarmları kullanın.

/stream endpoint'i

http
GET https://metalapi.com/api/v1/stream?base=USD&currencies=XAU,XAG,XPT,XPD

Akış, /latest ile aynı base, currencies (veya symbols) ve unit parametrelerini kabul eder (bkz. latest endpoint dokümantasyonu) ve aynı kimlik doğrulama yöntemlerini destekler: sorgu dizesinde api_key, bir Authorization: Bearer header'ı veya bir X-API-Key header'ı. Parametreler akış açılmadan önce doğrulanır; bu nedenle bilinmeyen bir sembol veya birim, boş bir akış yerine normal bir JSON hatası döndürür.

PlanAkıştaki fiyat güncellemeleriAylık istek
Pro60 saniyede bir100.000
Ultra15 saniyede bir500.000
Business15 saniyede bir500.000
Enterprise5 saniyede bir1.500.000

Daha düşük planlar, gerçek zamanlı akışın Pro ve üzeri planlarda sunulduğunu belirten bir mesajla birlikte 106 hatası (HTTP 403) alır. Her bağlantı için birkaç limit geçerlidir:

  • API anahtarı başına en fazla 5 eşzamanlı akış. Altıncı bağlantı 104 (HTTP 429) ile reddedilir.
  • Bağlantılar en fazla 60 dakika sürer. Bir saat sonra sunucu bir reconnect olayı gönderir ve akışı kapatır; istemci de yeniden bağlanır.
  • Anahtar her dakika yeniden kontrol edilir. Anahtar iptal edilmişse veya aylık kota tükenmişse sunucu bir error olayı gönderir ve akışı kapatır.
  • Bağlanmak, diğer tüm istekler gibi dakika başına limitten düşülür. Bu nedenle hızlı bir yeniden bağlanma döngüsüne takılan bir istemci 104 hataları almaya başlar.

Sunucu ne gönderir

Bir akışın başlangıcı ham metin olarak şöyle görünür (fiyatlar örnektir):

text/event-stream
retry: 5000
event: prices
data: {"timestamp":1792834892,"date":"2026-10-24","base":"USD","rates":{"XAU":0.0002501319,"USDXAU":3997.89,"XAG":0.0185392857,"USDXAG":53.94}}

: ping 1792834907000

event: prices
data: {"timestamp":1792834952,"date":"2026-10-24","base":"USD","rates":{"XAU":0.0002500812764,"USDXAU":3998.7,"XAG":0.01852194851,"USDXAG":53.99}}
OlayVeriNe yapmalı
pricesTam bir anlık görüntü: timestamp, date, base, rates ve birim troy ons değilse unitGösterdiğiniz fiyatları bununla değiştirin
reconnect{"reason":"max connection time reached"}Hiçbir şey; akış kapandığında yeniden bağlanın
errorREST API ile aynı kodları kullanan code, type ve infoKoda göre durun veya bekleyip yeniden deneyin
: ping …15 saniyede bir gönderilen bir yorum satırıAyrıştırıcılar bunu yok sayar; bağlantının canlı olduğunun işareti olarak kullanın

İstemci kodunu üç özellik belirler. Her prices olayı, istediğiniz sembollerin tam bir anlık görüntüsüdür, asla bir fark (delta) değildir; bu nedenle yeniden bağlanan bir istemci ilk olayla birlikte yeniden günceldir. Bu ilk olay, bağlandıktan hemen sonra gelir. Sonrasında bir olay yalnızca kurlar değiştiğinde gönderilir: sunucu planınızın aralığında kontrol eder ve hiçbir şey değişmediyse sessiz kalır; hafta sonları metaller için bu normaldir. Heartbeat'lerle birlikte gelen sessizlik sorun değildir; heartbeat'siz sessizlik ise bağlantının koptuğu anlamına gelir.

Olaylar id taşımaz, bu nedenle Last-Event-ID hiçbir rol oynamaz. Yeniden oynatılacak bir şey yoktur, çünkü her anlık görüntü kendisinden önceki her şeyin yerini alır. rates anahtarları REST formatını izler: USDXAU bir troy ons altının ABD doları cinsinden fiyatıdır, XAU ise bunun tersidir.

Tarayıcıda EventSource ile

EventSource bağlantıyı, ayrıştırmayı ve yeniden bağlanmaları sizin yerinize yönetir. Özel header gönderemediği için anahtar sorgu dizesine yazılır. Alan adlarınızla sınırlandırılmış bir anahtar kullanın: tarayıcı istekle birlikte bir Origin header'ı gönderir ve sınırlandırılmış bir anahtar, başka sitelerden açılan akışları 107 hatasıyla reddeder.

Tarayıcı
// Alan adlarınızla sınırlandırılmış bir anahtar kullanın: sayfayı açan herkes onu görebilir.
const url = "https://metalapi.com/api/v1/stream?api_key=YOUR_RESTRICTED_KEY&base=EUR&currencies=XAU,XAG&unit=gram";
const FATAL = [101, 102, 103, 105, 106, 107];
const es = new EventSource(url);

es.addEventListener("prices", (e) => {
  const { timestamp, base, rates } = JSON.parse(e.data);
  document.querySelector("#gold").textContent = rates[`${base}XAU`].toFixed(2);
  document.querySelector("#silver").textContent = rates[`${base}XAG`].toFixed(3);
  document.querySelector("#updated").textContent = new Date(timestamp * 1000).toLocaleTimeString();
});

es.addEventListener("error", (e) => {
  if (e instanceof MessageEvent && e.data) {
    // MetalAPI'nin akışı kapatmadan önce gönderdiği hata: {"code":105,"type":"quota_exceeded",...}
    const err = JSON.parse(e.data);
    console.warn("MetalAPI stream error", err.code, err.info);
    if (FATAL.includes(err.code)) es.close(); // yeniden bağlanmak yine başarısız olur
  } else if (es.readyState === EventSource.CLOSED) {
    console.warn("Stream refused; check the key and plan with /usage");
  }
  // Aksi halde bağlantı kopmuştur ve tarayıcı kendiliğinden yeniden bağlanır.
});

error dinleyicisine iki farklı şey gelir. Bir bağlantı sorunu düz bir Event olarak gelir ve EventSource, sunucunun retry satırıyla belirlenen 5 saniyenin ardından kendiliğinden yeniden bağlanır. MetalAPI'nin gönderdiği bir hata ise data alanında JSON hatasını taşıyan bir MessageEvent olarak gelir ve sunucu hemen ardından akışı kapatır. Yeniden denemenin düzeltemeyeceği kodlar için close() çağırın; aksi halde tarayıcı yeniden bağlanmayı ve reddedilmeyi sürdürür.

İlk yanıt zaten bir HTTP hatasıysa, örneğin plan akışı içermediği için 106 ise, EventSource yanıt gövdesini size sunmaz ve yeniden denemez: readyState değeri CLOSED olur. Nedenini öğrenmeniz gerektiğinde ücretsiz usage endpoint'i anahtarın planını gösterir.

Doğrudan tarayıcıdan açılan akışlar, birkaç kullanıcılı dahili paneller için uygundur. Açık her sekme ayrı bir akıştır: dakikada bir istek harcar ve anahtar başına 5 eşzamanlı akış limitine dahil edilir. Herkese açık bir sayfa için sunucunuzda tek bir akış çalıştırın ve aşağıdaki fiyat şeridinde olduğu gibi onu tüm ziyaretçilere dağıtın.

Node.js'te fetch ile

Sunucuda anahtar bir header içinde gönderilebilir ve yeniden bağlanmaları tam olarak siz kontrol edersiniz. Node.js 18 ve sonrası, akış halinde yanıt gövdelerini destekleyen fetch içerir; bu sayede eksiksiz bir istemci, hiçbir bağımlılık olmadan tek bir modüle sığar:

metal-stream.ts
// metal-stream.ts — Node.js 18+, bağımlılık yok
export interface Prices {
  timestamp: number;
  date: string;
  base: string;
  unit?: string;
  rates: Record<string, number>;
}

const STREAM_URL = "https://metalapi.com/api/v1/stream?base=USD&currencies=XAU,XAG,XPT,XPD";
const FATAL = new Set([101, 102, 103, 105, 106, 107]); // yeniden denemek işe yaramaz
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

class FatalStreamError extends Error {}

export async function streamPrices(onPrices: (p: Prices) => void): Promise<never> {
  let retryMs = 5_000; // sunucu `retry: 5000` gönderir
  let failures = 0;
  for (;;) {
    const ctrl = new AbortController();
    let watchdog: ReturnType<typeof setTimeout> | undefined;
    const kick = () => {
      clearTimeout(watchdog);
      watchdog = setTimeout(() => ctrl.abort(), 45_000); // 3 kaçırılmış heartbeat
    };
    try {
      const res = await fetch(STREAM_URL, {
        headers: { Authorization: `Bearer ${process.env.METALAPI_KEY}`, Accept: "text/event-stream" },
        signal: ctrl.signal,
      });
      if (!res.ok || !res.body) {
        const body = await res.json().catch(() => null);
        const code = body?.error?.code;
        throw FATAL.has(code) ? new FatalStreamError(`${code}: ${body.error.info}`) : new Error(`HTTP ${res.status}`);
      }
      kick();
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
      for (;;) {
        const { value, done } = await reader.read();
        if (done) break;
        kick();
        buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
        let end: number;
        while ((end = buffer.indexOf("\n\n")) !== -1) {
          const block = buffer.slice(0, end);
          buffer = buffer.slice(end + 2);
          let event = "message";
          const data: string[] = [];
          for (const line of block.split("\n")) {
            if (!line || line.startsWith(":")) continue; // heartbeat yorumu
            const i = line.indexOf(":");
            const field = i === -1 ? line : line.slice(0, i);
            const val = i === -1 ? "" : line.slice(i + 1).replace(/^ /, "");
            if (field === "event") event = val;
            else if (field === "data") data.push(val);
            else if (field === "retry" && /^\d+$/.test(val)) retryMs = Number(val);
          }
          if (event === "prices") {
            failures = 0;
            onPrices(JSON.parse(data.join("\n")));
          } else if (event === "error") {
            const err = JSON.parse(data.join("\n"));
            if (FATAL.has(err.code)) throw new FatalStreamError(`${err.code}: ${err.info}`);
          }
          // `reconnect`: sunucu ardından bağlantıyı kapatır; döngü yeniden bağlanır.
        }
      }
    } catch (err) {
      if (err instanceof FatalStreamError) throw err;
      failures++;
      console.warn("stream interrupted:", (err as Error).message);
    } finally {
      clearTimeout(watchdog);
    }
    // Normal kapanıştan sonra 5 sn; hatalar sürdükçe bekleme süresi bir dakikaya kadar artar.
    const backoff = Math.min(60_000, retryMs * 2 ** Math.min(failures, 4));
    await sleep(failures ? backoff + Math.random() * 1_000 : retryMs);
  }
}

Herhangi bir betikten veya servisten şöyle kullanın:

watch.ts
import { streamPrices } from "./metal-stream";

streamPrices((p) => {
  console.log(new Date(p.timestamp * 1000).toISOString(), "gold", p.rates.USDXAU, "silver", p.rates.USDXAG);
}).catch((err) => {
  console.error("Stream stopped:", err.message); // FATAL kodu: anahtarı, planı veya kotayı düzeltin
  process.exit(1);
});

Ayrıştırıcı SSE formatını izler: olaylar boş bir satırla ayrılır, event: olayın adını verir, data: satırları satır sonlarıyla birleştirilir ve iki nokta üst üste ile başlayan satırlar yorumdur. Bir sonraki bölümdeki yeniden bağlanma kurallarını uyguladığı sürece, istek header'larını ayarlamanıza izin veren herhangi bir SSE istemci kütüphanesi de işinizi görür.

Yeniden bağlanma ve hata yönetimi

Yukarıdaki modül, her akış istemcisinde bulunması gereken kısa bir kural dizisini izler:

DurumSinyalTepki
Saatlik yenilemereconnect olayı, ardından akışın sona ermesiSunucunun retry gecikmesinden (5 sn) sonra yeniden bağlanın
Ağ kesintisiOkuma hatası veya reconnect olmadan sona eren akışÜstel geri çekilme (exponential backoff) ve rastgele gecikmeyle (jitter), en fazla 60 sn arayla yeniden bağlanın
Sessiz bağlantı45 sn boyunca ne veri ne de heartbeatBağlantıyı iptal edip yeniden bağlanın; üç heartbeat kaçırılmıştır
Geçici ret104, 500, 503Bekleyip yeniden deneyin
Kalıcı ret101, 102, 103, 105, 106, 107Durun ve bir kişiyi uyarın

Watchdog (bekçi zamanlayıcı) göründüğünden daha önemlidir. TCP sıfırlaması olmadan ölen bir bağlantı, örneğin bir NAT ağ geçidinin veya yük dengeleyicinin arkasında, işletim sistemi fark edene kadar dakikalarca açık görünebilir. 15 saniyede bir gelen heartbeat sayesinde istemci bunu 45 saniye içinde tespit edebilir.

Yeniden bağlanma döngülerine dikkat edin: her başarılı bağlantı bir istek harcar ve her deneme dakika başına limitten düşülür. Bir şey sürekli başarısız olduğunda üstel geri çekilme zararı sınırlar.

Kotanın tükenmesi (105) ayrı bir uyarıyı hak eder. Akış bunu bir sonraki dakikalık kontrolde bir error olayı olarak bildirir ve kota ayın 1'inde (UTC) sıfırlanana ya da plan yükseltilene kadar yeniden bağlanmak işe yaramaz.

Canlı fiyat şeridi oluşturmak

Herkese açık bir fiyat şeridi, her ziyaretçi için ayrı bir MetalAPI akışı açmamalıdır. Bunun yerine küçük bir aktarıcı (relay), sunucuda tek bir akış tutar ve her anlık görüntüyü kendi SSE endpoint'i üzerinden tüm tarayıcılara iletir. Böylece kullanımınız ziyaretçi sayısına değil aktarıcı sayısına bağlı olur ve anahtar sunucudan hiç çıkmaz:

relay.ts
// relay.ts — sunucuda tek bir MetalAPI akışı, istediğiniz sayıda tarayıcı.
import { readFileSync } from "node:fs";
import http from "node:http";
import { streamPrices } from "./metal-stream";

const page = readFileSync(new URL("./ticker.html", import.meta.url));
const clients = new Set<http.ServerResponse>();
let latest: string | null = null;

const broadcast = (chunk: string) => {
  for (const res of clients) res.write(chunk);
};

streamPrices((p) => {
  latest = JSON.stringify({ timestamp: p.timestamp, base: p.base, rates: p.rates });
  broadcast(`event: prices\ndata: ${latest}\n\n`);
}).catch((err) => {
  console.error("upstream stream stopped:", err.message);
  process.exit(1);
});

setInterval(() => broadcast(": ping\n\n"), 15_000);

http.createServer((req, res) => {
  if (req.url === "/") return void res.writeHead(200, { "Content-Type": "text/html" }).end(page);
  if (req.url !== "/prices") return void res.writeHead(404).end();
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-store",
    "X-Accel-Buffering": "no",
  });
  res.write("retry: 3000\n\n");
  if (latest) res.write(`event: prices\ndata: ${latest}\n\n`); // yeni izleyiciler fiyatları hemen alır
  clients.add(res);
  req.on("close", () => clients.delete(res));
}).listen(3000, () => console.log("Ticker on http://localhost:3000"));

Aktarıcı, en son anlık görüntüyü her yeni izleyiciye hemen gönderir, yalnızca zaman damgasını, baz para birimini ve kurları iletir ve kendi heartbeat'lerini gönderir. Sayfanın, fiyat şeridini çizmek ve her fiyatı önceki güncellemeye göre yönüne bakarak renklendirmek için yalnızca birkaç satıra ihtiyacı vardır:

ticker.html
<!doctype html>
<meta charset="utf-8">
<title>Live metal prices</title>
<style>
  .ticker { display: flex; flex-wrap: wrap; gap: 1.5rem; font: 15px/1.4 system-ui, sans-serif; font-variant-numeric: tabular-nums; }
  .up { color: #15803d; }
  .down { color: #dc2626; }
</style>

<div id="ticker" class="ticker">Loading prices…</div>

<script>
  const NAMES = { XAU: "Gold", XAG: "Silver", XPT: "Platinum", XPD: "Palladium" };
  const previous = {};
  const ticker = document.getElementById("ticker");

  new EventSource("/prices").addEventListener("prices", (e) => {
    const { base, rates, timestamp } = JSON.parse(e.data);
    const money = new Intl.NumberFormat(undefined, { style: "currency", currency: base });
    ticker.replaceChildren(
      ...Object.entries(NAMES).map(([code, name]) => {
        const price = rates[base + code];
        const el = document.createElement("span");
        if (previous[code] !== undefined && price !== previous[code]) {
          el.className = price > previous[code] ? "up" : "down";
        }
        previous[code] = price;
        el.textContent = `${name} ${money.format(price)}`;
        return el;
      }),
    );
    ticker.title = `Prices as of ${new Date(timestamp * 1000).toLocaleString()}`;
  });
</script>

METALAPI_KEY=mk_live_... npx tsx relay.ts komutuyla başlatın ve http://localhost:3000 adresini açın. Fiyat şeridi her fiyatı bir önceki olayla karşılaştırır. Bunun yerine günlük değişimi göstermek için bugünün açılış fiyatını OHLC endpoint'inden bir kez alın ve her yeni fiyatı onunla karşılaştırın.

Canlı ortam için notlar

  • Proxy tamponlamasını kapatın. MetalAPI X-Accel-Buffering: no ve Cache-Control: no-transform gönderir. Aktarıcınızın önünde nginx veya başka bir proxy varsa SSE yolu için yanıt tamponlamasını devre dışı bırakın (nginx'te proxy_buffering off;); aksi halde olaylar topluca gelir.
  • Olay akışını sıkıştırmayın. Sıkıştırma middleware'leri küçük parçaları, tamponları dolana kadar bekletebilir. text/event-stream yanıtlarını sıkıştırmanın dışında tutun.
  • Aktarıcıyı uzun ömürlü bir süreç olarak çalıştırın. Serverless fonksiyonların genellikle çalışma süresi limitleri vardır; bu da onları günün her saati bir akışı açık tutmak için uygunsuz kılar.
  • Verinin zamanını gösterin. Tarayıcının olayı aldığı zamanı değil, olayın timestamp değerini gösterin; böylece izleyiciler sakin bir hafta sonu piyasasını takılıp kalmış bir veri akışından ayırt edebilir.
  • Ekran okuyucuları yormayın. Hızlı güncellenen bir fiyat şeridini bir aria-live bölgesine koymayın; her güncellemenin seslendirilmesi rahatsız edicidir.
  • Kotayı planlayın. Sürekli açık tek bir akış ayda yaklaşık 45.400 istek kullanır; dolayısıyla Pro planda normal API çağrılarının yanında iki akışa yer vardır. Kullanımınızı usage endpoint'iyle ücretsiz kontrol edin.

API dokümantasyonu akışın parametrelerini listeler; fiyatlandırma sayfası ise hangi planların akışı içerdiğini gösterir.

Kendi verilerinizle deneyin

Ücretsiz bir hesap oluşturun ve ilk isteğinizi iki dakikadan kısa sürede gönderin. Ayda 100 istek, kredi kartı gerekmez.

Okumaya devam edin