sibfly
v0.2.0
Published
Zero-dependency client for the SibFly ground-motion API — satellite-measured land subsidence/uplift (mm/yr) for any US address
Maintainers
Readme
sibfly
Zero-dependency Node client for SibFly — satellite-measured ground motion (sinking/uplift, mm/yr and in/yr) for any US address, from NASA OPERA Sentinel-1 InSAR. Flat $0.40 per covered report; misses are free.
Node 18+ (global fetch), CommonJS, TypeScript types included, no dependencies.
npm install sibflyQuickstart
const { SibFly, InsufficientCredits } = require("sibfly");
const client = await SibFly.register("[email protected]"); // self-onboard: key + free credits
try {
const r = await client.motion({ address: "425 Fremont St, Las Vegas, NV" });
console.log(r.velocity_vertical_mm_yr, r.assessment);
} catch (e) {
if (e instanceof InsufficientCredits) console.log("top up at:", e.topUpUrl);
}Already have a key? new SibFly("sf_...") or set SIBFLY_API_KEY in the env.
Out of credits? The full recovery loop
When a billed call throws InsufficientCredits, you can self-refill entirely
by API — no browser, no human:
const { SibFly, InsufficientCredits, SpendCapReached } = require("sibfly");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const client = new SibFly(); // SIBFLY_API_KEY in env
async function motionWithRefill(q) {
try {
return await client.motion(q);
} catch (e) {
if (e instanceof SpendCapReached) throw e; // your own daily cap — topping up won't help
if (!(e instanceof InsufficientCredits)) throw e;
const order = await client.buy(10); // POST /api/v1/buy -> Stripe session
console.log("pay here:", order.checkout_url); // open/relay this URL
while ((await client.balance()).credits_usd < 0.4) {
await sleep(15000); // credits land via webhook
}
return client.motion(q); // now it goes through
}
}
const report = await motionWithRefill({ address: "425 Fremont St, Las Vegas, NV" });e.buyApi on the exception carries the machine-readable refill recipe from the
402 body (suggested amount, endpoints). client.buy(amount, "crypto") returns
{invoice_url, txn_id} for BTC instead of a Stripe checkout_url.
Spend caps
await client.spendCap(5) sets a per-key daily cap of $5; once tripped, billed
calls throw SpendCapReached (a subclass of InsufficientCredits, so old
handlers still catch it — but don't top up: you still have credits, the cap is
yours). Clear it with client.spendCap(null).
Billed retries and Idempotency-Key — read this
motion(), batch(), and timeseries() are billed. Retrying a billed call
without an Idempotency-Key charges you again. The SDK protects you: it
auto-sets a fresh UUID Idempotency-Key header on every billed call, so its own
internal retries (network blips, 5xx) can never double-charge.
But the auto key is per call — if your code re-invokes motion(...) after
a crash, that is a new key and a new charge. To make your own retries free, pass
an explicit key and reuse it:
const r = await client.motion({ address: "...", idempotencyKey: "job-1234-row-7" });
// same call again with the same key -> served from cache, cost_usd === 0Replays are cached for 7 days.
Surface
| Method | Endpoint | Billed |
|---|---|---|
| SibFly.register(email) | POST /api/v1/autonomous/register — returns a ready client | free |
| client.motion({address} or {lat, lon}, ...gates) | GET /api/v1/motion | $0.40 (misses free) |
| client.batch(items, {async: true}) | POST /api/v1/motion/batch (max 1000; only covered rows billed) | per covered row |
| client.batchJob(jobId) / client.waitBatch(jobId) | GET /api/v1/motion/batch/{job_id} | free to poll |
| client.timeseries({...}) | GET /api/v1/timeseries | yes |
| client.buy(amountUsd, "stripe"\|"crypto") | POST /api/v1/buy -> payment URL | free (payment link) |
| client.spendCap(dailyUsd) | POST /api/v1/account/spend_cap (null clears) | free |
| client.coverage({...}) / client.coverageBatch(items) | GET /api/v1/coverage / POST /api/v1/coverage/batch | free |
| client.frames() / client.frameLastUpdated(id) | GET /api/v1/frames / /frames/{id}/last_updated | free |
| client.geocode(address) | GET /api/v1/geocode | free |
| client.balance() / client.me() / client.usage() | GET /api/v1/balance / /me / /usage | free |
| client.createKey({dailyCapUsd}) / listKeys() / revokeKey(k) | POST/GET /api/v1/keys, DELETE /api/v1/keys/{key} | free |
Gates (free-miss guards) pass through, camelCase or snake_case:
motion({ address, dryRun: 1 }), maxAgeDays: 90, minConfidence: 0.8,
include: "timeseries", brief: 1, ...
Built in
- Auto-retry — configurable via
new SibFly(key, { timeout: 30000, maxRetries: 3, maxBackoff: 30000 }): exponential backoff with jitter on 429/5xx/network errors, honorsRetry-After. - Idempotency — a UUID
Idempotency-Keyheader is auto-attached to billed calls (motion,batch,timeseries), so a retried request is never double-charged. PassidempotencyKeyto control it. - Typed errors —
AuthError(401),InsufficientCredits(402, carries.topUpUrl/.buyApi/.suggestedTopUpUsd),SpendCapReached(402spend_cap_reached, subclass ofInsufficientCredits),RateLimitError(429 after retries, carries.retryAfter),NetworkError(timeouts/connection failures,retryable: true),SibFlyErrorbase (.status,.code,.requestId,.body).
Full API contract: https://sibfly.com/llms.txt · MIT license.
