# SibFly
> Satellite-measured ground motion (sinking and uplift) for any US address, in mm/year AND in/year, from NASA OPERA DISP-S1 (Sentinel-1 InSAR).
## Endpoints
- GET /api/v1/motion?address=
(or ?lat=&lon=) -> one report. Costs $0.40 from prepaid credits.
optional: &include=timeseries (embed full history in the same call), &brief=1 (plain-English narration).
- POST /api/v1/motion/batch body {"items":[{"id":"a","address":"..."},{"id":"b","lat":..,"lon":..}]} -> array; up to 1000; ONLY covered rows are billed.
- GET /api/v1/balance -> credits_usd, price_per_report_usd, reports_remaining_est, top_up_url. Free.
- GET /api/v1/timeseries , /api/v1/map , /api/v1/coverage (coverage is free).
- /timeseries series_type: 'measured' = real per-date satellite passes (PREMIUM frames with a 150 m history cube; cell_m included) ; 'modeled' = the fitted trend evaluated on real acquisition dates (standard frames, see series_note; delta uncertainty = velocity sigma x years).
- No key -> free teaser with coverage, confidence, and would_cost_usd (a free 'should I buy?' check).
## Response fields (every value carries its unit in the key name)
- velocity_vertical_mm_yr + velocity_vertical_in_yr, velocity_uncertainty_mm_yr + _in_yr, seasonal_amplitude_mm + _in,
total_motion_mm + _in, display_units, trend, assessment, confidence, frame, n_measurements, span_years,
last_observation, data_age_days, data_coverage, provenance{calibration,...}, pixel_polygon, cell_size_m.
- Billing on every paid response: cost_usd, credits_remaining_usd, request_id (also headers X-Request-Id, X-Credits-Remaining).
## Agent notes
- Auth: header 'Authorization: Bearer '.
- Idempotency: send header 'Idempotency-Key: '; a retry with the same key replays the cached result and is NOT charged again (7 days). Works on motion and batch.
- Misses are FREE: out-of-coverage / no-data / bad address return HTTP 200 with coverage:false and cost_usd:0 (never 404, never billed) on /motion, /timeseries, /map. (The /api/v1/geocode helper alone 404s on address_not_found.)
- An invalid/unknown Bearer key returns HTTP 401 invalid_api_key (never a silent teaser) — re-check your key or register a new one.
- Idempotency also works on GET /api/v1/timeseries (same 7-day replay).
- Out of credits: HTTP 402 with top_up_url + suggested_top_up_usd + a buy_api block so you can self-refill (see 'Top up by API'). Errors are JSON {error,code,message,retryable,doc_url,request_id}.
- Rate limits: un-keyed 30/min/IP; Bearer keys a generous 120/min/key. 429s carry Retry-After; keyed 200s carry RateLimit-Limit/-Remaining/-Policy so you can pace BEFORE the wall. Bulk work -> use batch.
## Get a key (free, no card, no phone, no name, no captcha)
- Agents: POST https://sibfly.com/api/v1/autonomous/register {"email":"you@example.com"} -> {api_key, $1.00 free credits = 2 reports at $0.40, password}. One call, instant. (Anti-abuse: 5 registrations/day/IP; free credits on the first 2.)
- TEST MODE: add {"mode":"test"} -> an sf_test_ key with $100 FAKE credits and SYNTHETIC data; /api/v1/buy auto-credits instantly. Rehearse the full register->check->402->buy->retry loop for $0.
- Humans: POST https://sibfly.com/signup with form fields email, password, agree=1; key shown on /dashboard.
## Top up by API (no browser)
- POST /api/v1/buy {"amount_usd": 10} (Bearer) -> {checkout_url} (Stripe card session). {"method":"crypto"} -> {invoice_url, txn_id} (BTC).
- Open/relay the URL to complete payment; credits land automatically (webhook); poll GET /api/v1/balance to confirm. Every 402 body repeats these calls in its buy_api block.
- x402 (fully in-band, no links, no signup): GET or POST /api/v1/x402/motion?address=... -> HTTP 402 with the x402 v2 challenge (PAYMENT-REQUIRED header + accepts[] USDC on Base, CAIP-2 eip155:8453). Sign the transfer and RETRY with the PAYMENT-SIGNATURE header -> we verify+settle via the public Coinbase facilitator and serve the report in that same response (receipt in PAYMENT-RESPONSE). Discovery: /.well-known/x402. (Bearer-key /api/v1/motion 402s also carry accepts[] for keyed callers.)
- OAuth 2.1 for connector installs: /.well-known/oauth-authorization-server (DCR at /oauth/register, PKCE S256). Headless agents should keep using raw Bearer keys.
## Docs
- Interactive: https://sibfly.com/api/docs OpenAPI: https://sibfly.com/openapi.json Index: https://sibfly.com/api/v1
- US coverage, expanding. Information only, not a survey or guarantee. Terms: https://sibfly.com/terms
## Query params on GET /api/v1/motion
- dry_run=1 -> FREE 'should I buy?' oracle on a covered point: coverage, confidence, data_age_days, would_cost_usd, PLUS answer-quality prediction: expected_uncertainty_mm_yr + likely_significant (false = a paid report would say no_clear_trend). Never the rate, never billed.
- min_significance=1 -> if the rate is within its error bar (would be no_clear_trend), returns status:not_significant at $0 — never pay for statistical noise.
- max_age_days=N -> if the best data is older than N days, returns status:stale_data at $0 (not billed) instead of a stale reading.
- min_confidence=X (0-1) -> if pixel confidence < X, returns status:low_confidence at $0 (not billed). NOTE: confidence measures pixel DATA quality (coherence x record length), NOT trend certainty — use min_significance / likely_significant for that.
- include=timeseries, brief=1 (see below).
## confidence_tier (READ THIS to route trust) — graded signal-to-noise, not a binary
- confidence_tier = one of: 'measured' | 'likely' | 'within_margin', from signal_to_noise = |velocity| / velocity_uncertainty_mm_yr.
- 'measured' (SNR>=2): the direction is confident — trust the sign + magnitude.
- 'likely' (1<=SNR<2): leans a direction but within one margin — suggestive, NOT conclusive. Weight accordingly.
- 'within_margin' (SNR<1): the rate is inside its error bar. Split by within_margin_kind (below).
- within_margin_kind (present only when confidence_tier=='within_margin'): 'stable' = a CONFIDENT no-significant-movement PASS (a real green-flag answer, BILLED $0.40); 'inconclusive' = the radar can't give a trustworthy rate here (low coherence / stale) = a non-answer, returned as status:'inconclusive' with cost_usd:0 (NEVER billed, on motion, batch, and PDF).
- differential_motion (optional block): present when the ground moves unevenly over ~150 m (neighbor_spread>3 mm/yr). The foundation-relevant signal — uneven motion cracks buildings, uniform motion usually doesn't.
- neighborhood (block on every covered hit): block-vs-block differential at ~300 m — median of the ~210 m block around your address vs 8 compass blocks. Fields: max_block_difference_mm_yr, strongest_direction, neighbor_faster_sinking, pattern ('uniform' = whole area moves together, benign | 'mild_gradient' | 'strong_gradient' = edge-of-subsidence-bowl, the stress pattern that merits an elevation survey). Calibration-independent (same-frame differences cancel the offset). CAVEAT: when pattern=='uniform', strongest_direction is the argmax of 8 noise-level differences — a statistically meaningless compass point; ignore it unless pattern is a gradient.
- Honest uncertainty: velocity_uncertainty_mm_yr is floored to each frame's held-out GPS RMSE (~4-6 on stable ground; a FLOOR, typical parcels may be worse), or 4.0 on uncalibrated frames. It is ABSOLUTE accuracy vs GPS, wider than a naive fit error. dry_run predicts the tier + whether a paid report will bill.
## assessment_code (stable enum; thresholds on vertical mm/yr)
- <=-8 rapid_subsidence | (-8,-3] notable_subsidence | (-3,3) stable | [3,8) mild_uplift | >=8 strong_uplift. Route DIRECTION on assessment_code, but gate TRUST on confidence_tier (above).
- no_clear_trend / 'stable' code appears when SNR<1 (within margin); see confidence_tier for whether that's a confident 'stable' PASS or an 'inconclusive' free non-answer.
## More response fields
- pixel_id (stable 30 m cell id), seasonal_dominant (bool: seasonal swing > trend; read with care), velocity_display ('-6 mm/yr (-0.24 in/yr) +/-2'), queried_at (UTC), reports_remaining, query.geocoded_address (resolved canonical address).
- Misses carry no_data_reason / no_coverage_reason. ETag + Cache-Control on /motion: send If-None-Match to revalidate for free (304, no charge).
## brief=1 narration
- Adds ground_brief (plain-English) + engine code. Free accounts get GL52 (GLM), paying accounts get CL46 (Opus). It always restates the number, the +/- uncertainty, the data date, and the screening disclaimer; it does not invent numbers or give buy/sell/safety advice. Off the fast path (opt-in).
## Batch response shape (POST /api/v1/motion/batch)
- {status, summary:{requested,billed,succeeded,no_coverage,invalid,total_cost_usd}, credits_remaining_usd, request_id, results:[ per item ]}.
- Each result: {id, status:'ok'|'no_data'|'no_coverage'|'invalid_address'|..., cost_usd, ...motion fields when ok}. Duplicate cells in one batch are charged once and flagged same_pixel_as. Send header Idempotency-Key to replay the whole batch for free.
## Other endpoints
- GET /api/v1/coverage?address=|lat=&lon= -> per-point {covered, frame, data_age_days, would_cost_usd} (FREE). No args -> live frame list.
- GET /api/v1/timeseries?...&reference_date=YYYY-MM-DD&reference_date2=YYYY-MM-DD -> adds delta{movement_mm,movement_in,uncertainty_mm,days_between}: how much it moved between two dates. Free gates: dry_run=1 (points+cost preview), max_age_days=N. max_points=N decimates the series (token diet, keeps first+last). Returns cost_usd + credits_remaining_usd; honors Idempotency-Key (replays report cost_usd:0 + original_cost_usd).
- POST /api/v1/account/spend_cap {"daily_usd": N} -> per-key DAILY spend cap (402 spend_cap_reached before drain); null removes it.
- POST /api/v1/coverage/batch -> FREE batch coverage pre-check (covered/age/would_cost per row); size a job before spending.
- GET /api/v1/frames and /api/v1/frames/{id}/last_updated -> when frames last got new data (poll to re-pull). FREE.
- POST /api/v1/webhooks {"url":"https://...","events":["frame.updated","balance.low","batch.done"]} -> push instead of polling. Standard-Webhooks HMAC signing (headers webhook-id/-timestamp/-signature; verify HMAC-SHA256(secret, id+'.'+ts+'.'+body)). GET lists, DELETE /api/v1/webhooks/{id} removes. Max 5/account.
- GET /status -> health + per-frame freshness lag. GET /changelog.json -> additive API changelog + versioning policy.
- POST /api/v1/motion/batch with {"async":true} -> 202 {job_id}; poll GET /api/v1/motion/batch/{job_id}. {"since":"YYYY-MM-DD"} -> diff-billing (only newly-updated rows billed).
## MCP server
- POST https://sibfly.com/mcp (Streamable HTTP, JSON-RPC). Auth: Bearer . Tools: check_ground_motion, check_portfolio (max_spend_usd budget cap), get_coverage (accepts addresses[] for free batch sizing), get_motion_history, get_account, get_usage (billing ledger), ask_inspector (Q&A about a bought report), register_agent (get a key, FREE, no auth needed), buy_credits (payment link). The FULL lifecycle — register -> check -> understand -> pay -> top up -> reconcile — works inside MCP. Misses/coverage/account are free; reports cost $0.40.
## Agent onboarding & keys (no human needed)
- POST /api/v1/autonomous/register {email, password?} -> {api_key, free credits}. One call, no captcha; if password omitted it's generated and returned.
- POST /api/v1/keys {daily_cap_usd?, allowed_referer?, ttl_hours?, label?} -> a SCOPED child key (draws from your credits, capped + referer-locked + revocable). Safe to embed in a public browser widget. GET /api/v1/keys lists; DELETE /api/v1/keys/{key} revokes.
## More motion params
- min_geocode_score=0..1 -> free miss if the address only matched a coarse centroid (query.geocode gives match_type/score). compact=1 / fields=a,b,c / units=mm|in -> control payload size.
- Response adds: near_threshold + significant_nonzero (is the label trustworthy / distinguishable from zero), neighbor_consistent (False = lone-pixel artifact risk), provenance.license (caching/re-serve grant).
## Machine-readable schema
- GET /api/v1/schema -> all enums (assessment_code, status, freshness, error_codes, batch/job statuses, mcp_arg_map), thresholds, free gates, and the decision-tree as JSON. Parse this instead of prose.
## Free helper & account endpoints (no charge)
- GET /api/v1/geocode?address= -> {lat, lon, matched_address, match_type, score}. Resolve an address to a point without buying a report. (404 address_not_found if neither Census nor the OSM fallback matches.)
- GET /api/v1/pixel?lat=&lon= -> the exact 30 m cell polygon (GeoJSON) that covers a point.
- GET /api/v1/usage?limit=N -> your line-item ledger; each charge carries ts, endpoint, address, cost_usd AND request_id (matches the report's request_id -> full reconciliation).
- GET /api/v1/me -> your account {email, credits_usd, plan, joined, total_reports, last30, spent30_usd}. GET /api/v1/my-reports -> reports you've bought. GET /api/v1/my-chats -> your inspector chats.
- POST /api/v1/chat {messages:[{role,content},...], context: , convo?} -> {reply}. AI inspector Q&A about a report (no new report charge; per-account allowance -> 429 chat_quota_exhausted when spent). Agent shortcut: pass {address:"..."} instead of context and the server loads your bought report for that address.
- POST /api/v1/support {message, subject?, priority?} / GET /api/v1/support -> open + read support tickets (subject derived from message if omitted).
## Paid artifacts
- GET /report.pdf?address= (&brief=1 for narration, &label= for a display name) -> a rendered PDF report. Same $0.40 as a motion report (idempotent per point/day on the web tier).
- GET /api/v1/map?address=&half_km= -> a velocity heatmap PNG around the point ($0.40; un-keyed = a free low-res teaser).
## MCP tool arg names differ from REST params (see /api/v1/schema mcp_arg_map)
- check_ground_motion: use 'explain' (not brief). get_motion_history: use 'from_date'/'to_date' (map to reference_date/reference_date2). get_account -> GET /api/v1/balance. check_portfolio takes addresses[] only (no per-item gates). Over MCP, units/fields/include/mock are not exposed — use the REST API for those.
## Official SDKs (zero-dependency, retries + auto Idempotency-Key + typed InsufficientCredits/AuthError)
- Python: pip install sibfly -> from sibfly import SibFly; SibFly.register('you@example.com').motion(address='...')
- Node (>=18): npm install sibfly -> const {SibFly} = require('sibfly')
## Framework integrations (same API, drop-in tools)
- LangChain: pip install langchain-sibfly (SibflyGroundMotion tool)
- CrewAI: pip install crewai-sibfly (SibflyGroundMotionTool)
- LlamaIndex: pip install llama-index-tools-sibfly (SibflyToolSpec)
- n8n: n8n-nodes-sibfly (community node)
- MCP (any client): https://sibfly.com/mcp
## Human/SEO pages (measured data, good for citations)
- https://sibfly.com/how-fast-is-america-sinking -> the fastest-measured US subsidence, ranked, with a Dataset.
- https://sibfly.com/sinking -> ground motion for every covered US metro; /sinking/ and /sinking/ for detail.