{"_id":"@agaudeals/spot-price","name":"@agaudeals/spot-price","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@agaudeals/spot-price","version":"0.1.0","description":"Fetch USD spot prices for gold, silver, platinum, and palladium with built-in caching, ±20% sanity bounds, and stale-while-revalidate. Stooq (no key) and MetalPriceAPI adapters.","keywords":["gold","silver","platinum","palladium","spot-price","xau","xag","xpt","xpd","stooq","metalpriceapi","agaudeals"],"license":"MIT","author":{"name":"AgAu Deals","email":"hello@agaudeals.com"},"homepage":"https://agaudeals.com","repository":{"type":"git","url":"git+https://github.com/agaudeals/agaudeals-js.git","directory":"packages/spot-price"},"bugs":{"url":"https://github.com/agaudeals/agaudeals-js/issues"},"type":"module","main":"./dist/cjs/index.cjs","module":"./dist/esm/index.js","types":"./dist/types/index.d.ts","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.cjs"}},"engines":{"node":">=20.0.0"},"scripts":{"clean":"rm -rf dist","build:esm":"tsc -p tsconfig.esm.json","build:cjs":"tsc -p tsconfig.cjs.json && node ../../scripts/rename-cjs.mjs dist/cjs","build:types":"tsc -p tsconfig.types.json","build":"npm run clean && npm run build:esm && npm run build:cjs && npm run build:types","typecheck":"tsc -p tsconfig.json --noEmit","test":"node --test --import tsx test/*.test.ts"},"dependencies":{"undici":"^6.19.0"},"devDependencies":{"tsx":"^4.7.0"},"publishConfig":{"access":"public"},"_id":"@agaudeals/spot-price@0.1.0","gitHead":"2441ce387f6f30ae131f20876cc2c9133d501ab9","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-IrIDcJmPnebmq4dinl19FCFEiqZ4Lo25r+3Ry6MASH06EmDLQPZc+7Wo/BcZceWQL57OK0uxWHrhDGcatWdCpA==","shasum":"cee496e7049900cf3cf2ef9f710c7052dd61191a","tarball":"https://registry.npmjs.org/@agaudeals/spot-price/-/spot-price-0.1.0.tgz","fileCount":39,"unpackedSize":53086,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@agaudeals%2fspot-price@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGV+E+OFnqrxSAIhlpyg1jkIHNtKFKlfXKYOen0Lc66HAiAW/XRLs0N+JPdpRrXYuxOFAQQ9z49vrOce6Oe17xs3bg=="}]},"_npmUser":{"name":"agaudeals-dev","email":"agaudeals@gmail.com"},"directories":{},"maintainers":[{"name":"agaudeals-dev","email":"agaudeals@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/spot-price_0.1.0_1777827063002_0.5512847414143025"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-03T16:51:02.783Z","0.1.0":"2026-05-03T16:51:03.162Z","modified":"2026-05-03T16:51:03.650Z"},"maintainers":[{"name":"agaudeals-dev","email":"agaudeals@gmail.com"}],"description":"Fetch USD spot prices for gold, silver, platinum, and palladium with built-in caching, ±20% sanity bounds, and stale-while-revalidate. Stooq (no key) and MetalPriceAPI adapters.","homepage":"https://agaudeals.com","keywords":["gold","silver","platinum","palladium","spot-price","xau","xag","xpt","xpd","stooq","metalpriceapi","agaudeals"],"repository":{"type":"git","url":"git+https://github.com/agaudeals/agaudeals-js.git","directory":"packages/spot-price"},"author":{"name":"AgAu Deals","email":"hello@agaudeals.com"},"bugs":{"url":"https://github.com/agaudeals/agaudeals-js/issues"},"license":"MIT","readme":"# @agaudeals/spot-price\n\n[![npm version](https://img.shields.io/npm/v/@agaudeals/spot-price.svg)](https://www.npmjs.com/package/@agaudeals/spot-price)\n[![license](https://img.shields.io/npm/l/@agaudeals/spot-price.svg)](./LICENSE)\n[![CI](https://github.com/agaudeals/agaudeals-js/actions/workflows/ci.yml/badge.svg)](https://github.com/agaudeals/agaudeals-js/actions/workflows/ci.yml)\n\nFetch USD spot prices for gold (XAU), silver (XAG), platinum (XPT), and palladium (XPD) with built-in 60-second caching, ±20% sanity bounds against the last cached value, and stale-while-revalidate on provider failure. Ships **Stooq** (no API key, default) and **MetalPriceAPI** (user-provided key, opt-in real-time) adapters. Part of [agaudeals.com](https://agaudeals.com).\n\n## Install\n\n```sh\nnpm install @agaudeals/spot-price\n```\n\nRequires Node 20+. ESM and CJS entries; full TypeScript types.\n\n## Quick start\n\n```ts\nimport { SpotPrice } from '@agaudeals/spot-price';\n\nconst spot = new SpotPrice(); // defaults to Stooq, 60s TTL\nconst gold = await spot.getSpot('XAU');\n\nconsole.log(gold);\n// {\n//   metal: 'XAU',\n//   priceUsdPerOz: 2350.5,\n//   asOf: 2026-04-30T17:00:00.000Z,\n//   source: 'stooq',\n//   stale: false,\n//   staleAgeMs: 0,\n// }\n```\n\n### MetalPriceAPI (real-time, requires key)\n\n```ts\nconst spot = new SpotPrice({\n  provider: 'metalpriceapi',\n  apiKey: process.env.METALPRICEAPI_KEY!,\n  cacheTtlMs: 30_000,\n});\nconst silver = await spot.getSpot('XAG');\n```\n\n## API\n\n### `new SpotPrice(options)`\n\n| option        | type                                | default   | description                                  |\n| ------------- | ----------------------------------- | --------- | -------------------------------------------- |\n| `provider`    | `'stooq' \\| 'metalpriceapi'`        | `'stooq'` | Adapter to use.                              |\n| `apiKey`      | `string`                            | —         | Required for `metalpriceapi`.                |\n| `cacheTtlMs`  | `number`                            | `60_000`  | TTL for in-memory cache.                     |\n| `sanityBand`  | `number` (fraction, e.g. `0.2`)     | `0.2`     | Reject provider prices outside ±band.        |\n| `fetch`       | `FetchLike`                         | global    | Inject custom fetch (testing, retries, etc). |\n| `adapter`     | `ProviderAdapter`                   | —         | Inject a custom adapter; bypasses provider.  |\n\n### `spot.getSpot(metal, opts?) → Promise<SpotResult>`\n\n`metal` is `'XAU' | 'XAG' | 'XPT' | 'XPD'`. `opts.signal` is an optional `AbortSignal`.\n\n```ts\ninterface SpotResult {\n  metal: 'XAU' | 'XAG' | 'XPT' | 'XPD';\n  priceUsdPerOz: number;\n  asOf: Date;            // provider-reported observation time\n  source: 'stooq' | 'metalpriceapi';\n  stale: boolean;        // true if served from cache after a failed refresh\n  staleAgeMs: number;    // age of the cached value when stale; 0 when fresh\n}\n```\n\n## Freshness contract\n\nThe library is built around predictable, observable freshness — not the lowest-possible latency. If you need millisecond-fresh prices for execution, use a dedicated market-data feed.\n\n- **Typical latency.** Stooq publishes end-of-period quotes with a delay measured in minutes, not seconds. MetalPriceAPI updates approximately every 60 seconds on the standard tier. The library does not paper over this; `asOf` reflects the provider's reported timestamp.\n- **Cache TTL.** A successful fetch is held in-memory for `cacheTtlMs` (default **60 seconds**). Subsequent calls within the TTL return the cached value with `stale: false`, `staleAgeMs: 0` and **do not** hit the network.\n- **Stale-while-revalidate.** If a refresh fails (network error, HTTP error, parse error, or sanity-band rejection) and a previously cached value exists, the call returns that cached value with `stale: true` and `staleAgeMs` set to the actual age. The error is swallowed — the contract is *prefer slightly-old data over no data*. If there is no cached value, the error is rethrown as `SpotPriceError`.\n- **Sanity bounds.** A new fetch is rejected (and treated as a parse error) if its price is more than ±20% away from the last cached value. This protects callers from upstream data corruption (decimal-point bugs, currency-base errors, \"0\" sentinel values) without inventing prices.\n- **In-flight dedup.** Concurrent `getSpot` calls for the same metal coalesce into one upstream request.\n- **Daily reconciliation.** A GitHub Actions cron (`reconcile-lbma.yml`) compares Stooq's gold close against the LBMA London PM fix daily. Drift > 5% fails the run and surfaces in CI.\n\n`stale: true` means: *the upstream is currently unavailable or distrusted; this number is `staleAgeMs` ms old.* It does **not** mean \"expired\" — it means \"we tried to refresh and could not.\"\n\n## Providers\n\n| provider       | key needed | notes                                                                 |\n| -------------- | ---------- | --------------------------------------------------------------------- |\n| `stooq`        | no         | Default. CSV endpoint, USD/oz for XAU/XAG/XPT/XPD. Minute-grade fresh. |\n| `metalpriceapi`| yes        | Real-time tier. Returns rates `USD → metal`; library inverts to USD/oz. |\n\nExplicitly **not** supported (and won't be added): Yahoo Finance, Kitco scraping, Investing.com.\n\n## Errors\n\nAll thrown errors are instances of `SpotPriceError`. They are only thrown on a *cold* failure (no cached value to fall back to) or invalid construction. Refresh failures with cache present resolve to a `stale: true` result.\n\n## License\n\nMIT — see [LICENSE](./LICENSE). Maintained by [AgAu Deals](https://agaudeals.com).\n","readmeFilename":"README.md","_rev":"1-d94c467be0188e4aa59982465d1dc55a"}