MCP server: One MCP server.

Rafid Intelligence Network

@iabdullahmMCP server · External

Paid intelligence tools for AI agents across risk, finance, property, logistics and more.

Company lookupv0.1.4TypeScript
Freeno charge per call

One address for any MCP client. No sign-in needed on this server.

What it does

  1. Reads
  2. Writes
  3. High impact

At most: moves money, runs code or commands, deletes data, or sends messages for you.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Tools

None listed · checked 9 Oct 2026

Odel could not list its tools without signing in.

Try it in the Inspector
README44 min read

Rafid Intelligence Network — structured intelligence and paid tools for AI agents

Rafid Intelligence Network provides structured intelligence and paid APIs for AI agents across property, companies, suppliers, documents, risk, vehicles and logistics. AI agents are the primary consumer of this platform. MCP, OpenAPI, REST, x402 and API Credits are supported access channels.

Platform architecture

text
Rafid Intelligence Network
├── Property Intelligence
├── Company Intelligence
├── Supplier Intelligence
├── Document Intelligence
├── Risk Intelligence
├── Vehicle Intelligence
└── Logistics Intelligence

Existing property-specific capability contracts, routes and MCP tool names remain unchanged.

The intended flow for an agent is: discover a capability → select the right tool → pay per call over x402 (or authenticate with an API key) → execute → get a structured, machine-readable result. No account, dashboard or subscription is required for either access model, and none is planned — see "Design constraints" below.

Agent discovery

Website planning and inspection are available as the paid website_project_estimate ($0.25) and website_audit ($0.75) capabilities. See website capabilities for contracts, limitations, preview calls and an audit-to-estimate chaining example.

An agent (or an agent marketplace/directory crawler) can start from any of the following; all are public, unauthenticated, always present, and contain no secrets (no wallet private keys, no API keys, no usage data for other customers):

EndpointPurpose
GET /agent.jsonThe full agent manifest: product identity, every supported protocol (MCP, x402, REST) and its role, x402 terms, and the complete tool catalog with full input/output JSON Schemas. Start here.
GET /.well-known/ai-plugin.jsonManifest in the legacy OpenAI ChatGPT-plugin convention, for tooling that still discovers services this way.
GET /.well-known/agent.jsonAn Agent Card in the Agent2Agent (A2A) protocol's convention, listing each tool as a skill.
GET /llms.txtA plain-text briefing for an LLM-based agent: what Rafid does, every tool and how to call it, pricing, the x402 model, and known limitations — no JSON parsing required.
GET /api/v1/capabilitiesThe machine-first capability registry: name, description, whenToUse, price, schemas and examples for every tool — optimized for a model to decide what to call, not for a human to read.
GET /api/v1/mcp/statusFactual MCP status: which transports are live (stdio always, http only when MCP_REMOTE_ENABLED=true), the tool count, and the remote endpoint path if any.
GET /api/v1/agentLegacy service-metadata endpoint (kept for backward compatibility); superseded by /agent.json above.
GET /api/v1/pricingThe full price list (USD, pay-per-call), sourced from one catalog shared by every code path — never duplicated or out of sync.
GET /api/v1/toolsLegacy tool catalog (kept for backward compatibility); superseded by /api/v1/capabilities above.

Every one of these is generated from the single capability registry in src/domain/capabilities.ts — see "Capability registry" below — so they can never disagree with each other or with what a call actually does.

Once a tool is chosen, call it with an X-API-Key header, or — when X402_ENABLED=true — call its /api/v1/x402/... twin with no key and pay per call on-chain instead (see "Pay-per-call via x402" below).

A human visiting / in a browser instead gets a short landing page (agent integration examples, tool list, pricing, links to docs); agents and scripts that send Accept: application/json keep getting the original JSON discovery payload — see "Landing page" below.

Design constraints

This product is deliberately agent-native, not a human SaaS dashboard. It does not have, and is not planned to have, user accounts, a dashboard, subscriptions, Stripe billing, or a billing portal. The X-API-Key route family is a compatibility transport for callers that authenticate that way, not an invitation to build account management around it.

Customer storage phase

PostgreSQL-backed customers, hashed keys, usage tracking and per-customer limits are now available. See activation and administration. Existing env-key development mode remains unchanged; production requires AUTH_MODE=postgres. No payment collection is implemented outside x402 (see below).

Deploy on Vercel

The Express entry point exports the app when VERCEL=1 and keeps the normal port listener for local runs. vercel.json pins the function to iad1 near the current US East Neon database and limits requests to 15 seconds. .vercelignore prevents the local .env, caches and generated files from being uploaded.

Production API: https://api.rafidsystem.com
Landing page: https://api.rafidsystem.com/ (browsers) / same URL with Accept: application/json (agents)
Health: https://api.rafidsystem.com/api/v1/health
OpenAPI: https://api.rafidsystem.com/openapi.json
Agent metadata: https://api.rafidsystem.com/api/v1/agent

Connect the repository directory to a Vercel project, attach the Neon Marketplace database, and configure these Production environment variables:

dotenv
AUTH_MODE=postgres
NODE_ENV=production
LOG_LEVEL=info
X402_ENABLED=false
DATABASE_URL=<sensitive pooled Neon URL with sslmode=verify-full>

The Neon integration may inject DATABASE_URL; verify that it points to the rotated credential and pooled endpoint. Mark manually added database values as sensitive. Do not upload .env or use the previously exposed password. Deploy with Vercel CLI or a connected Git repository, then verify /api/v1/health, /openapi.json, /api/v1/agent, an authenticated analysis request, and the PostgreSQL usage record. The local stdio MCP process is not deployed by this Express function.

To also accept pay-per-call crypto payments, set X402_ENABLED=true and X402_WALLET_ADDRESS=<a 0x-prefixed EVM address you control> in Production. Leave X402_ENABLED=false (the default) until you are ready to receive real payments; see Configuration and the "Pay-per-call via x402" section below. On Base mainnet (eip155:8453) also set CDP_API_KEY_ID/CDP_API_KEY_SECRET (see Configuration).

Prerequisites and quick start

Use Node.js 24+ and npm. From the repository root:

powershell
Set-Location 'C:\Projects\rafid-agent-api'
npm.cmd ci --cache .npm-cache
if (-not (Test-Path .env)) { Copy-Item .env.example .env }
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"

Copy the generated key into RAFID_API_KEYS in .env. Keep it private. A blank or short key prevents HTTP startup. Multiple keys are comma-separated. On macOS/Linux use npm and cp .env.example .env.

powershell
npm.cmd run dev

Development loads .env and watches TypeScript sources. For a compiled run:

powershell
npm.cmd run typecheck
npm.cmd run build
npm.cmd test
npm.cmd start

npm test first builds and then runs service, live HTTP, agent-marketplace, OpenAPI contract, and compiled MCP stdio tests. npm start loads .env and runs dist/server.js; dependencies are pinned in package-lock.json.

Configuration

VariableDefault / behavior
PORT8787
NODE_ENVdevelopment; accepts development, test, production
RAFID_API_KEYSRequired for REST; random keys of at least 24 characters
API_KEYLegacy fallback when RAFID_API_KEYS is empty
LOG_LEVELinfo; error logs only HTTP/tool 5xx completions; silent disables request logs
X402_ENABLEDfalse; true enables pay-per-call crypto payments at /api/v1/x402/... (no API key needed there) and requires X402_WALLET_ADDRESS. Disabled (the default) never affects normal API-key calls — the x402 route family simply doesn't exist and is omitted from discovery//openapi.json
X402_NETWORKeip155:84532 (Base Sepolia testnet); the public facilitator only settles this network for EVM. Any other network, e.g. eip155:8453 (Base mainnet), requires CDP_API_KEY_ID/CDP_API_KEY_SECRET below
X402_WALLET_ADDRESSEmpty; required 0x-prefixed EVM address that receives payments when X402_ENABLED=true
X402_FACILITATOR_URLhttps://x402.org/facilitator; must be an https URL
CDP_API_KEY_IDEmpty; Coinbase Developer Platform API key ID (Ed25519 Secret API Key). Required together with CDP_API_KEY_SECRET for any network besides Base Sepolia
CDP_API_KEY_SECRETEmpty; Coinbase Developer Platform API key secret. When both CDP vars are set, they take over as the facilitator (X402_FACILITATOR_URL is then ignored)
MCP_REMOTE_ENABLEDtrue; mounts the Streamable HTTP MCP transport at /mcp. false unmounts it entirely (404, and every manifest/llms.txt advertises stdio only) — a rollback switch with no code change
RAFID_LOGO_URLEmpty; logo_url in /.well-known/ai-plugin.json. Left blank rather than fabricated
RAFID_CONTACT_EMAILEmpty; contact_email in /.well-known/ai-plugin.json
RAFID_LEGAL_INFO_URLEmpty; legal_info_url in /.well-known/ai-plugin.json
USAGE_REPOSITORYconsole (default: one JSON line to stderr per call); memory (local inspection only, lost on restart); postgres (durable rafid_agent_usage table — requires DATABASE_URL, independent of AUTH_MODE)
RATE_LIMIT_ENABLEDtrue; per-process, IP-keyed rate limiting on discovery, x402 and remote MCP routes (three independent budgets). false disables it (development/tests)
RATE_LIMIT_WINDOW_MS60000 (one minute)
RATE_LIMIT_MAX60 requests per window, per IP, per route group
OMAN_PROPERTY_DATA_MODEmanual (default); database; composite. Which Oman property data source(s) analyze_oman_property actually queries — see "Production Oman market data" below
OMAN_MARKET_DATABASE_URLEmpty; falls back to DATABASE_URL when unset. Lets the market-data database be separate from the customer/billing database if desired
OMAN_MARKET_STALE_DAYS365; a comparable sample whose median age exceeds this is flagged staleMarketData: true and its confidence score is deterministically reduced. Independent of and shorter than comparables.ts's 540-day hard recency cutoff (which excludes a record from the pool entirely)
OMAN_MARKET_CACHE_TTL_MSEmpty/0 (disabled); when set, DatabaseOmanPropertyDataProvider caches a comparable-pool query in memory for this many milliseconds — see "Production Oman market data" below
OMAN_RECENT_SALES_DAYS730 (~2 years); the window analyze_oman_property's historicalSalesContext.recentComparableSales/recentMedianPricePerSqmOMR treat as "recent" — separate from, and never affecting, comparables.ts's fixed 540-day cutoff used for the current market estimate. See "Historical sales context" below
NCSI_API_BASE_URLhttps://map.ncsi.gov.om/ODPAPI (verified live base — see "Official market context (NCSI)" below)
NCSI_REQUEST_TIMEOUT_MS8000
NCSI_CACHE_TTL_MS21600000 (6 hours); 0 disables caching official context lookups
NCSI_REAL_ESTATE_DATASET_IDEmpty; no default — see "Official market context (NCSI)" below for why this must be operator-verified, never guessed
NCSI_FIELD_MAP_JSONEmpty; a JSON object mapping this capability's field names to the configured dataset's actual field names — see "Official market context (NCSI)" below
MARKET_DATA_INTERNAL_API_KEYEmpty; no default. Shared secret gating GET /api/v1/internal/market-data/status and .../partners — see "Partner Data Feed" below. Unset means those two routes always return 503, never a silently-open endpoint
PARTNER_FEED_STALE_DAYS7; a partner whose latest accepted record is older than this is reported stale: true by GET /api/v1/internal/market-data/partners — a per-partner monitoring signal only, never affecting ingestion, analyze_oman_property, or any other partner's staleness
VEHICLE_MARKET_DATA_MODEnone (default: no vehicle market-data provider — vehicle_value_estimate returns insufficient_market_data); database enables the vehicle_market_records provider
VEHICLE_MARKET_DATABASE_URLEmpty; falls back to DATABASE_URL
VEHICLE_MARKET_COUNTRIESEmpty (the database provider claims no market); comma-separated ISO alpha-2 codes the imported evidence covers, e.g. OM,AE, or *
VEHICLE_MARKET_PROVIDER_TIMEOUT_MS4000; per-provider timeout — a slow provider is reported as timeout, never blocks the request
VEHICLE_MARKET_CACHE_TTL_MS21600000 (6 hours); provider search-result cache (valuations themselves are never cached)
VEHICLE_MARKET_MAX_LISTING_AGE_DAYS365; older evidence is never used
MARKETCHECK_API_KEYEmpty; setting it enables the MarketCheck provider (US/Canada used-car dealer listings, licensed/paid). Never logged or returned
MARKETCHECK_PAGES2; result pages of 50 listings per market per uncached search (1–10)
VEHICLE_MARKET_FEEDS_JSONEmpty; JSON array of partner HTTPS feeds [{"id","url","countries","format","authHeaderEnv"}] — see "vehicle_value_estimate → Market-data providers"
VEHICLE_FX_SOURCESEmpty (no currency conversion); comma list tried in order per pair: ecb, exchangerate_api
EXCHANGERATE_API_KEYEmpty; uses ExchangeRate-API's keyed endpoint instead of the rate-limited open-access one
VEHICLE_FX_CACHE_TTL_MS21600000 (6 hours)
VEHICLE_VIN_DECODERnone (offline VIN checks only); nhtsa decodes VINs with NHTSA vPIC (sends the VIN to NHTSA)
VEHICLE_VIN_DECODER_TIMEOUT_MS3000

The app accepts pre-existing environment variables over .env. No live credentials are included. .env, dependencies, build output and npm cache are ignored by Git. Never commit or log CDP_API_KEY_SECRET, X402_WALLET_ADDRESS's private key (never requested or stored by this app), or DATABASE_URL.

Architecture

text
src/
  api/app.ts             Express application factory; no listener side effects
  api/agent.ts           Agent-marketplace discovery, pricing, tool-catalog and capability-registry builders
  api/manifest.ts        /agent.json, /.well-known/ai-plugin.json and /.well-known/agent.json builders
  api/llms-txt.ts        /llms.txt plain-text briefing builder
  api/landing.ts         Dependency-free HTML landing page for browsers
  api/openapi.ts         OpenAPI 3.1 built from shared Zod schemas
  domain/capabilities.ts The single AgentCapability registry every consumer above reads from
  domain/roadmap.ts      Planned-but-not-implemented future tools (metadata only, nothing callable)
  domain/financial.ts    Percentage/rounding helpers and maintenance defaults
  domain/oman/types.ts          Oman domain vocabulary (property types, furnished status, source types)
  domain/oman/locations.ts      Muscat area/wilayat normalization (English + Arabic aliases)
  domain/oman/comparables.ts    Comparable selection: size/bedroom/furnished tolerances, IQR outlier removal
  domain/oman/confidence.ts     Deterministic confidence scoring (never model-invented)
  domain/oman/dataProviders.ts  OmanPropertyDataProvider abstraction: Official (real NCSI context) + Listing (stub) + Manual + Database + Composite
  domain/oman/fixtures.ts       Curated/demo Muscat rental & sale benchmark dataset (clearly labeled, not live data)
  domain/oman/marketRepository.ts PropertyMarketRepository interface (engine-agnostic) + MemoryPropertyMarketRepository
  domain/oman/importPipeline.ts Validating CSV/JSON import pipeline for production market records
  domain/oman/dataQualityScore.ts Deterministic, non-LLM per-record data-quality scoring (Partner Data Feed)
  domain/oman/partners.ts       PropertyDataPartner/PartnerRepository abstraction + MemoryPartnerRepository (Partner Data Feed)
  domain/oman/cache.ts          ComparableCache interface + MemoryComparableCache (Redis-ready shape, not yet wired)
  domain/oman/config.ts         OMAN_PROPERTY_DATA_MODE / staleness threshold / cache TTL / NCSI / Partner Data Feed config readers
  domain/oman/officialContext.ts       OfficialMarketContext type + field-map-driven NCSI record adapter (never a hardcoded field name)
  domain/oman/officialContextCache.ts  OfficialMarketContextCache interface + MemoryOfficialMarketContextCache
  services/ncsi/ncsiClient.ts   NcsiClient: catalog discovery, dataset records, retries, timeout, normalized errors
  db/marketSchema.ts    PostgreSQL schema for property_market_records (independent migration ledger)
  db/marketStore.ts     PostgresPropertyMarketRepository: the production PropertyMarketRepository implementation
  db/partnerSchema.ts   PostgreSQL schema for data_partners + property_market_records partner columns (migration v2)
  db/partnerStore.ts    PostgresPartnerRepository: the production PartnerRepository implementation (shares its Pool with PostgresPropertyMarketRepository)
  api/marketDataRoutes.ts Partner Data Feed ingestion + internal status/health routes (never in the capability registry)
  middleware/partnerAuth.ts Per-partner bearer-token auth (X-Partner-Token) + internal shared-secret auth (X-Internal-Api-Key)
  marketImportCli.ts    `npm run market:import -- <file> [PARTNER_ID]` CLI entry point
  ncsiDiscoverCli.ts    `npm run ncsi:discover` CLI: lists NCSI's live catalog and flags real-estate-relevant datasets
  schemas/              Strict shared input and output contracts
  services/property.ts  Validated calculations shared by both transports
  services/omanProperty.ts Deterministic Oman property-analysis pipeline (analyze_oman_property)
  middleware/auth.ts    API-key authentication
  middleware/rateLimit.ts In-memory, IP-keyed rate limiter for public agent endpoints
  billing/catalog.ts    Indicative prices and injectable authorization boundary (single source of truth)
  billing/service.ts    BillingService: the only place a price is read; usage recording
  billing/usage.ts      UsageRepository abstraction (Console/Memory/Postgres implementations)
  billing/x402.ts       Real x402 payment middleware + always-on x402 info builder
  config/env.ts         Validated environment configuration
  utils/                Safe errors and metadata-only logging
  mcp/server.ts         MCP registration factory (shared by both transports)
  mcp/remote.ts         Remote Streamable HTTP MCP handler (/mcp) + /api/v1/mcp/status builder
  mcp.ts                Local stdio entry point
  server.ts             HTTP listener and shutdown
  calculators.ts        Compatibility re-exports

Services validate direct calls as well as transport calls. New capabilities belong in the shared catalog with input/output schemas and a service function; the catalog drives REST, MCP and OpenAPI. No calculation logic lives in the HTTP or MCP handlers, and no pricing logic lives in the calculation services — every price is read through BillingService, which reads billing/catalog.ts.

Capability registry

src/domain/capabilities.ts exports one array, capabilities: AgentCapability[], and that array is the only place a tool's name, path, description, whenToUse/useCases (the recommendation-layer hint for which tool answers which situation), input/output schema, example, price, currency, payment protocol, idempotency and side-effect flag are written. Every consumer below reads this array rather than holding a second copy:

  • REST route registration (api/app.ts) — both the X-API-Key and x402 route families
  • The OpenAPI document (api/openapi.ts) — operationId is the capability's own name (analyze_property, compare_properties, estimate_maintenance, analyze_oman_property), and request/response examples come from capability.example/capability.execute(capability.example)
  • The MCP server (mcp/server.ts) — tool name, schemas, and a description built from capability.description + capability.whenToUse
  • The x402 payment gate and BillingService (billing/x402.ts, billing/service.ts) — billing/catalog.ts's prices object is derived from capabilities (Object.fromEntries(capabilities.map(c => [c.name, c.price]))), never a second literal
  • Every agent-marketplace/discovery endpoint (/api/v1/agent, /api/v1/pricing, /api/v1/tools, /api/v1/capabilities, /agent.json, the two /.well-known/... manifests, /llms.txt)

Adding a capability to this one array is what makes it real everywhere at once; nothing else needs to be told about it separately. src/domain/roadmap.ts holds a separate, much smaller list (plannedCapabilities) of future tools — estimate_property_rent, analyze_lease, check_contract_risk, diagnose_maintenance_issue, estimate_repair_cost, generate_property_report — that are metadata only: no route, no MCP registration, no schema, no price, nothing callable. They exist so /agent.json and /llms.txt can tell an agent what's coming without it mistaking a name on a list for a working endpoint; promoting one to a real capability means adding a full entry to capabilities, the only registry that drives actual behavior (analyze_oman_property was itself on this list until this phase; it now has a full entry and has been removed from plannedCapabilities).

REST API

REST base: http://localhost:8787.

MethodEndpointAuth
GET/Public — HTML landing page (browsers) or JSON discovery (Accept: application/json)
GET/agent.jsonPublic — full agent manifest (protocols, x402 terms, complete tool catalog)
GET/.well-known/ai-plugin.jsonPublic — OpenAI-plugin-style manifest
GET/.well-known/agent.jsonPublic — A2A-style Agent Card
GET/llms.txtPublic — plain-text briefing for LLM-based agents (text/plain)
GET/api/v1/capabilitiesPublic — machine-first capability registry (schemas, pricing, when to use)
GET/api/v1/agentPublic — legacy agent-marketplace metadata (superseded by /agent.json)
GET/api/v1/pricingPublic — full pay-per-call price list
GET/api/v1/toolsPublic — legacy tool catalog (superseded by /api/v1/capabilities)
GET/api/v1/x402Public — x402 protocol/pricing info (always available, independent of X402_ENABLED)
GET/api/v1/x402/statusPublic — factual runtime status: enabled, mode, network, asset, facilitator, walletConfigured, paymentEnforcement
POST/api/v1/property/analyzeX-API-Key
POST/api/v1/property/compareX-API-Key
POST/api/v1/maintenance/estimateX-API-Key
POST/api/v1/oman/property/analyzeX-API-Key
POST/api/v1/risk/company-reputation-checkX-API-Key (x402: /api/v1/x402/risk/company-reputation-check; L402: /api/v1/l402/risk/company-reputation-check)
POST/api/v1/risk/business-risk-scoreX-API-Key (x402: /api/v1/x402/risk/business-risk-score; L402: /api/v1/l402/risk/business-risk-score; MPP: /api/v1/mpp/charge/business_risk_score)
POST/api/v1/documents/facts-extractX-API-Key (x402: /api/v1/x402/documents/facts-extract; L402: /api/v1/l402/documents/facts-extract; MPP: /api/v1/mpp/charge/document_facts_extract) — 1 MB JSON body limit
POST/api/v1/finance/invoice-anomaly-checkX-API-Key (x402: /api/v1/x402/finance/invoice-anomaly-check; L402: /api/v1/l402/finance/invoice-anomaly-check; MPP: /api/v1/mpp/charge/invoice_anomaly_check) — 1 MB JSON body limit
POST/api/v1/automotive/vehicle-value-estimateX-API-Key (x402: /api/v1/x402/automotive/vehicle-value-estimate; L402: /api/v1/l402/automotive/vehicle-value-estimate; MPP: /api/v1/mpp/charge/vehicle_value_estimate)
POST/api/v1/intelligence/research-companyX-API-Key
POST/api/v1/intelligence/find-companiesX-API-Key
POST/api/v1/intelligence/analyze-company-riskX-API-Key
POST/api/v1/procurement/oman-supplier-checkX-API-Key
GET/api/v1/healthPublic liveness
GET/openapi.jsonPublic raw OpenAPI document
POST/api/v1/market-data/importX-Partner-Token (Partner Data Feed — infrastructure, not an agent capability; see below)
GET/api/v1/internal/market-data/statusX-Internal-Api-Key (Partner Data Feed; see below)
GET/api/v1/internal/market-data/partnersX-Internal-Api-Key (Partner Data Feed; see below)

Legacy /v1/... POST routes and /health remain available. Response migration: legacy routes now return the same envelope as canonical routes; read results from response.data. Unknown fields are rejected, names must be unique after trimming, and unauthenticated startup is no longer allowed.

Success:

json
{
  "success": true,
  "data": {},
  "meta": { "requestId": "server-generated-uuid" }
}

A tool call (the four POST capabilities, on either the API-key or x402 route family) additionally enriches meta with the tool name, its price and its currency, so an agent can confirm what it was charged without a second lookup:

json
{
  "success": true,
  "data": { "grossYield": 8.47 },
  "meta": { "requestId": "server-generated-uuid", "tool": "analyze_property", "price": 0.01, "currency": "USD" }
}

Error:

json
{
  "success": false,
  "error": {
    "code": "INVALID_INPUT",
    "message": "Input validation failed",
    "details": [{ "path": "propertyValue", "message": "Property value must be at least 0.01" }]
  },
  "meta": { "requestId": "server-generated-uuid" }
}

Responses include X-Request-ID. Codes cover 400 validation/malformed JSON, 401 authentication, 404 unknown endpoints, 413 bodies over 32kb, 415 unsupported content type/encoding, 500 sanitized internal failures, and 429 for a configured rate-limit adapter. PostgreSQL mode enforces customer minute limits and monthly quotas; env-key development mode has no default limiter. /openapi.json intentionally returns the raw specification for discovery tools.

Agent discovery, pricing and tool catalog

bash
curl -s https://api.rafidsystem.com/api/v1/agent
json
{
  "success": true,
  "data": {
    "name": "Rafid Intelligence Network",
    "description": "Property and facility intelligence tools for AI agents",
    "version": "0.1.0",
    "mcp": true,
    "docs": "/docs",
    "openapi": "/openapi.json",
    "health": "/api/v1/health",
    "pricing": "/api/v1/pricing",
    "tools": "/api/v1/tools",
    "x402": "/api/v1/x402",
    "x402Enabled": false,
    "endpoints": ["/api/v1/property/analyze", "/api/v1/property/compare", "/api/v1/maintenance/estimate", "/api/v1/oman/property/analyze"]
  },
  "meta": { "requestId": "..." }
}

GET /api/v1/pricing returns { "currency": "USD", "model": "pay-per-call", "tools": { "analyze_property": 0.01, "compare_properties": 0.03, "estimate_maintenance": 0.02, "analyze_oman_property": 0.25 } } — the exact same object billing/catalog.ts defines, with nothing recomputed or duplicated. GET /api/v1/tools returns one entry per capability with a full JSON Schema for its input and a derived output summary, enough for an agent to construct a valid call without reading any documentation.

Landing page

Human visitors to / get a small, dependency-free HTML page (no build step, no external assets) led by the agent-native positioning ("Property intelligence built for AI agents. Discover. Pay per call. Execute."), an "Agent Integration" section with copyable MCP/x402/OpenAPI/manifest examples, the tool list and pricing, and links to /docs, /openapi.json and /api/v1/health. API-key/REST access is documented as a compatibility option, not the primary message. This is purely a presentation layer over the same data the JSON endpoints return; nothing about the API's behavior changes. Any client that sends Accept: application/json (including a plain fetch()/curl with an explicit header, or any existing integration) is unaffected and keeps receiving the JSON discovery payload it always has.

Property analysis

PowerShell (replace the key placeholder):

powershell
$headers = @{ 'X-API-Key' = '<your-api-key>' }
$body = @{ propertyValue = 85000; annualRent = 7200; serviceCharge = 650; maintenanceCost = 400 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:8787/api/v1/property/analyze -Headers $headers -ContentType application/json -Body $body

The resulting data includes grossYield: 8.47, netYield: 7.24, annualOperatingCost: 1050, and annualNetIncome: 6150.

Optional inputs: serviceCharge, maintenanceCost, otherAnnualCosts, vacancyRatePct (0–100). The old maintenance alias is accepted, but cannot be supplied alongside maintenanceCost.

Formulas:

  • Gross annual income = annualRent.
  • Effective annual rent = annualRent × (1 − vacancyRatePct / 100).
  • Operating cost = serviceCharge + maintenanceCost + otherAnnualCosts.
  • Net annual income = effective annual rent − operating cost.
  • Gross yield = annualRent / propertyValue × 100.
  • Net yield = net annual income / propertyValue × 100.
  • Simple payback = propertyValue / net annual income; null for nonpositive income or a number too large to represent safely.

All money is OMR. Outputs are rounded to two decimals using JavaScript numbers, preserving the original MVP convention (not a settlement ledger). Finite input amounts are bounded to 1e12; property value must be at least 0.01. Negative net income is valid. Legacy output fields grossYieldPct, netYieldPct, and annualOperatingCosts remain. Financing, taxes, transaction fees and appreciation are excluded.

Property comparison

powershell
$body = @{ properties = @(
  @{ name = 'A'; propertyValue = 85000; annualRent = 7200 },
  @{ name = 'B'; propertyValue = 100000; annualRent = 7000 }
) } | ConvertTo-Json -Depth 5
Invoke-RestMethod -Method Post -Uri http://localhost:8787/api/v1/property/compare -Headers $headers -ContentType application/json -Body $body

Requires 2–20 properties with unique, nonblank names (up to 120 characters). Returns calculated properties and sortedByNetYield names, descending by rounded net yield, retaining input order for ties. This ordering is a metric comparison, not investment advice.

Maintenance estimate

powershell
$body = @{ propertyValue = 100000; ageYears = 12; units = 1 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:8787/api/v1/maintenance/estimate -Headers $headers -ContentType application/json -Body $body

This example returns annual maintenance 1300 OMR, monthly reserve 108.33 OMR and maintenance percentage 1.3, plus assumptionsUsed.

Default annual rates: age below 5 → 0.6%; 5–under 10 → 0.9%; 10–under 20 → 1.3%; 20+ → 1.8%. Defaults: age 0, units 1. Additional units add 35 OMR/year each. Supported ages are 0–200 and units 1–10000.

text
annual estimate = propertyValue × annualRatePct / 100
                + (units − 1) × additionalUnitCost

Override assumptions per request with "assumptions": { "annualRatePct": 1.5, "additionalUnitCost": 50 }. The returned percentage includes the additional unit allowance. Legacy annualRent is accepted but unused. Property type and area are not modeled yet and are rejected as unknown fields. This preserves the existing heuristic without inventing unsupported local cost factors; it requires calibration before commercial estimates.

Oman property analysis (Muscat)

analyze_oman_property ($0.25/call) is the first Rafid capability whose value comes from local market evidence, not just arithmetic over the caller's own numbers. It answers "is this Muscat property reasonably priced as a rental investment?" with structured, sourced evidence — never a hard-coded "good"/"bad investment" verdict.

powershell
$body = @{ governorate = 'Muscat'; area = 'Al Mouj'; propertyType = 'apartment'; bedrooms = 2; sizeSqm = 130; askingPriceOMR = 118000 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:8787/api/v1/oman/property/analyze -Headers $headers -ContentType application/json -Body $body

Required: governorate, area, propertyType (apartment | villa | townhouse), sizeSqm, askingPriceOMR. Optional: wilayat, bedrooms, bathrooms, furnished (furnished | semi_furnished | unfurnished), optionalAnnualServiceChargeOMR, optionalAnnualMaintenanceOMR.

Coverage (MVP): Muscat governorate only. Supported areas: Al Mouj, Muscat Hills, Qurum, Bausher, Azaiba, Al Khuwair, Madinat Al Irfan, Ghubrah (domain/oman/locations.ts's MUSCAT_AREAS). English and common Arabic spellings both resolve to the same canonical area (e.g. الموج / Al Mouj / the wave → Al Mouj) — deliberately without fuzzy/phonetic matching, so an unlisted place name is reported matchType: "unmatched" rather than guessed at.

Comparable selection (domain/oman/comparables.ts): never compares across property type; filters by area + property type + a ±20% size tolerance (SIZE_TOLERANCE_PCT) to build the reported comparableCount, then narrows to an exact bedroom match and exact furnished-status match where at least 3 (MIN_COMPARABLES) exist, relaxing to ±1 bedroom or "any furnished status" only when the exact match is too small (reported via bedroomToleranceApplied/furnishedFilterRelaxed and surfaced in riskFlags/assumptions); records older than 540 days are excluded outright as stale before any of this; a standard 1.5×IQR rule then removes statistical outliers from what remains (sampleSizeUsed). Rents quoted annually are normalized to a monthly figure before any comparison.

Confidence (domain/oman/confidence.ts): a fixed, documented weighted formula over sample size, data freshness, size/bedroom similarity to the subject, and price dispersion (coefficient of variation) — never a model-invented number. Below MIN_COMPARABLES used comparables, confidence is always { score: 0, level: "insufficient" }.

Fallback behavior: below MIN_COMPARABLES comparables (rental side) or an unsupported governorate/area, insufficientMarketData: true is returned, unavailableOutputs lists exactly which fields were withheld (e.g. market.estimatedMonthlyRentOMR, investment.grossYieldPct), and deterministic figures that need no market data (pricePosition.askingPricePerSqmOMR, investment.estimatedOperatingCostOMR) are still computed. Nothing is ever fabricated to fill a gap.

Rafid does not scrape property websites, and never will as a way of sourcing this data. OfficialOmanDataProvider and ListingDataProvider (domain/oman/dataProviders.ts) are real, wired-in seams for a future official/licensed feed — they return empty today rather than faking or scraping data. Production market data is expected to come from one of: official government/statistical sources, a licensed real-estate data vendor, a partner data-sharing agreement, or a customer's own listing/transaction export. All of these reach the analysis pipeline the same way — as rows imported into the property_market_records table via the import pipeline below — so no scraping code exists or is planned anywhere in this codebase.

Provenance: every response includes a provenance array — { sourceType, sourceName, sourceDate, recordCount } — so a caller can tell official_statistics, listing_asking_price, partner_feed and manual_benchmark apart, and can tell an asking price from a confirmed completed-transaction price. The bundled ManualDatasetProvider (domain/oman/fixtures.ts) is a small, explicitly curated benchmark dataset, clearly labeled in every sourceName as demo/MVP data — never official statistics and never confirmed transaction prices. riskFlags includes demo_dataset_not_live_market_data only when a manual_benchmark record actually contributed to the comparables used for that specific answer — it is computed per-response, not hard-coded, so an answer built entirely from database-backed production records (official statistics, a licensed listing feed, or a partner feed) does not carry it.

Data freshness: every response includes a dataQuality block — { latestDataDate, dataFreshnessDays, sampleSize, sourceTypes, staleMarketData } — computed from whichever comparables actually drove the answer, even when the sample is too small to produce a full result (freshness is never hidden). dataFreshnessDays beyond OMAN_MARKET_STALE_DAYS (default 365 days) sets staleMarketData: true, adds a stale_market_data risk flag, and applies a fixed, documented penalty inside the confidence formula (domain/oman/confidence.ts) — so an agent consuming this API can decide how much to trust an answer from dataQuality/confidence/riskFlags alone, without needing to inspect raw records.

Output shape: normalizedLocation, subjectProperty, market (estimated monthly/annual rent range, comparable/sample counts, data freshness), investment (gross/net yield, operating cost, net income), pricePosition (asking price/sqm vs. the observed sale-comparable range, below_market/at_market/above_market/insufficient_data), comparablesSummary (rent-per-sqm low/median/high of the sample actually used), dataQuality, historicalSalesContext, riskFlags, confidence, provenance, assumptions, insufficientMarketData, unavailableOutputs. Strict output schema in schemas/omanOutputs.ts.

Historical sales context: every response also includes historicalSalesContext — { available, recordsAvailable, recentComparableSales, medianHistoricalPricePerSqmOMR, recentMedianPricePerSqmOMR, oldestRecordDate, latestRecordDate, sourceTypes, priceSemantics, phaseBreakdown } — a separate, additive field surfacing long-term completed-sale history (e.g. a multi-year partner feed spanning 2006-2026) alongside, but never mixed into, market/pricePosition/comparablesSummary/confidence above, which stay driven exclusively by the current-comparable pool (comparables.ts's 540-day MAX_DATA_AGE_DAYS cutoff, unchanged). recordsAvailable/oldestRecordDate/latestRecordDate/medianHistoricalPricePerSqmOMR are computed with real database aggregation (PropertyMarketRepository.getHistoricalSaleStatistics()) rather than reusing the capped, most-recent-first candidate pool findSaleComparables() returns, so they stay accurate for a history longer than that cap. recentComparableSales/recentMedianPricePerSqmOMR are a genuinely matched subset — area, property type, size similarity (±20%) and bedrooms where available — within the configurable OMAN_RECENT_SALES_DAYS window (default 730 days), computed by comparables.ts's selectRecentSaleRecords(); this is a deliberately separate selection path from selectComparables(), both because the recency window differs and because a completed-sale record routinely has no recorded furnished status (true of every partner-feed sale record observed so far) — selectComparables() requires one and would otherwise silently report zero recent sales for exactly this data. phaseBreakdown (only relevant to a phased-development partner feed like Al Mouj's) lists per-phase { phaseName, recordCount, medianPriceOMR, medianPricePerSqmOMR, oldestRecordDate, latestRecordDate }, but only for phases meeting the same MIN_COMPARABLES sample-size floor used elsewhere — a phase trend is never reported from an insufficient sample — capped to the busiest 10 phases. available: false (with every other field at its empty default) whenever the location is unsupported or the active provider has no genuine transaction-level record store for it (manual/demo mode, official statistics, the not-yet-integrated listing feed) — demo/benchmark data is never presented as real historical sales intelligence. priceSemantics reports only what the source data itself recorded (e.g. "contracted_unit_price") — never verified_conveyance_price, government_registered_transaction or final_title_transfer_price unless the source genuinely supports that claim.

Rafid Agent Intelligence: company research, discovery and risk (research_company, find_companies, analyze_company_risk)

Phase 1 of a broader expansion beyond Oman property/business data: three capabilities that gather evidence about a company from the open web (plus, for identity, Rafid's own Oman company registry data) rather than computing purely from caller-supplied numbers. All three are entirely inert by default — no external network call, no cost — until an operator explicitly configures a provider; see src/intelligence/config.ts and the block of WEB_SEARCH_PROVIDER/TAVILY_API_KEY/INTELLIGENCE_LLM_PROVIDER/ANTHROPIC_API_KEY/INTELLIGENCE_LLM_MODEL/RISK_LIVE_CHECKS_ENABLED/INTELLIGENCE_CACHE_TTL_MS/RISK_CACHE_TTL_MS variables in .env.example. With nothing configured, every one of these still returns a fully schema-valid, honest dataMode: "not_configured" result — an empty/null shape, never a fabricated one — which is also what makes them safe to exercise from the generic per-capability test loops (tests/http.test.ts/tests/mcp.test.ts/tests/x402.test.ts, which call capability.execute(capability.example) with no way to inject a mock network dependency).

  • research_company (POST /api/v1/intelligence/research-company, $0.15/call) — a structured research brief (overview, products/services, leadership, funding, competitors, technology signals, recent developments, risk flags) built from public web search results (src/intelligence/webSearch/), optionally passed through an LLM synthesis step (src/intelligence/synthesis/) to extract structured fields — every synthesized field is schema-validated before being returned, and any fact the evidence doesn't support is null/empty rather than guessed. Every response carries sources, confidence, dataFreshness and limitations; caching is by normalized company/website/country/depth/focusAreas (src/intelligence/companyResearch/provider.ts).
  • find_companies (POST /api/v1/intelligence/find-companies, $0.05/call) — discovers candidate companies from public web search results matching industry/location/size/keyword criteria. Never fabricates a company to fill a requested limit: without an LLM synthesis provider configured, no structured company records are extracted at all (only raw cited sources are returned) rather than risk inventing one. Results are capped internally at 20 per request regardless of the requested limit (up to 100); requestedLimit vs appliedLimit in the response makes that gap explicit (src/intelligence/companyDiscovery/provider.ts).
  • analyze_company_risk (POST /api/v1/intelligence/analyze-company-risk, $0.35/call) — gathers evidence-tiered risk signals across up to 8 checks (corporate_identity, domain, website, sanctions, adverse_news, security_signals, reputation, legal_signals) and never returns a safe/unsafe verdict — only evidence, each item explicitly tiered confirmed_evidence / public_allegation / automated_indicator / missing_information, for the calling agent or a human to weigh. corporate_identity reuses search_oman_company's own service function directly (no second lookup implementation) and is the only check performed by default; domain/website/sanctions are free public-source checks (RDAP, the public OFAC/Consolidated Screening List) gated behind RISK_LIVE_CHECKS_ENABLED because they make a real outbound call; adverse_news/reputation/legal_signals are web-search-backed and require a web search provider. The website check reuses domain/oman/feedSecurity.ts's and safeFeedFetch.ts's SSRF protections (private/loopback IP blocking, DNS-rebinding-safe resolution, validated redirects) unchanged — the same protection the Production Feed Runner relies on for partner-supplied URLs (src/intelligence/risk/provider.ts, src/intelligence/risk/checks/).

Cost/margin visibility (internal only): src/intelligence/costEstimator.ts holds Rafid's own documented, hand-estimated upstream cost assumptions per capability (never a provider's actual confidential pricing) plus a per-process running counter of real provider calls actually made; GET /api/v1/internal/revenue/unit-economics (same REVENUE_INTERNAL_API_KEY gate as the rest of revenueRoutes.ts — no new secret, no new dashboard) exposes revenuePerCallUSD/estimatedProviderCostPerCallUSD/estimatedLlmCostPerCallUSD/estimatedGrossMarginPerCallUSD per priced capability. This is a gross, not net, margin — it excludes hosting, database and facilitator/network fees — and is never exposed on any public endpoint.

Provider architecture: WebSearchProvider (src/intelligence/webSearch/provider.ts) and IntelligenceSynthesizer (src/intelligence/synthesis/synthesizer.ts) are both small interfaces with a NotConfigured* default and one real implementation each (Tavily for search, Anthropic for synthesis) — mirroring business-data/sources/provider.ts's own provider-interface pattern — so swapping either vendor later means writing one new class, never touching a capability's schema, route, MCP registration or pricing.

Procurement: Oman supplier screening (oman_supplier_check, $0.50/call)

POST /api/v1/procurement/oman-supplier-check (API key) · POST /api/v1/x402/procurement/oman-supplier-check (x402, $0.50) · MCP tool oman_supplier_check. For procurement agents that need to screen an Oman supplier before requesting a quotation. It is public-source procurement screening — not KYC/AML, not legal verification, not vendor approval — so it answers with identityConfirmed, procurementSuitability (appears_suitable / review_recommended / insufficient_information / potential_risk) and sanctions potential_match, never "verified", "approved" or "sanctioned".

Input: companyName (required), optional crNumber, website, email, phone, address, requiredProductOrService. Inputs are normalized deterministically (src/supplier-check/normalize.ts): the registry's own company-name normalizer, a small Arabic→English company-word glossary (ش.م.م → LLC, للتجارة → TRADING, ...), Oman phone numbers to E.164, URLs/domains (incl. co.om-style registrable domains), free-mail detection and CR cleanup.

Checks, each assessed independently (src/supplier-check/analysis/): company identity (strong/moderate/weak/unconfirmed, scored from name match + CR + contact corroboration × source trust — demo data can never reach "confirmed"), business-activity relevance (deterministic EN/AR keyword taxonomy; pass/partial/fail/unknown), website (SSRF-safe fetch of the supplied or registry website: name on site, domain vs name/registry, corporate email, phone, address, redirects), contact consistency (email/website/phone vs registry and website; lookalike-domain detection; a Gmail/Outlook address alone is only a low note), address consistency (pass/partial/conflict/unknown vs registry governorate/wilayat/area), sanctions (provider-based: UN Security Council Consolidated List and US Consolidated Screening List incl. OFAC SDN, with a conservative central fuzzy matcher — ≥0.88 similarity and ≥60% distinctive-token overlap; exact-name matches are still only potential_match), and public-risk signals (web results must name the supplier next to a risk term; derived lookalike-domain / contradictory-CR indicators; neutral "potential public-risk signal detected" wording).

Risk model (src/supplier-check/scoring.ts, fully documented in code and returned per call as riskModel.components): adverse evidence adds points (e.g. exact-name sanctions match +60, CR registered to another company +35, fuzzy sanctions match +35, lookalike domain +25, activity mismatch +15, email-domain mismatch +10); missing information adds no risk points and instead lowers confidence (weighted evidence coverage). score ≥45 → high, ≥20 → medium, else low; thin coverage → insufficient_data unless the score is already high.

Data sources and reuse: identity resolves through the existing canonical Oman company registry (getOmanCompanyDataProvider() — same OMAN_BUSINESS_DATA_MODE provider, rankCompanies, mergeCompanyRows and provenance as search_oman_company). No company records are created. Website and sanctions checks reuse RISK_LIVE_CHECKS_ENABLED; public-web signals reuse WEB_SEARCH_PROVIDER. With nothing enabled, those checks report not_checked honestly.

Caching & idempotency: each provider's evidence is cached independently in rafid_supplier_evidence (PostgreSQL when SUPPLIER_EVIDENCE_DATABASE_URL/DATABASE_URL is set — one CREATE TABLE IF NOT EXISTS, upsert on (provider_id, evidence_key); in-memory otherwise). Default TTLs: identity 7 days, website 3 days, sanctions 12 hours, public web 1 day (override with SUPPLIER_*_CACHE_TTL_MS). Only successful evidence is cached (outages are retried), and the final risk result is never cached — it is recomputed from evidence on every call. This is an evidence cache, not a second company/source-evidence model: it is never merged into oman_companies.

Risk intelligence: global company reputation check (company_reputation_check, $0.40/call)

POST /api/v1/risk/company-reputation-check (API key) · POST /api/v1/x402/risk/company-reputation-check (x402, $0.40) · POST /api/v1/l402/risk/company-reputation-check (L402, when enabled) · MCP tool company_reputation_check. Category risk_intelligence, idempotent. For an agent that must look into a company in any country before signing a contract, onboarding a vendor, sending money, partnering, investing or approving a marketplace seller. It is evidence-first: every signal cites evidence ids, every evidence item carries its source, authority tier, retrieval time and relevance. It never labels a company "safe", "legitimate", "fraudulent" or "sanctioned". It is not a legal, KYC/AML, credit or compliance determination and does not guarantee legitimacy.

Input: companyName (required); strongly recommended country (ISO-2/ISO-3/English name); optional website, domain, registrationNumber, lei (checksum-validated), legalName, city, industry. Strict schema: unknown fields, unknown countries, invalid LEIs and a website/domain pair that disagree are rejected with 400 INVALID_INPUT — which also means no x402 settlement and no L402 token burn.

bash
curl -X POST http://localhost:8787/api/v1/risk/company-reputation-check \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"companyName":"Example Technologies Ltd","country":"United Kingdom","website":"https://example.com"}'

Output (abridged): company, resolution (status resolved|probable|ambiguous|unresolved|registry_not_checked, confidence, candidates), reputationScore 0-100, confidenceScore 0-100, trustLevel (insufficient_evidence / significant_concerns / some_concerns / no_major_concerns_found / favorable_public_signals), scores per dimension plus scoreBreakdown (weight, raw score, coverage) and confidenceBreakdown, identity, sanctions (possible vs high-confidence matches, lists checked/unavailable), adverseMedia (events with category, legalStage, established, coverage count, sources), customerSentiment, onlinePresence, legalRiskSignals, businessStabilitySignals, cyberDomainSignals, transparencySignals, positiveSignals, redFlags, evidenceSummary, evidence[], coverage.providers[] (per-provider status incl. not_configured / timeout / stale_cache), warnings[] (COMPANY_NOT_RESOLVED, AMBIGUOUS_COMPANY, INSUFFICIENT_EVIDENCE, PROVIDER_UNAVAILABLE, PROVIDER_TIMEOUT, RATE_LIMITED, DATA_STALE, PROVIDER_NOT_CONFIGURED, COUNTRY_NOT_PROVIDED), limitations, summary, methodology. The registry's documented example (/api/v1/capabilities, OpenAPI) is the real pipeline run over an explicitly synthetic scenario (fictional company, .example publishers) — regenerate with node --import tsx scripts/generateCompanyReputationExample.ts.

Pipeline (src/company-reputation/): input validation → normalization (normalization.ts: global legal forms, ISO countries, domains, URLs, registration numbers, LEI) → providers run concurrently, isolated, time-boxed (providers/runner.ts: per-provider timeout, one retry for free idempotent sources only, never for paid searches) with a cache-first hybrid lookup → evidence dedup and syndication grouping (deduplication.ts) → entity resolution (companyResolver.ts) → relevance filtering of third-party items → analyzers (analyzers/*: identity, sanctions, adverse media, customer sentiment, online presence, business stability, cyber/domain, transparency) produce signals → scoring.ts → confidence.ts. Providers only collect normalized evidence; analyzers never fetch; scoring only sees signals.

Scoring (config.ts, version crc-1.0.0): weights identity 20%, legal/regulatory 20%, adverse media 15%, customer reputation 15%, online presence 10%, business stability 10%, cyber/domain 5%, transparency 5%. Per dimension: signal points = severity points (info 3, low 8, medium 20, high 40, critical 80) × strength (authority × relevance × bounded corroboration), saturating (swing × (1 − e^(−points/40))) so many weak items cannot add up linearly and one weak item moves a dimension only a few points. Each dimension is then shrunk toward a neutral prior of 50 by its evidence coverage: score = 50 + coverage × (raw − 50). Missing data therefore reads as "unknown" (50) with low confidence, never as "good". Documented caps: a high-confidence sanctions match caps the score at 20; an established conviction/judgment for fraud, corruption, sanctions or criminal conduct from an official or major source caps it at 35.

Confidence (separate from reputation): 30% entity-resolution confidence, 20% weighted dimension coverage, 15% independent sources (event groups, not articles), 10% source authority, 10% freshness (stale cache and old coverage count less), 10% evidence diversity, 5% jurisdiction-registry coverage; −3 per provider outage; capped at 40 when identity is ambiguous and 50 when it is unresolved. 78/91 and 78/34 mean very different things.

Safety properties (tested): a similar sanctioned name is not a match (shared conservative matcher from src/shared/sanctions/, ≥0.88 similarity and ≥60% token overlap); an exact name without a corroborating country/registration identifier is only possible; one anonymous complaint cannot cause a major penalty; ten syndicated copies are one event; missing data and provider timeouts are never negative signals; allegations stay allegations (legal stage is taken only from explicit source wording); same-name companies in other jurisdictions (different country, different legal form in the text) are excluded, and with an ambiguous identity only identifier-backed items are attributed. External text is sanitized and instruction-like text is removed; nothing is sent to an LLM.

Providers:

ProviderCategoryEnabled byCost
GLEIF Global LEI Index (global, official)registryRISK_LIVE_CHECKS_ENABLED=truefree
UK Companies Houseregistry (GB only)COMPANIES_HOUSE_API_KEYfree key
Rafid Oman company registryregistry (OM only)OMAN_BUSINESS_DATA_MODE=database/composite (demo data never used)own DB
UN Security Council Consolidated ListsanctionsRISK_LIVE_CHECKS_ENABLED=truefree
US Consolidated Screening List (incl. OFAC SDN)sanctionsRISK_LIVE_CHECKS_ENABLED=true (+ optional SANCTIONS_CSL_API_KEY)free
EU Financial Sanctions FilessanctionsEU_SANCTIONS_LIST_URL (tokenised URL issued by the EU)free
OpenSanctions (UN/EU/UK/US and national lists)sanctionsOPENSANCTIONS_API_KEYpaid licence for commercial use
News / adverse media (2 searches)newsWEB_SEARCH_PROVIDER=tavily + TAVILY_API_KEY~$0.016
Review platforms / forums (1 search)reviewssame as news~$0.008
Company website (1 page, SSRF-safe)websiteRISK_LIVE_CHECKS_ENABLED=truefree
RDAP domain registrationdomainRISK_LIVE_CHECKS_ENABLED=truefree

Any provider can be switched off with COMPANY_REPUTATION_DISABLED_PROVIDERS (ids or categories). With nothing enabled the call still returns a valid, clearly limited result (trustLevel: insufficient_evidence).

Cache: provider evidence is cached per (provider, normalized company identity) in rafid_company_evidence_cache (PostgreSQL via COMPANY_REPUTATION_DATABASE_URL or DATABASE_URL, one CREATE TABLE IF NOT EXISTS; in-memory otherwise). TTLs: registry 7d, sanctions 12h, news 1d, reviews 3d, website 3d, domain 7d (COMPANY_REPUTATION_*_TTL_MS). Cached evidence keeps its original retrieval time (fetchedAt/observedAt, fromCache: true). If a refresh fails, expired evidence up to COMPANY_REPUTATION_MAX_STALE_MS (default 14 days) is served flagged stale_cache + DATA_STALE and lowers confidence. Outages are never cached, and the final score is never cached.

Unit economics: worst case uncached ~3 web searches (≈ $0.024 at Tavily-class pricing); everything else is free public data. Estimated gross margin at $0.40 ≈ $0.37/call before hosting and facilitator fees. OpenSanctions, if enabled, adds its own per-call licence cost. Per-provider calls, cache hits, stale serves, outages and latency are recorded in-process (src/company-reputation/telemetry.ts) and returned by the internal GET /api/v1/internal/revenue/unit-economics route (companyReputationTelemetry).

Premium decision API: company_due_diligence ($1.50/call)

POST /api/v1/risk/company-due-diligence · MCP tool company_due_diligence. This is the normalized decision layer over the existing business_risk_score evidence pipeline: it resolves the company, evaluates registration, website identity, sanctions, adverse news, legal, financial and reputation signals, then returns a deterministic riskScore/riskLevel, confidence, provenance, a recommendation and a machine-actionable decision.action.

Request fields are company (required), plus optional domain, country, registrationNumber, lei, purpose, depth (quick, standard, enhanced, default standard) and a strict checks object (registry, sanctions, adverseMedia, reputation, businessRisk, all defaulting to true). The capability costs $1.50 USD and uses the shared registry/payment rails; it is automatically present in REST, MCP, x402/L402/MPP/account billing where enabled, OpenAPI and discovery manifests.

Example:

bash
curl -X POST https://<host>/api/v1/risk/company-due-diligence \
  -H "Content-Type: application/json" -H "X-API-Key: <key>" \
  -d '{"company":"Example Ltd","domain":"example.com","country":"GB","purpose":"supplier_onboarding"}'

The free preview only reports recognized input, source coverage and available result sections. It never returns the paid score, sanctions result, red flags, recommendation or decision. If the entity is ambiguous or cannot be reliably resolved, the paid request returns AMBIGUOUS_ENTITY or ENTITY_NOT_FOUND rather than guessing; unavailable providers lower coverage/confidence and remain explicit in the result.

Risk intelligence: global business risk score (business_risk_score, $0.50/call)

text
business_risk_score
$0.50 / call
Global business due-diligence and risk assessment

POST /api/v1/risk/business-risk-score (API key) · POST /api/v1/x402/risk/business-risk-score (x402, $0.50 = 500000 USDC atomic units) · POST /api/v1/l402/risk/business-risk-score (L402, when enabled) · POST /api/v1/mpp/charge/business_risk_score and MPP session calls (when enabled) · MCP tool business_risk_score (remote /mcp, stdio, and paid /mcp/mpp when enabled). Category risk_intelligence, idempotent. Answers one question for procurement, finance, vendor-onboarding, B2B sales, marketplace, insurance, lending, compliance and autonomous purchasing agents: is it risky to do business with this company? — anywhere in the world, from evidence rather than model knowledge.

text
Agent:
"Check this supplier before I pay their invoice."

↓

business_risk_score
$0.50

↓

Returns:
- risk score (0–100, 100 = highest detected risk) and risk level
- confidence (0–1, separate from risk)
- company identity (entity match confidence, registry status)
- corporate risk
- financial risk
- sanctions / compliance risk (with match strength)
- reputation risk
- operational risk
- digital risk
- evidence (source, URL, observed/retrieved dates, reliability, claim)
- positive signals
- risk flags (each traceable to evidence ids)
- recommended due-diligence action

Input (strict; unknown fields → 400 INVALID_INPUT, no payment): companyName (required) plus optional country (ISO 3166 alpha-2/alpha-3/English name — strongly recommended), registrationNumber, lei, website, address, city, industry, knownAliases (≤ 5), includeNews (default true), includeDigitalSignals (default true). Names are normalized on their distinctive core (legal forms compared separately), websites to an https:// origin + registrable domain, countries to ISO-2.

bash
curl -X POST http://localhost:8787/api/v1/risk/business-risk-score \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"companyName":"Example Trading Ltd","country":"GB","website":"https://example.com"}'

Output: status (assessed | insufficient_data), assessmentId, business (name, legalName, country, registrationNumber, registrationStatus, incorporationDate, registry, website, entityMatchConfidence, resolutionStatus), riskScore, riskLevel (0–20 low · 21–40 moderate · 41–60 elevated · 61–80 high · 81–100 critical), confidence, components (corporate/financial/compliance/reputation/operational/digital risk; null = no data coverage), scoreBreakdown (per-category contributions, effective weights, floors applied), riskFlags[] and positiveSignals[] (code, category, severity, weight, confidence, points, evidenceIds, factStatus observed|reported|alleged, requiresVerification), sanctionsScreening (lists checked/unavailable, names screened, matches with matchStrength possible|high and list type sanctions vs export-control/debarment), evidence[] (type, evidenceClass — official_record / regulatory_action / court_record / credible_journalism / other_publication / user_generated / allegation / self_published — sourceName, sourceUrl, publishedAt, observedAt, retrievedAt, ageDays, stale, freshness, claim, reliability, relevance, fromCache; lookup_result items document clean checks), recommendation (proceed / proceed_with_monitoring / enhanced_due_diligence / manual_review / avoid_automated_transaction + reason codes), dataCoverage, confidenceBreakdown, providers[], warnings[], assumptions[], limitations[], methodology, evaluatedAt. The documented example (OpenAPI, /api/v1/capabilities, x402 Bazaar metadata) is the real pipeline over an explicitly synthetic scenario — regenerate with node --import tsx scripts/generateBusinessRiskExample.ts (a test fails if it drifts).

Structured errors (no charge on x402/L402/MPP — only successful calls settle): AMBIGUOUS_ENTITY 409 (details.candidates with registration numbers — retry with one), ENTITY_NOT_FOUND 404 (the jurisdiction's registry has no such company and nothing else shows it exists), PROVIDER_TIMEOUT 504 / PROVIDER_UNAVAILABLE 503 / RATE_LIMITED 429 (every identity source failed — the minimum identity check could not be performed), INVALID_INPUT 400, INTERNAL_ERROR 500. INSUFFICIENT_DATA is a 200 result (status: "insufficient_data", null score) returned only when the deployment has no evidence source for the company at all.

Pipeline (src/business-risk/): normalization → stage 1 (registries GLEIF / UK Companies House / Rafid Oman registry, company website, RDAP — concurrent, isolated, time-boxed, cache-first) → entity resolution (companyResolver.ts from company_reputation_check: registration number/LEI decisive, other countries never match, near-equal candidates are ambiguous — deep scoring and every paid search are skipped for ambiguous/unknown entities) → stage 2 with the resolved identifiers (sanctions & restricted-party lists screening the name, supplied aliases and former registry names; news; reviews; regulator publications; Companies House filing status; Google Safe Browsing) → dedup + relevance filtering (same-name companies in other jurisdictions are excluded, never merged) → six detectors (signals.ts) → scoring.ts → confidence.ts → recommendation.ts. It reuses company_reputation_check's provider contract, runner, evidence cache, normalizers, resolver and sanctions/adverse-media classifiers; only the risk model is new.

Scoring (config.ts, version brs-1.0.0, echoed in every response's methodology): every signal contributes SEVERITY_POINTS[severity] (info 0 · low 8 · medium 20 · high 40 · critical 70) × rule weight × confidence. Category score = baseline − baseline·(1−e^(−positive/30)) + (100−baseline)·(1−e^(−negative/45)) — positives only erode the residual baseline, they never cancel a red flag; categories without coverage are null. Overall = weighted mean of the cov

Similar modules

Browse Company lookup