# 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.
## Machine schema (enums, thresholds, statuses)
{
"schema_version": "1.1.0",
"sign_convention": "negative mm/yr = subsidence (sinking); positive = uplift",
"units": {
"primary": "mm_yr",
"also": "in_yr",
"factor_mm_to_in": 0.0393701,
"selector_param": "units=mm|in"
},
"assessment_code": {
"enum": [
"rapid_subsidence",
"notable_subsidence",
"stable",
"mild_uplift",
"strong_uplift",
"no_clear_trend"
],
"thresholds_mm_yr": {
"rapid_subsidence": "v <= -8",
"notable_subsidence": "-8 < v <= -3",
"stable": "-3 < v < 3",
"mild_uplift": "3 <= v < 8",
"strong_uplift": "v >= 8",
"no_clear_trend": "abs(v) <= velocity_uncertainty_mm_yr (margin >= signal; OVERRIDES the sign-based codes above)"
}
},
"confidence_tier": {
"enum": [
"measured",
"likely",
"within_margin"
],
"from": "signal_to_noise = abs(velocity_vertical_mm_yr) / velocity_uncertainty_mm_yr",
"thresholds": {
"measured": "SNR >= 2 \u2014 direction is confident (trust the sign + magnitude)",
"likely": "1 <= SNR < 2 \u2014 leans a direction but within one margin (suggestive, NOT conclusive)",
"within_margin": "SNR < 1 \u2014 the rate is inside its error bar (split by within_margin_kind)"
},
"note": "route DIRECTION on assessment_code, but gate TRUST on confidence_tier"
},
"within_margin_kind": {
"enum": [
"stable",
"inconclusive",
null
],
"note": "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 \u2014 on motion, batch, and PDF)"
},
"signal_to_noise": {
"type": "number",
"note": "abs(velocity_vertical_mm_yr) / velocity_uncertainty_mm_yr \u2014 the raw ratio behind confidence_tier"
},
"differential_motion": {
"note": "optional block; present when the ground moves unevenly over ~150 m (neighbor_spread_mm_yr > 3). The foundation-relevant signal \u2014 uneven motion cracks buildings, uniform motion usually doesn't.",
"fields": [
"neighbor_spread_mm_yr",
"note"
]
},
"trend": {
"enum": [
"subsiding",
"stable",
"uplifting",
"no_clear_trend"
]
},
"data_freshness": {
"enum": [
"current",
"recent",
"stale",
"very_stale",
"unknown"
],
"days": {
"current": "<=120",
"recent": "<=365",
"stale": "<=730",
"very_stale": ">730"
}
},
"calibration": {
"enum": [
"gps_tied",
"relative"
]
},
"geocode_match_type": {
"enum": [
"coordinates",
"rooftop_interpolated",
"street_interpolated",
"area_centroid"
],
"scores": {
"coordinates": 1.0,
"rooftop_interpolated": 0.9,
"street_interpolated": 0.7,
"area_centroid": 0.4
}
},
"motion_status": {
"discriminator": "status",
"enum": [
"ok",
"no_data",
"no_coverage",
"stale_data",
"low_confidence",
"low_geocode_precision",
"no_new_data",
"inconclusive"
],
"billed": [
"ok"
],
"note": "every non-ok status is FREE (cost_usd:0). Discriminate on 'status'. 'inconclusive' = the honest free non-answer (see confidence_tier / within_margin_kind)."
},
"batch_item_status": {
"enum": [
"ok",
"no_data",
"no_coverage",
"no_new_data",
"skipped_for_budget",
"insufficient_credits",
"invalid_item",
"invalid_address",
"unprocessed",
"inconclusive"
]
},
"async_job_status": {
"enum": [
"queued",
"processing",
"done",
"error"
]
},
"error_codes": {
"note": "JSON errors are {error(code), message, retryable, doc_url, request_id}",
"enum": [
"unauthorized",
"login_required",
"invalid_api_key",
"insufficient_credits",
"spend_cap_reached",
"forbidden_referer",
"account_suspended",
"rate_limited",
"address_not_found",
"need_address_or_coords",
"invalid_coordinates",
"bad_request",
"too_many_items",
"job_not_found",
"key_not_found",
"frame_not_found",
"invalid_email",
"disposable_email",
"email_exists",
"bad_mock",
"bad_amount",
"bad_method",
"message_required",
"card_unavailable",
"crypto_unavailable",
"stripe_error",
"crypto_error",
"invalid_arguments"
]
},
"mcp_arg_map": {
"note": "MCP tool args differ from REST param names",
"check_ground_motion": "explain->brief; supports dry_run/max_age_days/min_confidence/since; NOT units/fields/include/mock",
"get_motion_history": "from_date->reference_date, to_date->reference_date2 (both required for a delta); supports dry_run/max_age_days",
"get_account": "-> GET /api/v1/balance",
"check_portfolio": "addresses[] only (no per-item gates)",
"register_agent": "-> POST /api/v1/autonomous/register (no auth needed)",
"buy_credits": "-> POST /api/v1/buy (Bearer)"
},
"buy_api": {
"endpoint": "POST /api/v1/buy",
"body": {
"amount_usd": "1..10000",
"method": "stripe|crypto"
},
"returns": "checkout_url (stripe) or invoice_url+txn_id (crypto); credits land automatically after payment \u2014 poll /api/v1/balance"
},
"free_gates": [
"dry_run",
"max_age_days",
"min_confidence",
"since",
"min_geocode_score",
"min_significance"
],
"confidence_semantics": {
"confidence": "pixel DATA quality (coherence x record length) \u2014 NOT trend certainty",
"trend_certainty": "use significant_nonzero / dry_run's likely_significant / the min_significance gate",
"why": "a 0.99-confidence pixel can still yield no_clear_trend when |velocity| <= uncertainty"
},
"test_mode": {
"how": "POST /api/v1/autonomous/register {email, mode:'test'} -> sf_test_ key, $100 fake credits",
"behavior": "synthetic data, /api/v1/buy auto-credits, full billing lifecycle rehearsable at $0"
},
"webhooks": {
"endpoint": "POST /api/v1/webhooks",
"events": [
"frame.updated",
"balance.low",
"batch.done"
],
"signing": "Standard-Webhooks style: HMAC-SHA256(secret, webhook-id + '.' + webhook-timestamp + '.' + body)"
},
"payload_control": [
"compact",
"fields",
"units"
],
"decision_tree": "get_coverage (free) or motion?dry_run=1 (free) to confirm coverage+cost -> motion to buy ($0.40) -> route on assessment_code. Misses & gated results are free.",
"index": "https://sibfly.com/api/v1",
"docs": "https://sibfly.com/llms.txt"
}