Crie uma calculadora de preços de joias com cotações do ouro ao vivo
Crie no servidor uma calculadora de preços de joias em TypeScript e Python, com ouro por grama ao vivo, quilates, mão de obra, IVA e cache dentro da sua cota.
O que a calculadora faz
O preço de uma joia normalmente parte de quatro dados: o peso da peça, o teor do ouro, a cotação atual do ouro e a mão de obra do joalheiro, com os impostos por cima. Uma calculadora que faça isso para cada página de produto ou pedido de orçamento precisa do preço do ouro por grama na moeda da loja. E precisa desse preço rapidamente, dentro da cota da API e sem expor a chave de API ao navegador.
Este tutorial cria um pequeno serviço no servidor em TypeScript (Node.js) e em Python. O navegador, ou o backend da sua loja, envia um peso e um quilate para o seu próprio endpoint. O seu servidor combina esses dados com uma cotação do ouro da MetalAPI mantida em cache e retorna o detalhamento completo do preço. A fórmula:
valor do metal = peso (g) × preço do ouro por grama (24K) × teor
mão de obra = por grama, um percentual do valor do metal ou um valor fixo
preço líquido = valor do metal + mão de obra
IVA = preço líquido × alíquota do IVA
total = preço líquido + IVAO ouro à vista é um preço de referência do mercado atacadista. Os preços de varejo também refletem o design, as pedras, as perdas de fabricação e a margem, e os preços de recompra costumam ficar abaixo do preço à vista. Trate esta calculadora como a parte do seu preço baseada no metal e acrescente as suas próprias regras de negócio.
Ouro por grama na sua moeda
O endpoint /latest retorna preços em qualquer moeda base e, para metais, em qualquer unidade de peso. Uma única requisição fornece o preço de um grama de ouro puro em euros:
curl "https://metalapi.com/api/v1/latest?base=EUR¤cies=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 é o preço de uma unidade de XAU, aqui um grama, na moeda base. Esse é o número de que a calculadora precisa. rates.XAU é o inverso: gramas de ouro por euro. A resposta inclui unit sempre que a unidade não é a onça troy padrão, e timestamp é o horário dos dados de preço. Exiba-o ao lado de cada orçamento.
A moeda base e a unidade funcionam em todos os planos, inclusive no Free, e a mesma requisição pode incluir XAG, XPT e XPD se você também vende peças de prata ou platina. Os planos pagos também podem usar o endpoint de quilates, que retorna diretamente preços por quilate como XAU22K, ou todos os quilates de uma vez com carat=all. Este tutorial aplica o teor por conta própria para que o código funcione em qualquer plano e possa usar os valores de teor das marcações oficiais, que é o próximo assunto.
Teor, mão de obra e IVA
O quilate expressa o teor de ouro em 24 avos, então o fator de pureza é karat / 24, que é a convenção usada pelo endpoint /carat. Muitos mercados, porém, marcam o ouro pelo teor em milésimos, e para alguns quilates os dois valores diferem ligeiramente:
| Quilate | quilate / 24 | Marcação usual |
|---|---|---|
| 24K | 1,0000 | 999 |
| 22K | 0,9167 | 916 |
| 21K | 0,8750 | 875 |
| 18K | 0,7500 | 750 |
| 14K | 0,5833 | 585 |
| 9K | 0,3750 | 375 |
Para um anel de 14K com 5,2 g, a um preço ilustrativo de € 110 por grama, 14/24 resulta em um valor do metal de € 333,67 e 0.585 resulta em € 334,62. Nenhum dos dois está errado. Use a convenção das suas marcações e notas fiscais; é por isso que o código abaixo recebe um fator de pureza em vez de um quilate. O guia de preços por quilate aprofunda esse contexto.
A mão de obra costuma assumir uma de três formas, e a versão em TypeScript aceita todas elas:
- Por grama: um valor fixo para cada grama da peça, por exemplo € 12/g.
- Percentual: uma parcela do valor do metal, por exemplo 8%. Ela sobe e desce junto com o preço do ouro.
- Fixa: uma taxa fixa por peça, comum em itens pequenos e trabalhos sob encomenda.
O IVA ou imposto sobre vendas vem por último. A alíquota, e aquilo sobre o que ela incide, depende do país: alguns tributam o preço integral, outros tratam o ouro para investimento de forma diferente das joias, e outros tributam apenas a mão de obra. Mantenha a alíquota e a base de cálculo na configuração, confirme-as com o seu contador e arredonde cada linha para centavos, para que as linhas de uma nota fiscal somem exatamente o total.
Cache para ficar dentro da sua cota
Cada plano atualiza os preços em um intervalo fixo, e as atualizações caem em múltiplos desse intervalo contados a partir de 00:00 UTC: às :00, :10, :20 e assim por diante no plano Standard, e uma vez por dia à meia-noite UTC no Free. Todas as requisições entre dois desses limites retornam os mesmos preços, então o cache ideal é renovado uma vez por limite. Renovar a cada limite, 24 horas por dia, custa isto em um mês de 31 dias:
| Plano (cota mensal) | Intervalo de atualização | Requisições a cada limite | Dentro da cota? |
|---|---|---|---|
| Free (100) | Diário | 31 | Sim |
| Lite (1.000) | 30 min | 1.488 | Não: renove a cada hora (744) |
| Standard (10.000) | 10 min | 4.464 | Sim |
| Advanced (50.000) | 1 min | 44.640 | Sim, com menos folga para outras chamadas |
Mais duas regras deixam o cache robusto. Use requisição única (single flight): quando o cache expira sob carga, a primeira requisição busca um novo preço e as requisições simultâneas aguardam esse resultado, em vez de cada uma chamar a API. E sirva o último preço em caso de erro (serve stale on error): se a API não puder ser acessada, continue cotando com o último preço válido por algumas horas e tente de novo a cada minuto, em vez de derrubar cada finalização de compra. Requisições que falham com erro do servidor não são cobradas, mas ainda assim não há motivo para disparar uma rajada delas durante uma indisponibilidade.
O cache abaixo fica na memória do processo, o que é ideal para um único servidor. Em plataformas serverless ou com várias instâncias, cada instância mantém a sua própria cópia. Guarde o preço em um armazenamento compartilhado, como o Redis, ou use o cache de dados do seu framework com a mesma expiração.
A implementação em TypeScript
O módulo abaixo roda no Node.js 18 ou mais recente e não tem dependências. Ele busca o preço em euros por grama, mantém-no em cache até o próximo limite do plano e transforma um peso, um fator de pureza e uma mão de obra em um detalhamento de preço:
// gold-quote.ts — Node 18+. A chave de API fica no servidor.
const API_KEY = process.env.METALAPI_KEY!;
const CURRENCY = "EUR";
const PLAN_INTERVAL_S = 600; // Plano Standard: os preços são atualizados a cada 10 minutos
const MIN_TTL_S = 0; // Plano Lite: use 3600 para ficar abaixo de 1.000 requisições por mês
interface GoldPrice {
perGram: number; // ouro puro (24K), em CURRENCY por grama
timestamp: number; // horário dos dados de preço (segundos Unix)
fetchedAt: number;
expiresAt: number;
}
let cached: GoldPrice | null = null;
let inflight: Promise<GoldPrice> | null = null;
// Os preços só mudam em múltiplos do intervalo do plano (contados a partir de 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}¤cies=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, // alguns segundos de folga
};
}
export function goldPerGram(): Promise<GoldPrice> {
if (cached && Date.now() < cached.expiresAt) return Promise.resolve(cached);
// Requisição única: requisições simultâneas compartilham uma só chamada à API.
inflight ??= fetchGold()
.then((fresh) => (cached = fresh))
.catch((err) => {
// Continua cotando com o último preço válido por até 6 horas; tenta de novo em um minuto.
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% do valor do metal
| { 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),
};
}Algumas linhas merecem um olhar mais atento. nextBoundary() arredonda para cima até o próximo múltiplo do intervalo do plano, então no Standard um preço buscado às 10:03 expira às 10:10:05, alguns segundos depois da próxima atualização, e não às 10:13. Com MIN_TTL_S definido como 3600 no Lite, a próxima renovação espera pelo primeiro limite que esteja a pelo menos uma hora de distância, o que limita o uso a 24 requisições por dia. O ramo .catch é a regra de servir o último preço: ele retorna o último preço válido e agenda a próxima tentativa para um minuto depois, de modo que uma indisponibilidade custa no máximo uma requisição com falha por minuto, e não uma por visitante.
Exponha o módulo por meio de um pequeno endpoint HTTP. Valide a entrada com rigor: aceite apenas os teores que você vende e uma faixa de peso razoável. A mão de obra e a alíquota do IVA são configurações do servidor; nunca as aceite do cliente.
// server.ts — npm install express, depois: METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { quote } from "./gold-quote";
// Fatores de pureza conforme marcados nas suas peças (veja a tabela de quilates acima).
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 {
// Mão de obra e IVA são configurações do servidor, nunca dados enviados pelo cliente.
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"));Uma requisição a /api/jewelry-quote?karat=18&grams=5.2 retorna um detalhamento como este (preço do ouro ilustrativo):
{
"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
}A implementação em Python
A versão em Python segue o mesmo desenho. Ela usa Decimal para valores monetários, de modo que os valores nunca acumulam erros de ponto flutuante binário, e um lock para que requisições simultâneas em um servidor com threads compartilhem uma só chamada à API. Para manter o código curto, ela aceita apenas mão de obra por grama:
# 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 # Plano Standard: os preços são atualizados a cada 10 minutos
MIN_TTL_S = 0 # Plano Lite: use 3600 para ficar abaixo de 1.000 requisições por mês
_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]:
"""Ouro puro (24K) em CURRENCY por grama, e o horário Unix desse preço."""
with _lock: # uma chamada à API por vez; as threads em espera reutilizam o resultado
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 o último preço válido, tenta de novo em um minuto
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),
}Qualquer framework web pode servi-lo. Com o FastAPI, endpoints definidos com def comum rodam em um pool de threads, e é exatamente para isso que serve o lock:
# app.py — pip install fastapi uvicorn requests, depois: METALAPI_KEY=mk_live_... uvicorn app:app
from decimal import Decimal
from fastapi import FastAPI, HTTPException, Query
from gold_quote import quote
# Fatores de pureza conforme marcados nas suas peças.
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.")A versão em Python retorna os valores como strings, e a versão em TypeScript, como números. Se um orçamento for para um sistema de pedidos, armazene os valores como strings decimais ou centavos inteiros, e não como números de ponto flutuante.
Um exemplo prático
Aqui está o anel de 18K precificado passo a passo, com um preço ilustrativo do ouro de € 110,00 por grama, mão de obra de € 12 por grama e IVA de 20%:
| Etapa | Cálculo | Valor |
|---|---|---|
| Ouro 24K por grama | rates.EURXAU | € 110,00 |
| Teor | 18K, marcação 750 | 0,75 |
| Valor do metal | 5,2 g × € 110,00 × 0,75 | € 429,00 |
| Mão de obra | 5,2 g × € 12,00 | € 62,40 |
| Preço líquido | € 429,00 + € 62,40 | € 491,40 |
| IVA | € 491,40 × 20% | € 98,28 |
| Total | € 589,68 |
Agora, uma corrente de 22K com 10 g e mão de obra de 8%, com o mesmo preço do ouro. Com teor 22/24, o valor do metal é € 1.008,33, a mão de obra é 8% desse valor (€ 80,67), o preço líquido é € 1.089,00 e o total com IVA é € 1.306,80. Com o fator da marcação 916, o valor do metal seria € 1.007,60.
Apenas o valor do metal acompanha o preço do ouro, então o total varia menos que o próprio ouro. Se o ouro subir 1%, para € 111,10 por grama, o valor do metal do anel de 18K passa a € 433,29 e o total a € 594,83, um aumento de € 5,15, ou 0,87%. Com mão de obra percentual, o total acompanharia o preço do ouro mais de perto.
Checklist para produção
- Mantenha a chave de API em uma variável de ambiente no servidor. O endpoint da calculadora retorna preços, nunca a chave.
- Exiba
price_timeem cada orçamento, para que clientes e equipe vejam quão atual é o preço do ouro. - Congele o preço quando um pedido for feito: armazene com o pedido o preço do ouro, o timestamp dele, o teor e o valor de cada linha, em vez de recalcular depois.
- Espere preços estáveis no fim de semana, quando o mercado de ouro está fechado. Isso não é um cache desatualizado.
- Acompanhe o uso com o endpoint de uso, que não conta para a sua cota, e crie um aviso para você mesmo antes que o mês acabe.
- Trate o erro
105(cota mensal esgotada) servindo o preço em cache e alertando a sua equipe. A cota é renovada no dia 1º, às 00:00 UTC, e um upgrade vale imediatamente. - Para reagir a grandes movimentos de preço, por exemplo para reprecificar o seu catálogo, use alertas de preço com webhooks em vez de fazer polling com mais frequência.
Compare planos e cotas na página de preços ou crie uma conta grátis para obter uma chave e testar a calculadora com preços reais.
Experimente com seus próprios dados
Crie uma conta grátis e faça sua primeira requisição em menos de dois minutos. 100 requisições por mês, sem cartão de crédito.
Continue lendo
API de preços do ouro: o guia completo para desenvolvedores
Como usar uma API de preços do ouro: obtenha uma chave, chame-a com curl, JavaScript e Python, leia as cotações certo e trate histórico, erros, cotas e cache.
Como adicionar um widget de cotação do ouro ao vivo ao seu site
Widget com a cotação do ouro ao vivo no WordPress, na Shopify ou em HTML puro: crie no painel da MetalAPI, escolha símbolos, layout e tema e cole duas linhas.
Preços do ouro por quilate: 24K, 22K, 18K e como calculá-los
O que significam 24K, 22K e 18K, como se relacionam com teores como 916 e 750, como calcular o preço por grama de cada quilate e como obtê-lo por uma API.