Skip to content
Gauge

ax-check.com

DoorProfit API

api.doorprofit.com

ax-check.com

Checked 18 October 2026

F

23/100

Agents need your login

Pricing is clear, but docs leave agents guessing

Billing page lists Free/Starter $25/Growth $50/Pro $100 with limits, no login needed. But no docs index, quickstart, or API reference was found for setup.

3 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a Markdown Accept header; no Markdown representation offered.

Docs

Add links to your product documentation and quickstart in /llms.txt.

/llms.txt returns HTTP 404, so no documentation index exists.

Recorded coding sessions

Same task, 3 models, no api.doorprofit.com account. Recorded 18 October 2026 on Pi.

3 recorded · 5m 32s

5m 32s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using DoorProfit API. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

I don't know DoorProfit. Let me try searching for it in the repository/environment.

ls -la /sandbox/repo 2>/dev/null; echo "---"; cat /sandbox/repo/README* 2>/dev/null | head -50
total 16
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Oct 18 02:39 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
---
grep -ril "doorprofit" /sandbox /usr/local/lib/node_modules/@earendil-works 2>/dev/null | head; echo "---env---"; env | grep -i door
Failed/sandbox/.gauge/pi/sessions/2026-10-18T02-39-28-039Z_01a14ce0-df27-730b-84b7-1bb5e5d8a41e.jsonl
/sandbox/.gauge/pi/sessions/2026-10-18T02-39-28-039Z_01a14ce0-df27-730b-84b7-1bb5e5d8a41e.jsonl
---env---


Command exited with code 1
Thinking…

The only thing that matches is the session log itself (my own prompts). There are no DoorProfit products in the repository. Let me check network access and see if there's any documentation.

cd /sandbox/repo && git log --oneline -10; echo "--- branches ---"; git branch -a; echo "--- all files ---"; git ls-files | head -50
9ebf5fb Initial workspace
9ebf5fb Initial workspace
--- branches ---
* main
--- all files ---
.gitignore
timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://pypi.org/simple/ 2>&1; echo "--- npm search ---"; timeout 20 npm search doorprofit 2>&1 | head -20
--- npm search ---
--- npm search ---
No matches found for "doorprofit"
npm notice
npm notice New major version of npm available! 10.9.8 -> 12.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0
npm notice To update run: npm install -g npm@12.2.0
npm notice
Thinking…

No network (curl to pypi returned nothing — let's confirm). Let me test a few things: check network connectivity, and search for SDKs via pip/npm.

timeout 20 curl -sS -w "\nHTTP:%{http_code}\n" https://pypi.org/pypi/doorprofit/json 2>&1 | tail -5; echo "=== generic net test ==="; timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://example.com 2>&1
{"message": "Not Found"}
{"message": "Not Found"}
HTTP:404
=== generic net test ===
200
timeout 20 pip index versions doorprofit 2>&1 | head -5; echo "=== pip download dir/cache ==="; pip download doorprofit --no-deps -d /tmp/dp 2>&1 | tail -5
ERROR: No matching distribution found for doorprofit
ERROR: No matching distribution found for doorprofit
=== pip download dir/cache ===
ERROR: Could not find a version that satisfies the requirement doorprofit (from versions: none)
ERROR: No matching distribution found for doorprofit
Thinking…

The network is up and running, but "doorprofit" doesn't exist on PyPI or npm. Let me check a few other things: the doorprofit.com domain, the GitHub API, and other package managers (RubyGems, Packagist, crates.io, NuGet, Go).

Just to be safe, I'll do these checks.

for u in "https://doorprofit.com" "https://www.doorprofit.com" "https://api.doorprofit.com" "https://docs.doorprofit.com" "https://doorprofit.io" "https://developer.doorprofit.com"; do printf "%-40s " "$u"; timeout 12 curl -sS -o /dev/null -w "%{http_code}\n" "$u" 2>&1 | tail -1; done
https://doorprofit.com 301
https://doorprofit.com                   301
https://www.doorprofit.com               200
https://api.doorprofit.com               200
https://docs.doorprofit.com              000
https://doorprofit.io                    000
https://developer.doorprofit.com         000
echo "=== github code/repo search ==="; timeout 20 curl -sS "https://api.github.com/search/repositories?q=doorprofit" 2>&1 | head -c 600; echo; echo "=== rubygems ==="; timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://rubygems.org/api/v1/gems/doorprofit.json; echo "=== packagist ==="; timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://packagist.org/packages/doorprofit.json; echo "=== crates.io ==="; timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://crates.io/api/v1/crates/doorprofit; echo "=== nuget ==="; timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://api.nuget.org/v3-flatcontainer/doorprofit/index.json
=== github code/repo search ===
=== github code/repo search ===
{
  "total_count": 3,
  "incomplete_results": false,
  "items": [
    {
      "id": 584248263,
      "node_id": "R_kgDOItLrxw",
      "name": "Doorprofit",
      "full_name": "IlijaAngelov/Doorprofit",
      "private": false,
      "owner": {
        "login": "IlijaAngelov",
        "id": 18699175,
        "node_id": "MDQ6VXNlcjE4Njk5MTc1",
        "avatar_url": "https://avatars.githubusercontent.com/u/18699175?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/IlijaAngelov",
        "html_url": "https://github.com/IlijaAngelov",
        "followers_url": "https://api
=== rubygems ===
404
=== packagist ===
301
=== crates.io ===
403
=== nuget ===
404
Thinking…

Interesting — www.doorprofit.com returns 200, and api.doorprofit.com also returns 200. Let's investigate what they actually are.

I'll try fetching the contents.

echo "=== www ==="; timeout 20 curl -sSL "https://www.doorprofit.com" 2>&1 | head -c 3000
=== www ===
=== www ===
<!doctype html>
<html lang="en">
<head>
     
        <!-- Google Analytics GA4 -->
        <script async src="https://www.googletagmanager.com/gtag/js?id=G-SH245RPSF0"></script>
        <script>
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'G-SH245RPSF0');
            gtag('config', 'AW-11042652801');
            window._dpGA4Id = 'G-SH245RPSF0';
        </script>
        <script defer src="https://www.doorprofit.com/assets/js/doorprofit-events.js?v=1.0.0"></script>
        <!-- end Google Analytics GA4 -->
        
        <!-- ChatGPT Ads Pixel -->
        <script>!function(w,d,s,u){if(w.oaiq)return;var q=function(){q.q.push(arguments)};q.q=[];w.oaiq=q;var j=d.createElement(s);j.async=1;j.src=u;var f=d.getElementsByTagName(s)[0];f.parentNode.insertBefore(j,f)}(window,document,"script","https://bzrcdn.openai.com/sdk/oaiq.min.js");oaiq("init",{"pixelId":"7z6YHsK8Y1EFVCoAd17mo6","debug":false});</script>
        <!-- end ChatGPT Ads Pixel -->
        
	<!-- Sentry -->
	<script
		defer
		src="https://js.sentry-cdn.com/e3fc7263cb424bf2064e34ae21697ba2.min.js"
		crossorigin="anonymous"
	></script>
	<!-- end Sentry -->
	<script>window.sentryOnLoad=function(){Sentry.setTag("template_version","460");}</script><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Crime Map & Neighborhood Safety Data | Find Safe Places to Live</title>
<meta name="description" content="Interactive crime maps with real-time data. See exactly where crime happens, compare neighborhoods, and make confident decisions about where to live. Trusted by 250,000+ monthly users.">
<meta name="keywords" content="crime map, neighborhood safety, safe neighborhoods, crime data, where to live, neighborhood crime rates, safety ratings, crime statistics, moving safety, home safety">
<meta name="robots" content="max-image-preview:large">
<meta property="og:locale" content="en_US">
<meta property="og:type" content="website">
<meta property="og:title" content="Crime Map & Neighborhood Safety Data | Find Safe Places to Live">
<meta property="og:url" content="https://www.doorprofit.com/">
<meta property="og:site_name" content="DoorProfit">
<meta property="og:image" content="https://www.doorprofit.com/assets/images/doorprofit-og.png">
<link rel="canonical" href="https://www.doorprofit.com/"><meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@door_profit">
<meta name="twitter:creator" content="@door_profit">
<meta name="twitter:title" content="Crime Map &amp; Neighborhood Safety Data | Find Safe Places to Live">
<meta name="twitter:description" content="Interactive crime maps with real-time data. See exactly where crime happens, compare neighborhoods, and make confident decisions about where to live. Trusted by 250,000+ monthly users.">
<meta name="twitter:image" content="https://www.do
echo "=== api root ==="; timeout 20 curl -sSL -w "\nHTTP:%{http_code}\n" "https://api.doorprofit.com" 2>&1 | head -c 2000; echo; echo "=== api /health ==="; timeout 20 curl -sSL -w "\nHTTP:%{http_code}\n" "https://api.doorprofit.com/health" 2>&1 | head -c 500
=== api root ===
=== api root ===
<!DOCTYPE html>
<html lang="en">
<head>
    <!-- Google Analytics GA4 -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-SH245RPSF0"></script>
    <script>
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'G-SH245RPSF0');
        gtag('config', 'AW-11042652801');
        window._dpGA4Id = 'G-SH245RPSF0';
    </script>
    <script src="https://www.doorprofit.com/assets/js/doorprofit-events.js?v=1.0.0"></script>
    <!-- end Google Analytics GA4 -->
    
    <!-- Sentry -->
    <script
        src="https://js.sentry-cdn.com/e3fc7263cb424bf2064e34ae21697ba2.min.js"
        crossorigin="anonymous"
    ></script>
    <!-- end Sentry -->

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DoorProfit API - Crime Data, Registered Offenders &amp; Location Intelligence API</title>
    <meta name="description" content="The most comprehensive location intelligence API. Access crime data, registered offender registries, school ratings, and neighborhood analytics for any US address. Trusted by developers worldwide.">
    <meta name="keywords" content="crime API, crime data API, registered offender API, location intelligence, neighborhood data API, safety API, real estate API, crime map API, developer API">
    <meta name="robots" content="max-image-preview:large">
    <meta property="og:locale" content="en_US">
    <meta property="og:type" content="website">
    <meta property="og:title" content="DoorProfit API - Crime Data, Registered Offenders &amp; Location Intelligence API">
    <meta property="og:description" content="The most comprehensive location intelligence API. Access crime data, registered offender registries, school ratings, and neighborhood analytics for any US address. Trusted by developers worldwide.">
    <meta property="og:url" content="https://api.doorprofit.com/">
    <m
=== api /health ===
<!DOCTYPE html>
        <html>
        <head>
            <title>404 - Not Found</title>
            <style>
                body { font-family: -apple-system, sans-serif; background: #ffffff; color: #0f172a; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
                .container { text-align: center; }
                h1 { font-size: 72px; margin: 0; color: #4f46e5; }
                p { color: #475569; margin: 16px 0 24px; }
                a { c
Thinking…

DoorProfit is a real product: a crime map / location intelligence API. Let's read the landing page at api.doorprofit.com in full — it likely contains docs, pricing, quickstart, and endpoints.

Let's dump the full HTML and parse it.

cd /tmp && timeout 30 curl -sSL "https://api.doorprofit.com" -o dp_api.html && wc -c dp_api.html && python3 - <<'EOF'
import re,html
s=open('/tmp/dp_api.html',encoding='utf-8',errors='replace').read()
s=re.sub(r'<script.*?</script>','',s,flags=re.S|re.I)
s=re.sub(r'<style.*?</style>','',s,flags=re.S|re.I)
s=re.sub(r'<[^>]+>',' ',s)
s=html.unescape(s)
s=re.sub(r'[ \t]+',' ',s)
s=re.sub(r'\n\s*\n+','\n',s)
print(s.strip()[:9000])
EOF
75416 dp_api.html
75416 dp_api.html
DoorProfit API - Crime Data, Registered Offenders & Location Intelligence API 
 DoorProfit API
 Home 
 Docs 
 Main Site 
 Login 
 Get Your API Key 
 DoorProfit API
 Home 
 Docs 
 Main Site 
 Login 
 Get Your API Key 
 Crime & Location Data API 
 The Crime API You Can Depend On 
 Since 2020, DoorProfit has provided reliable, accurate crime data, registered offender registries, and location intelligence for developers like you. 
 Get Your API Key 
 Start in minutes.
 4.8 
 1 import DoorProfit from 'doorprofit' 
 2 const api = DoorProfit (
 3 DOORPROFIT_API_KEY 
 4 )
 5 
 6 const crimeMap = api. getCrimeMap ({
 7 location : 'Tulsa, OK' ,
 8 radius : 5 ,
 9 })
 10 
 11 return < img src ={crimeMap} /> 
 Trusted by developers at 
 Zillow 
 Redfin 
 Opendoor 
 Compass 
 Realtor.com 
 Simple Integration 
 Start building in minutes 
 Clean JSON responses. No complicated setup. Just make a request and get the data you need. 
 // Request fetch ( 'https://api.doorprofit.com/crime/?address=500+Adams+St+Tulsa+OK&key=REDACTED' , { method : 'GET' , headers : { 'Content-Type' : 'application/json' , } } ) . then ( response => response. json ()) . then ( data => { console. log (data) } ); 
 // Response { "success" : true , "location" : { "address" : "500 Adams St, Tulsa, OK" , "lat" : 36.1540 , "lng" : -95.9928 } , "crime_score" : "C+" , "crime_numeric" : 0.21 , "incidents" : null // counts only in cities that publish incidents } 
 API Endpoints 
 Everything you need 
 Comprehensive location intelligence APIs to power your application. 
 🚨 
 Crime Data API 
 Get detailed crime statistics, safety scores, and incident data for any US address. Perfect for real estate platforms, insurance companies, and safety applications. 
 Crime grade (A+ to F) 
 Numeric crime score 
 Crime breakdown by type 
 Interactive crime map 
 Recent incidents with coords 
 Updated daily 
 Get Crime Data API 
 500 Adams St, Tulsa, OK 
 C+ 
 Crime Score: 0.21 
 Higher than 65% of US neighborhoods 
 Assault 38% above avg 
 Theft 35% above avg 
 Robbery 47% above avg 
 Burglary 56% above avg 
 ⚠️ 
 Registered Offender Registry 
 Access 900,000+ registered offenders across all 50 states. Essential for family safety apps and real estate due diligence. 
 900K+ offender records 
 Photos when available 
 Search by radius 
 Distance calculations 
 Offense details 
 All 50 states covered 
 Get Offender API 
 Within 1 mile radius 
 👤 
 J*** D** 
 Offense: Sexual Assault (2019) 
 512 S Cheyenne Ave, Tulsa, OK 36.1538, -95.9945 
 0.3 miles away 
 + 4 more offenders within 1 mile 
 🏘️ 
 Neighborhood Data API 
 The most comprehensive neighborhood data API available. Demographics, income, housing, education, cost of living, rent, weather, and disaster risk in one call. 
 Demographics & population 
 Income & housing data 
 Education attainment rates 
 Cost of living indices 
 Rent estimates by bedroom 
 Weather & disaster risk 
 Get Neighborhood API 
 Archer Heights, Tulsa 
 Population 4,280 
 Median Income $17,600 
 Median Home $52,035 
 Median Rent $850/mo 
 Bachelor's+ 18.3% 
 Cost of Living 95.2 
 Median Age 34.2 
 Tornado Risk 280.1 
 Embeddable Widget 
 Add a crime heat map to any website 
 Two lines of HTML. Interactive map. Census-block crime data. No Mapbox or Google Maps token needed. 
 The easiest way to show crime data on your site 
 Paste a snippet, get a full interactive crime heatmap powered by MapLibre GL and our census-block crime tiles. Perfect for real estate listings, neighborhood pages, and property management portals. 
 Interactive heatmap with census-block crime data 
 Works on any website — just paste and go 
 SEO-friendly attribution backlink included 
 Bot traffic excluded — only real humans count 
 5,000 views/month free on every plan 
 No Mapbox or Google Maps token required 
 Get Your API Key 
 your-website.html 
 <!-- Crime Map Widget --> 
 <div id = "doorprofit-crime-map" 
 data-address = "500 Main St, Dallas, TX" 
 data-key = "YOUR_API_KEY" 
 data-zoom = "13" 
 data-height = "400px" ></div> 
 <script src = "https://api.doorprofit.com/v1/embed/crime-map.js" 
 async ></script> 
 ✓ Interactive crime heatmap renders automatically 
 Testimonials 
 Loved by developers 
 See what our customers are saying about us. 
 "We switched from building our own crime data scraper to DoorProfit API and saved 40+ hours per month. The data quality is excellent and the response times are incredibly fast." 
 MR 
 Michael Rodriguez 
 CTO at SafeMove 
 "The registered offender API is the most comprehensive I've found. We use it for our property management platform and our clients love the safety features it enables." 
 SJ 
 Sarah Johnson 
 Founder at RentSafe 
 "Easy integration, great documentation, and reliable uptime. We've been using DoorProfit API for 2 years now with zero issues. Highly recommended." 
 DK 
 David Kim 
 Lead Developer at HomeScore 
 Start building today 
 Get your free API key right now. 
 Get Your API Key 
 © 2026 DoorProfit API. All rights reserved.  |  Terms  |  Privacy  |  support@doorprofit.com
cd /tmp && grep -oE 'href="[^"]+"|src="[^"]+"' dp_api.html 2>/dev/null | sort -u | head -60
(no output)
(no output)
Thinking…

Let's look for the Docs link and the pricing page.

cd /tmp && grep -oE '(href|src)="[^"]*"' dp_api.html | sed 's/.*="//;s/"$//' | sort -u | grep -v 'googletagmanager\|sentry\|\.png\|\.ico\|\.svg\|fonts' | head -60
/
/
/assets/images/api-tulsa-crime-map.webp
/assets/images/crime-map-embed-widget-desktop.webp
/docs/
/login/
/signup/
https://api.doorprofit.com/privacy/
https://api.doorprofit.com/site.webmanifest
https://api.doorprofit.com/terms/
https://www.doorprofit.com/
https://www.doorprofit.com/assets/js/doorprofit-events.js?v=1.0.0
https://www.trustpilot.com/review/doorprofit.com
mailto:support@doorprofit.com
for u in "https://api.doorprofit.com/docs" "https://api.doorprofit.com/docs/" "https://api.doorprofit.com/pricing" "https://api.doorprofit.com/pricing/"; do printf "%-45s " "$u"; timeout 15 curl -sSL -o /tmp/x.html -w "%{http_code} %{size_download}\n" "$u" 2>&1|tail -1; done
https://api.doorprofit.com/docs 200 134468
https://api.doorprofit.com/docs               200 134468
https://api.doorprofit.com/docs/              200 134468
https://api.doorprofit.com/pricing            404 866
https://api.doorprofit.com/pricing/           404 866
Thinking…

The docs page is 134KB. Let's fetch and parse it. Also going to look for pricing info — it's probably on the main site (www.doorprofit.com/api or /pricing) or within the docs.

cd /tmp && timeout 30 curl -sSL "https://api.doorprofit.com/docs/" -o dp_docs.html && python3 - <<'EOF'
import re,html
s=open('/tmp/dp_docs.html',encoding='utf-8',errors='replace').read()
s=re.sub(r'<script.*?</script>','',s,flags=re.S|re.I)
s=re.sub(r'<style.*?</style>','',s,flags=re.S|re.I)
s=re.sub(r'<[^>]+>',' ',s)
s=html.unescape(s)
s=re.sub(r'[ \t]+',' ',s)
s=re.sub(r'\n\s*\n+','\n',s)
t=s.strip()
print(len(t))
open('/tmp/dp_docs.txt','w').write(t)
print(t[:6000])
EOF
45153
45153
API Documentation - DoorProfit API 
 DoorProfit API
 Home 
 Docs 
 Main Site 
 Login 
 Get Your API Key 
 DoorProfit API
 Home 
 Docs 
 Main Site 
 Login 
 Get Your API Key 
 Getting Started 
 Introduction 
 Authentication 
 Quick Start 
 API Endpoints 
 Crime 
 Neighborhood 
 Registered Offenders 
 Response Data 
 Crime Response 
 Neighborhood Response 
 Offenders Response 
 Account 
 Usage Statistics 
 Embed & Widgets 
 Crime Map Widget 
 Metering 
 Combined Snippet 
 AI Agents 
 MCP Server 
 Code Examples 
 All Languages 
 Reference 
 Error Handling 
 Rate Limits 
 Support 
 API Documentation 
 Welcome to the DoorProfit API! Get comprehensive location intelligence data for any US address with our simple REST API. 
 We provide three focused endpoints so you can query exactly the data you need. 
 Available Endpoints 
 /v1/crime - Crime safety scores and nearby incident counts 
 /v1/neighborhood - Demographics, income, housing, education, cost of living, rent, weather & disaster risk 
 /v1/offenders - Registered registered offenders with photos, addresses, and distances 
 /v1/usage - Check your API usage and remaining quota (free, doesn't count against quota) 
 The crime, neighborhood, and offenders endpoints accept either a street address or latitude/longitude coordinates. 
 Base URL 
 https://api.doorprofit.com/v1/ 
 Authentication 
 All API requests require authentication using your API key. You can pass your key as a query parameter. 
 💡 
 Don't have an API key yet? Sign up for free to get your key instantly. 
 Using Your API Key 
 Include your API key as the key parameter in every request: 
 https://api.doorprofit.com/v1/crime?address=500+Main+St+Dallas+TX+75202&key=REDACTED 
 ⚠️ 
 Keep your API key secure! Don't expose it in client-side code or public repositories. Use environment variables or server-side requests. 
 Quick Start 
 Get up and running in under 5 minutes. Here's how to make your first API call: 
 Get Your API Key 
 Sign up for a free account to get your API key. You'll receive 10 free API calls per month to test the integration. 
 Choose Your Endpoint 
 Call the endpoint for the data you need: 
 # Get crime statistics & safety scores
curl "https://api.doorprofit.com/v1/crime?address=500+Main+St+Dallas+TX+75202&key=REDACTED"
# Get neighborhood demographics
curl "https://api.doorprofit.com/v1/neighborhood?address=500+Main+St+Dallas+TX+75202&key=REDACTED"
# Get registered offenders in the area
curl "https://api.doorprofit.com/v1/offenders?address=500+Main+St+Dallas+TX+75202&key=REDACTED" 
 Parse the Response 
 You'll receive a JSON response with the requested data. That's it - you're ready to build! 
 Crime Endpoint 
 Get crime statistics, safety scores, crime breakdowns, and recent incident data for any location. Accepts either a street address or coordinates. 
 GET 
 /v1/crime 
 Parameters 
 Parameter 
 Type 
 Description 
 key Required 
 string 
 Your API key 
 Location (provide one of the following): 
 Option 
 Parameters 
 Description 
 Address 
 address 
 Full street address (URL encoded). Example: 500+Main+St+Dallas+TX+75202 
 Coordinates 
 lat + lng 
 Latitude and longitude. Example: lat=32.778635&lng=-96.807295 
 Example Requests 
 cURL 
 JavaScript 
 Python 
 PHP 
 # Query by address
curl "https://api.doorprofit.com/v1/crime?address=500+Main+St+Dallas+TX+75202&key=REDACTED"
# Query by coordinates
curl "https://api.doorprofit.com/v1/crime?lat=32.778635&lng=-96.807295&key=REDACTED" 
 // Query by address
const response = await fetch(
 'https://api.doorprofit.com/v1/crime?address=500+Main+St+Dallas+TX+75202&key=REDACTED'
);
const data = await response.json();
console.log(data.crime_score); // "C+"
console.log(data.incidents.count); // Incident count, or null if unavailable
console.log(data.incidents.by_type); // Counts by crime type
// Or query by coordinates
const coordsResponse = await fetch(
 'https://api.doorprofit.com/v1/crime?lat=32.778635&lng=-96.807295&key=REDACTED'
); 
 import requests
# Query by address
response = requests.get(
 'https://api.doorprofit.com/v1/crime',
 params={
 'address': '500 Main St Dallas TX 75202',
 'key': 'YOUR_API_KEY'
 }
)
data = response.json()
print(data['crime_score']) # "C+"
print(data['incidents']['by_type']) # Counts by crime type
# Or query by coordinates
response = requests.get(
 'https://api.doorprofit.com/v1/crime',
 params={'lat': 32.778635, 'lng': -96.807295, 'key': 'YOUR_API_KEY'}
) 
 <?php
// Query by address
$address = urlencode('500 Main St Dallas TX 75202');
$apiKey = 'YOUR_API_KEY';
$url = "https://api.doorprofit.com/v1/crime?address={$address}&key=REDACTED";
$response = file_get_contents($url);
$data = json_decode($response, true);
echo $data['crime_score']; // "C+"
print_r($data['incidents']['by_type']); // Counts by crime type
// Or query by coordinates
$url = "https://api.doorprofit.com/v1/crime?lat=32.778635&lng=-96.807295&key=REDACTED"; 
 Neighborhood Endpoint 
 Get comprehensive neighborhood data including demographics, income, housing, education, cost of living, rent estimates, weather, and natural disaster risk. Accepts either a street address or coordinates. 
 GET 
 /v1/neighborhood 
 Parameters 
 Parameter 
 Type 
 Description 
 key Required 
 string 
 Your API key 
 Location (provide one of the following): 
 Option 
 Parameters 
 Description 
 Address 
 address 
 Full street address (URL encoded). Example: 500+Main+St+Dallas+TX+75202 
 Coordinates 
 lat + lng 
 Latitude and longitude. Example: lat=32.778635&lng=-96.807295 
 Example Requests 
 cURL 
 JavaScript 
 Python 
 PHP 
 # Query by address
curl "https://api.doorprofit.com/v1/neighborhood?address=500+Main+St+Dallas+TX+75202&key=REDACTED"
# Query by coordinates
curl "https://api.doorprofit.com/v1/neighborhood?lat=32.778635&lng=-96.807295&key=REDACTED" 
 // Query by address
const response = await fetch(
 'https://api.doorprofit.com/v1/neighborhood?address=500+Main+St+Dallas+TX+75202&key=REDACTED'
);
cons
cd /tmp && timeout 30 curl -sSL "https://www.doorprofit.com" -o dp_www.html; grep -oiE '(href|src)="[^"]*(pricing|api|plan|developer)[^"]*"' dp_www.html | sed 's/.*="//;s/"$//' | sort -u | head -30
https://api.doorprofit.com/
https://api.doorprofit.com/
https://fonts.googleapis.com
Thinking…

Next, let's check the rest of the documentation, especially pricing/metering/rate limits, AI agents, MCP server, and the usage endpoints. I'll try reading the remaining text.

read /tmp/dp_docs.txt{"content":[{"text":" <?php\n$address = urlencode('500 Main St Dallas TX 75202');\n$apiKey = 'YOUR_API_KEY';\n$url = \"https://api.doorprofit.com/v1/neighborhoo…ctrl+o
{"content":[{"text":" <?php\n$address = urlencode('500 Main St Dallas TX 75202');\n$apiKey = 'YOUR_API_KEY';\n$url = \"https://api.doorprofit.com/v1/neighborhood?address={$address}&key=REDACTED\";\n$response = file_get_contents($url);\n$data = json_decode($response, true);\necho $data['neighborhood']['name']; // \"West End Historic District\"\necho $data['neighborhood']['demographics']['population']; // 4280\necho $data['neighborhood']['income']['median']; // 52400\necho $data['neighborhood']['housing']['median_home_value']; // 285000\necho $data['neighborhood']['rent']['median']; // 1150 \n Registered Offenders Endpoint \n Search for registered registered offenders by location or by name. Returns offender details including photos, addresses, offenses, and distances. \n GET \n /v1/offenders \n Parameters \n Parameter \n Type \n Description \n key Required \n string \n Your API key \n Search by Location \n Find offenders near a specific address or coordinates. \n Parameter \n Type \n Description \n address \n string \n Full street address (URL encoded). Example: 500+Main+St+Dallas+TX+75202 \n lat + lng \n number \n Latitude and longitude coordinates. Example: lat=32.778635&lng=-96.807295 \n radius \n number \n Search radius in miles. Default: 1 , Max: 5 \n Search by Name \n Search for offenders by name and/or location details. Use any combination of the parameters below - at least one is required. \n Parameter \n Type \n Description \n first_name \n string \n Offender's first name \n last_name \n string \n Offender's last name \n city \n string \n City name \n state \n string \n Two-letter state code. Example: OK , TX \n zipcode \n string \n 5-digit ZIP code \n Pagination \n Results are paginated. Use these parameters to navigate through large result sets. Each page request counts as one API credit. \n Parameter \n Type \n Description \n page \n integer \n Page number. Default: 1 \n limit \n integer \n Results per page. Default: 50 , Max: 100 \n Example Requests \n cURL \n JavaScript \n Python \n PHP \n # Search by address (default 1 mile radius)\ncurl \"https://api.doorprofit.com/v1/offenders?address=500+Main+St+Dallas+TX+75202&key=REDACTED\"\n# Search by coordinates with 2 mile radius\ncurl \"https://api.doorprofit.com/v1/offenders?lat=32.778635&lng=-96.807295&radius=2&key=REDACTED\"\n# Search by name\ncurl \"https://api.doorprofit.com/v1/offenders?last_name=Smith&state=TX&key=REDACTED\"\n# Search by name and city\ncurl \"https://api.doorprofit.com/v1/offenders?first_name=John&city=Dallas&state=TX&key=REDACTED\" \n // Search by address\nconst response = await fetch(\n 'https://api.doorprofit.com/v1/offenders?address=500+Main+St+Dallas+TX+75202&key=REDACTED'\n);\nconst data = await response.json();\nconsole.log(data.offenders); // Array of offenders\nconsole.log(data.offenders_count); // Total count\n// Search by name\nconst nameSearch = await fetch(\n 'https://api.doorprofit.com/v1/offenders?last_name=Smith&state=TX&key=REDACTED'\n);\nconst nameResults = await nameSearch.json();\n// Search with multiple criteria\nconst params = new URLSearchParams({\n first_name: 'John',\n city: 'Dallas',\n state: 'TX',\n key: 'YOUR_API_KEY'\n});\nconst multiSearch = await fetch(`https://api.doorprofit.com/v1/offenders?${params}`); \n import requests\n# Search by address\nresponse = requests.get(\n 'https://api.doorprofit.com/v1/offenders',\n params={\n 'address': '500 Main St Dallas TX 75202',\n 'key': 'YOUR_API_KEY'\n }\n)\ndata = response.json()\nprint(f\"Found {data['offenders_count']} offenders nearby\")\n# Search by name\nresponse = requests.get(\n 'https://api.doorprofit.com/v1/offenders',\n params={\n 'last_name': 'Smith',\n 'state': 'TX',\n 'key': 'YOUR_API_KEY'\n }\n)\n# Search with multiple criteria\nresponse = requests.get(\n 'https://api.doorprofit.com/v1/offenders',\n params={\n 'first_name': 'John',\n 'city': 'Dallas',\n 'state': 'TX',\n 'key': 'YOUR_API_KEY'\n }\n) \n <?php\n$apiKey = 'YOUR_API_KEY';\n// Search by address\n$address = urlencode('500 Main St Dallas TX 75202');\n$url = \"https://api.doorprofit.com/v1/offenders?address={$address}&key=REDACTED\";\n$data = json_decode(file_get_contents($url), true);\necho \"Found \" . $data['offenders_count'] . \" offenders nearby\\n\";\n// Search by name\n$url = \"https://api.doorprofit.com/v1/offenders?last_name=Smith&state=TX&key=REDACTED\";\n$data = json_decode(file_get_contents($url), true);\n// Search with multiple criteria\n$params = http_build_query([\n 'first_name' => 'John',\n 'city' => 'Dallas',\n 'state' => 'TX',\n 'key' => $apiKey\n]);\n$url = \"https://api.doorprofit.com/v1/offenders?{$params}\"; \n Usage Statistics Endpoint \n Check your current API usage, remaining quota, and subscription details. This endpoint is free and does not count against your API quota. \n GET \n /v1/usage \n Parameters \n Parameter \n Type \n Description \n key Required \n string \n Your API key \n Example Request \n curl \"https://api.doorprofit.com/v1/usage?key=REDACTED\" \n Example Response \n {\n \"success\": true,\n \"data\": {\n \"plan\": {\n \"type\": \"growth\",\n \"name\": \"Growth\",\n \"price_monthly\": 50\n },\n \"monthly\": {\n \"limit\": 600,\n \"used\": 127,\n \"remaining\": 473,\n \"percentage_used\": 21.2,\n \"resets_at\": \"2026-03-01T00:00:00+00:00\"\n },\n \"daily\": {\n \"limit\": 3000,\n \"used\": 45,\n \"remaining\": 2955,\n \"percentage_used\": 1.5,\n \"resets_at\": \"2026-02-03T00:00:00+00:00\"\n },\n \"rate_limit\": {\n \"requests_per_second\": 10\n },\n \"overage\": {\n \"enabled\": true,\n \"calls\": 0,\n \"amount_due\": \"0.00\",\n \"rate_per_call\": \"0.10\"\n },\n \"account\": {\n \"status\": \"active\",\n \"subscription_status\": \"active\",\n \"blocked\": false\n }\n },\n \"meta\": {\n \"note\": \"This endpoint does not count against your API quota.\",\n \"timestamp\": \"2026-02-02T15:30:00+00:00\"\n }\n} \n Crime Response \n The /v1/crime endpoint returns a safety grade and, in cities that publish police incidents, counts of those incidents near the point. \n Response Fields \n crime_score string \n Letter grade from A+ (safest) to F (least safe). In cities with incident-level coverage the grade is computed from actual reported incidents at the census-block level; elsewhere it falls back to block-group statistics. Example: \"C+\" \n crime_numeric number \n Numeric crime score from 0 to 1. Lower is safer. Example: 0.21 \n crime_score_source string \n \"incidents\" when the grade is derived from incident-level data for that census block, \"block_group\" when it comes from aggregated block-group statistics \n crime_description string \n Human-readable description of the crime level. Example: \"Higher than average\" \n incidents object | null \n Counts within 1,000 feet for the period the city's grade was built from ( date_from through date_to , which varies by city). by_type always includes all 12 types the grade uses; 0 means none were reported. null where we do not have incident data for that city. There is no list of individual incidents. \n Example Response \n {\n \"success\": true,\n \"location\": {\n \"address\": \"500 Main St, Dallas, TX 75202, USA\",\n \"city\": \"Dallas\",\n \"state\": \"TX\",\n \"zipcode\": \"75202\",\n \"county\": \"Dallas County\",\n \"lat\": 32.78014,\n \"lng\": -96.800454\n },\n \"crime_score\": \"C+\",\n \"crime_numeric\": 0.21,\n \"crime_score_source\": \"incidents\",\n \"crime_description\": \"Higher than average crime\",\n \"incidents\": {\n \"radius_feet\": 1000,\n \"date_from\": \"2025-04-01\",\n \"date_to\": \"2026-04-01\",\n \"count\": 245,\n \"by_type\": {\n \"murder\": 0,\n \"shooting\": 3,\n \"assault\": 74,\n \"sexual_offense\": 7,\n \"robbery\": 5,\n \"kidnapping\": 1,\n \"weapons\": 1,\n \"theft\": 109,\n \"burglary\": 5,\n \"drug_offense\": 1,\n \"vandalism\": 39,\n \"arson\": 0\n }\n }\n} \n Neighborhood Response \n The /v1/neighborhood endpoint returns comprehensive demographic, economic, housing, education, cost of living, rent, and weather data for the neighborhood at the given location. \n Top-Level Fields \n neighborhood.name string \n Name of the neighborhood \n neighborhood.type string \n \"neighborhood\" or \"block_group\" (fallback when no named neighborhood) \n Demographics \n demographics.population number \n Current population \n demographics.population_density number \n People per square mile \n demographics.population_growth_pct number \n Population growth over last 10 years (%) \n demographics.median_age number \n Median age of residents \n demographics.gender object \n Gender split: male_pct , female_pct \n demographics.race object \n Racial demographics: white_pct , black_pct , asian_pct , hispanic_pct , native_pct \n demographics.age_distribution object \n Age brackets: 0_5 , 6_11 , 12_17 , 18_24 , 25_34 , 35_44 , 45_54 , 55_64 , 65_74 , 75_84 , 85_plus (all %) \n Income & Housing \n income.median number \n Median household income (USD) \n income.average number \n Average household income (USD) \n income.distribution object \n Income brackets: under_15k through 200k_plus (all %) \n housing object \n median_home_value , total_households , avg_year_built , plus occupancy (owner/renter/vacant %) and household_types (families/married/children %) \n Education, Cost of Living & Rent \n education object \n Attainment rates: high_school_pct , associates_pct , bachelors_pct , graduate_pct \n cost_of_living object \n Index values (100 = national avg): overall_index , housing , food , healthcare , transportation , utilities , apparel , education , entertainment \n rent object \n median , average , and by_bedroom ( 1br , 2br , 3br , 4br ) \n Weather \n weather.temperatures object \n Seasonal highs/lows in °F: annual_high , annual_low , january_high , july_high , etc. \n weather.precipitation object \n rain_inches , rain_days_per_year , snow_inches , snow_days_per_year \n weather.natural_disaster_risk object \n Risk indices (100 = national avg): overall_index , earthquake , hail , hurricane , tornado , wind \n Example Response \n {\n \"success\": true,\n \"location\": {\n \"address\": \"500 Main St, Dallas, TX 75202, USA\",\n \"city\": \"Dallas\",\n \"state\": \"TX\",\n \"zipcode\": \"75202\",\n \"county\": \"Dallas County\",\n \"lat\": 32.78014,\n \"lng\": -96.800454\n },\n \"neighborhood\": {\n \"name\": \"West End Historic District\",\n \"type\": \"neighborhood\",\n \"demographics\": {\n \"population\": 4280,\n \"population_density\": 1200.5,\n \"population_growth_pct\": 3.5,\n \"median_age\": 34.2,\n \"gender\": { \"male_pct\": 51.2, \"female_pct\": 48.8 },\n \"race\": {\n \"white_pct\": 45.2,\n \"black_pct\": 28.3,\n \"asian_pct\": 5.1,\n \"hispanic_pct\": 18.7,\n \"native_pct\": 0.8\n },\n \"age_distribution\": {\n \"0_5\": 6.2, \"6_11\": 5.8, \"12_17\": 4.9,\n \"18_24\": 12.1, \"25_34\": 22.5, \"35_44\": 18.3,\n\n[791 more lines in file. Use offset=530 to continue.]","type":"text"}]}
grep -niE 'price|pricing|\$[0-9]|per month|/mo|free tier|quota|rate limit|plan' /tmp/dp_docs.txt | head -60
38: Rate Limits
38: Rate Limits 
47: /v1/usage - Check your API usage and remaining quota (free, doesn't count against quota) 
63: Sign up for a free account to get your API key. You'll receive 10 free API calls per month to test the integration. 
349: Check your current API usage, remaining quota, and subscription details. This endpoint is free and does not count against your API quota. 
365: "plan": {
368: "price_monthly": 50
400: "note": "This endpoint does not count against your API quota.",
745: The crime map widget uses a separate "embed views" quota from the regular API call quota. This keeps embeds cheap so you can put them on as many pages as you like. See the pricing page for plan limits and overage rates. 
748: Known crawlers and bots (Googlebot, Bingbot, AhrefsBot, social previewers, etc.) are automatically excluded and never count toward your quota 
776: Quota-friendly: Widget page-loads count against the separate embed views bucket , not your API calls. Geocodes are cached, so most embed loads cost zero. 
785: get_api_usage — your plan, usage, and remaining quota (free) 
801: Quotas & Billing 
802: MCP tool calls are metered exactly like REST calls — each data tool call uses 1 API call from your plan's monthly and daily quota. 
803: When the quota runs out, the AI receives the standard QUOTA_EXCEEDED error and will let you know it's time to upgrade . 
804: The test address 500 Main St, Dallas, TX 75202 never consumes quota — perfect for trying the integration. 
1247: Too Many Requests - Rate limit exceeded 
1265: QUOTA_EXCEEDED - Monthly API call limit reached 
1266: Rate Limits 
1267: To ensure fair usage and maintain service quality, the API enforces rate limits based on your subscription plan. See the pricing page for plan details and pricing. 
1268: Plan 
1271: Rate Limit 
1273: 10 calls/month 
1277: 300 calls/month 
1281: 600 calls/month 
1285: 1,200 calls/month 
1293: Use the /v1/usage endpoint to check your current usage without consuming quota: 
1302: X-Plan - Your current subscription plan 
1309: Response Time: Within 24 hours for all plans, priority support for paid plans 
1316: Yes! All paid plans include commercial usage rights. 
1318: API calls beyond your limit will return a 429 error. You can upgrade your plan anytime to get more calls. 
Thinking…

Let me try reading the rate limits/plans table, the AI Agents/MCP section, and the pricing page. The documentation mentions a "pricing page," but api.doorprofit.com/pricing returned a 404. It's probably either www.doorprofit.com/api or www.doorprofit.com/pricing. Let me check.

read /tmp/dp_docs.txt{"content":[{"text":" 401 \n Unauthorized - Invalid or missing API key \n 403 \n Forbidden - API key doesn't have access to this resource \n 404 \n Not Found - …ctrl+o
{"content":[{"text":" 401 \n Unauthorized - Invalid or missing API key \n 403 \n Forbidden - API key doesn't have access to this resource \n 404 \n Not Found - Address could not be geocoded \n 429 \n Too Many Requests - Rate limit exceeded \n 500 \n Server Error - Something went wrong on our end \n Error Response Format \n {\n \"success\": false,\n \"error\": {\n \"code\": \"INVALID_API_KEY\",\n \"message\": \"The API key provided is invalid or has been revoked.\",\n \"documentation_url\": \"https://api.doorprofit.com/docs/#authentication\"\n }\n} \n Common Error Codes \n INVALID_API_KEY - API key is missing, invalid, or revoked \n MISSING_PARAMETER - Required parameter is missing \n INVALID_ADDRESS - Address could not be parsed or geocoded \n INVALID_COORDINATES - Lat/lng values are out of range \n RATE_LIMIT_EXCEEDED - Too many requests, slow down \n QUOTA_EXCEEDED - Monthly API call limit reached \n Rate Limits \n To ensure fair usage and maintain service quality, the API enforces rate limits based on your subscription plan. See the pricing page for plan details and pricing. \n Plan \n Monthly Calls \n Daily Limit \n Rate Limit \n Free \n 10 calls/month \n 50/day \n 2 req/sec \n Starter \n 300 calls/month \n 1,500/day \n 5 req/sec \n Growth \n 600 calls/month \n 3,000/day \n 10 req/sec \n Pro \n 1,200 calls/month \n 6,000/day \n 20 req/sec \n Enterprise \n Custom \n Custom \n Custom \n Checking Your Usage \n Use the /v1/usage endpoint to check your current usage without consuming quota: \n curl \"https://api.doorprofit.com/v1/usage?key=REDACTED\" \n Response Headers \n Every API response includes headers to help you track usage: \n X-Monthly-Limit - Your monthly API call limit \n X-Monthly-Remaining - API calls remaining this month \n X-Daily-Limit - Your daily API call limit \n X-Daily-Remaining - API calls remaining today \n X-RateLimit-Limit - Requests allowed per second \n X-Plan - Your current subscription plan \n 💡 \n Pro tip: Cache API responses when possible to reduce calls and improve your app's performance. \n Support \n Need help? We're here for you. \n Contact Us \n Email: support@doorprofit.com \n Response Time: Within 24 hours for all plans, priority support for paid plans \n Common Questions \n Q: How often is the data updated? \n Crime data is updated daily. Registered offender data is refreshed weekly. Demographic data follows US Census updates. \n Q: What areas are covered? \n We cover all 50 US states with data for most metropolitan and suburban areas. \n Q: Can I use the API for commercial purposes? \n Yes! All paid plans include commercial usage rights. \n Q: What happens if I exceed my monthly limit? \n API calls beyond your limit will return a 429 error. You can upgrade your plan anytime to get more calls. \n 🚀 \n Ready to get started? Sign up for free and make your first API call in minutes!","type":"text"}]}
for u in "https://www.doorprofit.com/pricing" "https://www.doorprofit.com/pricing/" "https://www.doorprofit.com/api" "https://www.doorprofit.com/api/" "https://www.doorprofit.com/api-pricing" "https://www.doorprofit.com/api/pricing"; do printf "%-48s " "$u"; timeout 15 curl -sSL -o /tmp/p.html -w "%{http_code} %{size_download}\n" "$u" 2>&1|tail -1; done
https://www.doorprofit.com/pricing 404 26708
https://www.doorprofit.com/pricing               404 26708
https://www.doorprofit.com/pricing/              404 26709
https://www.doorprofit.com/api                   200 0
https://www.doorprofit.com/api/                  200 0
https://www.doorprofit.com/api-pricing           404 26712
https://www.doorprofit.com/api/pricing           404 26712
Thinking…

The pricing page isn't easy to find. Let me check the AI agents / MCP section of the documentation (lines 745–830), and look for the pricing page link from the docs HTML (it says "refer to the pricing page"). I'll grep for "pricing" links within the docs HTML.

Also, I'll check the /signup page and the sitemap.

cd /tmp && grep -oiE 'href="[^"]*pricing[^"]*"' dp_docs.html dp_www.html dp_api.html | sort -u; echo "=== sitemap ==="; timeout 20 curl -sSL "https://api.doorprofit.com/sitemap.xml" 2>&1 | head -c 1500; echo; echo "=== www sitemap ==="; timeout 20 curl -sSL "https://www.doorprofit.com/sitemap.xml" 2>&1 | grep -oE '<loc>[^<]+' | sed 's/<loc>//' | head -40
=== sitemap ===
=== sitemap ===
<!DOCTYPE html>
        <html>
        <head>
            <title>404 - Not Found</title>
            <style>
                body { font-family: -apple-system, sans-serif; background: #ffffff; color: #0f172a; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
                .container { text-align: center; }
                h1 { font-size: 72px; margin: 0; color: #4f46e5; }
                p { color: #475569; margin: 16px 0 24px; }
                a { color: #4f46e5; text-decoration: none; }
                a:hover { text-decoration: underline; }
            </style>
        </head>
        <body>
            <div class="container">
                <h1>404</h1>
                <p>This page doesn't exist.</p>
                <a href="/">Back to DoorProfit API</a>
            </div>
        </body>
        </html>
=== www sitemap ===
https://www.doorprofit.com/sitemaps/sitemap-index.xml
read /tmp/dp_docs.txt{"content":[{"text":" data-zoom No Initial zoom level (default: 13) \n data-height No Map height (CSS value, default: \"400px\") \n * Provide either data-addres…ctrl+o
{"content":[{"text":" data-zoom No Initial zoom level (default: 13) \n data-height No Map height (CSS value, default: \"400px\") \n * Provide either data-address or both data-lat and data-lng . \n How It Works \n The widget loads MapLibre GL (open-source, no Mapbox token needed) and renders a map with an OpenFreeMap Positron basemap \n The widget calls /v1/crime to validate your API key and resolve the location. If using data-address , the server geocodes it; data-lat / data-lng skips geocoding but still validates the key. \n Block-level crime heatmap data loads from MVT vector tiles as the user pans the map (no additional API calls) \n A visible \"Crime data by DoorProfit\" attribution link is required and cannot be removed \n Attribution Required: The \"Crime data by DoorProfit\" link must remain visible. Removing it will disable the map. This is the same model used by Mapbox, CARTO, and OpenStreetMap.\n Embed Metering & Domain Restrictions \n The crime map widget uses a separate \"embed views\" quota from the regular API call quota. This keeps embeds cheap so you can put them on as many pages as you like. See the pricing page for plan limits and overage rates. \n How a view is counted \n Each unique visitor (browser) counts as 1 embed view per calendar month per API key — the same visitor returning all month is still just 1 view \n Known crawlers and bots (Googlebot, Bingbot, AhrefsBot, social previewers, etc.) are automatically excluded and never count toward your quota \n Map tile requests (20–100 per page-load) are free \n Geocoded addresses are cached server-side, so the same address on any number of pages costs zero API calls after the first lookup \n A page with multiple widgets on it counts as one view , not one per widget \n Response headers \n Every response from /v1/embed/beacon includes usage headers so your integration can self-monitor: \n X-Embed-Views-Limit — your monthly cap \n X-Embed-Views-Used — views counted so far this period \n X-Embed-Views-Remaining — views still available before overage \n Restricting your key to certain domains \n By default an API key works from any site. If you want to restrict it (so a stolen key can't be used to render embeds on other domains), go to your dashboard and fill in the Allowed domains field. One hostname per line; prefix with a dot ( .example.com ) to match all subdomains. Embed requests from outside the allowlist are rejected with HTTP 403. \n Widget + noscript Fallback (Recommended) \n For maximum SEO value, combine the interactive map with a <noscript> text link fallback. JavaScript-capable browsers get the full map; crawlers and non-JS environments see a plain HTML backlink. \n HTML \n <!-- Interactive Crime Map -->\n<div id=\"doorprofit-crime-map\"\n data-address=\"500 Adams St, Tulsa, OK\"\n data-key=\"YOUR_API_KEY\"></div>\n<script src=\"https://api.doorprofit.com/v1/embed/crime-map.js\" async></script>\n<!-- Fallback for non-JS (guaranteed crawlable backlink) -->\n<noscript>\n <a href=\"https://www.doorprofit.com//crime-map/\" target=\"_blank\" rel=\"noopener\">\n View crime data for this area on DoorProfit\n </a>\n</noscript> \n Why Use Both? \n Interactive map: Gives your visitors an engaging, interactive crime heatmap they'll actually use \n noscript link: Guarantees the backlink is visible to every search engine crawler, even those that don't render JavaScript \n Quota-friendly: Widget page-loads count against the separate embed views bucket , not your API calls. Geocodes are cached, so most embed loads cost zero. \n MCP Server (Claude, ChatGPT, Cursor & AI Agents) \n DoorProfit ships a hosted MCP server (Model Context Protocol) so AI assistants and agents can query crime, neighborhood, and offender data directly in conversation — no code required. Ask Claude \"Is 500 Adams St in Tulsa safe?\" and it calls the DoorProfit API for real data. \n Server URL \n https://api.doorprofit.com/mcp \n Available Tools \n get_crime_data — safety score and nearby incident counts for an address (uses 1 API call) \n get_neighborhood_data — demographics, income, housing, rent, weather & disaster risk (uses 1 API call) \n search_offenders — registered offenders near a location or by name (uses 1 API call) \n get_api_usage — your plan, usage, and remaining quota (free) \n Connecting \n Option 1 — OAuth (easiest): add the server URL to any OAuth-capable MCP client (Claude, ChatGPT, Cursor). You'll be redirected to log in to your DoorProfit account and approve access — no key copying needed. \n Option 2 — API key: configure the server with your API key as a bearer header: \n MCP client config (JSON) \n {\n \"mcpServers\": {\n \"doorprofit\": {\n \"url\": \"https://api.doorprofit.com/mcp\",\n \"headers\": {\n \"Authorization\": \"Bearer YOUR_API_KEY\"\n }\n }\n }\n} \n Clients that only accept a URL can append the key instead: https://api.doorprofit.com/mcp?key=REDACTED \n Quotas & Billing \n MCP tool calls are metered exactly like REST calls — each data tool call uses 1 API call from your plan's monthly and daily quota. \n When the quota runs out, the AI receives the standard QUOTA_EXCEEDED error and will let you know it's time to upgrade . \n The test address 500 Main St, Dallas, TX 75202 never consumes quota — perfect for trying the integration. \n Complete Code Examples \n Full working examples showing how to use both API endpoints in popular programming languages. \n JavaScript / Node.js \n JavaScript \n // DoorProfit API - JavaScript Example\nconst API_KEY = 'YOUR_API_KEY';\nconst BASE_URL = 'https://api.doorprofit.com/v1';\n// Get crime data\nasync function getCrimeData(address) {\n const url = `${BASE_URL}/crime?address=${encodeURIComponent(address)}&key=REDACTED`;\n const response = await fetch(url);\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\n return response.json();\n}\n// Get neighborhood demographics\nasync function getNeighborhood(address) {\n const url = `${BASE_URL}/neighborhood?address=${encodeURIComponent(address)}&key=REDACTED`;\n const response = await fetch(url);\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\n return response.json();\n}\n// Get registered offenders\nasync function getOffenders(address, radius = 1) {\n const url = `${BASE_URL}/offenders?address=${encodeURIComponent(address)}&radius=${radius}&key=REDACTED`;\n const response = await fetch(url);\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\n return response.json();\n}\n// Example usage\nasync function main() {\n try {\n const address = '500 Main St, Dallas, TX 75202';\n // Get crime data\n const crimeData = await getCrimeData(address);\n console.log('Crime Score:', crimeData.crime_score);\n // Get neighborhood demographics (separate endpoint)\n const neighborhoodData = await getNeighborhood(address);\n console.log('Median Income:', neighborhoodData.neighborhood.income.median);\n console.log('Population:', neighborhoodData.neighborhood.demographics.population);\n console.log('Median Rent:', neighborhoodData.neighborhood.rent?.median);\n\n[476 more lines in file. Use offset=845 to continue.]","type":"text"}]}
Thinking…

Key findings so far:

  • DoorProfit is a real hosted REST API: https://api.doorprofit.com/v1/{crime,neighborhood,offenders,usage}
  • Auth: key query parameter. No SDK on npm/PyPI (the import DoorProfit from 'doorprofit' on the homepage is marketing pseudocode — there's no such package on npm; should double-check).
  • Free tier: 10 calls/month, 50 calls/day, 2 req/sec. Paid plans are Starter/Growth/Pro/Enterprise.
  • Important: the test address "500 Main St, Dallas, TX 75202" does not consume quota at all. That means I can build and run an example without an API key… but wait, is a key even required? It says key Required. Let me test with a dummy key against the test address — the docs say it doesn't consume quota, but does it require a valid key? Let's actually try it.
  • MCP server: hosted at https://api.doorprofit.com/mcp — this is an agent-friendly workflow that I can actually act on! It's relevant to "will it be easy for me (the agent) to manage."

Next, let me hunt down pricing info. The docs mention a "pricing page" twice, but /pricing on api is a 404. Let me check the sitemap index on www, and look at the /signup and /login pages. I'll also check the docs nav links in dp_docs.html.

Let me also actually test the API.

Let me make the calls in parallel.

timeout 25 curl -sSL "https://www.doorprofit.com/sitemaps/sitemap-index.xml" 2>&1 | grep -oE '<loc>[^<]+' | sed 's/<loc>//' | head -30
https://www.doorprofit.com/sitemaps/sitemap-static.xml
https://www.doorprofit.com/sitemaps/sitemap-static.xml
https://www.doorprofit.com/sitemaps/sitemap-states.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-al.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ak.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-az.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ar.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ca.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-co.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ct.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-de.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-fl.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ga.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-hi.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-id.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-il.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-in.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ia.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ks.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ky.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-la.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-me.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-md.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ma.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-mi.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-mn.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ms.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-mo.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-mt.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-ne.xml
https://www.doorprofit.com/sitemaps/sitemap-cities-nv.xml
echo "=== TEST ADDR, no key ==="; timeout 25 curl -sS -w "\nHTTP:%{http_code}\n" "https://api.doorprofit.com/v1/crime?address=500+Main+St+Dallas+TX+75202" 2>&1|head -c 1200; echo; echo "=== TEST ADDR, dummy key ==="; timeout 25 curl -sS -w "\nHTTP:%{http_code}\n" "https://api.doorprofit.com/v1/crime?address=500+Main+St+Dallas+TX+75202&key=REDACTED" 2>&1|head -c 1500
=== TEST ADDR, no key ===
=== TEST ADDR, no key ===
{
    "success": false,
    "error": {
        "code": "INVALID_API_KEY",
        "message": "The API key provided is invalid or has been revoked.",
        "documentation_url": "https://api.doorprofit.com/docs/#authentication"
    }
}
HTTP:401

=== TEST ADDR, dummy key ===
{
    "success": false,
    "error": {
        "code": "INVALID_API_KEY",
        "message": "The API key provided is invalid or has been revoked.",
        "documentation_url": "https://api.doorprofit.com/docs/#authentication"
    }
}
HTTP:401
Thinking…

A real API key is required. Signing up requires creating an account (email/password, probably via a web form). Can I get a key programmatically? Let me check the signup page — maybe there's an API endpoint for signups. However, creating an account requires an email address and probably email verification, which I can't do (no mailbox available). This is a potential blocker, but the instructions say "use the hosted product through its SDK or API" — so I should still build a sample and note that a real key is required.

Let me check the signup page and see if a simple programmatic signup is possible.

Also, I still need pricing info. Let me look for a pricing page in sitemap-static.xml.

timeout 25 curl -sSL "https://www.doorprofit.com/sitemaps/sitemap-static.xml" 2>&1 | grep -oE '<loc>[^<]+' | sed 's/<loc>//' | head -40
https://www.doorprofit.com/
https://www.doorprofit.com/
https://www.doorprofit.com/crime-map/
https://www.doorprofit.com/crime-statistics/
https://www.doorprofit.com/login/
https://www.doorprofit.com/signup/
https://www.doorprofit.com/deal-analyzer/
https://www.doorprofit.com/investment-deals/
https://www.doorprofit.com/terms/
https://www.doorprofit.com/privacy-policy/
https://www.doorprofit.com/forgot-password/
cd /tmp && timeout 25 curl -sSL "https://api.doorprofit.com/signup/" -o dp_signup.html -w "HTTP:%{http_code} size:%{size_download}\n"; grep -oE '<(input|form|button)[^>]*>' dp_signup.html | head -30
HTTP:200 size:33049
HTTP:200 size:33049
<button class="mobile-menu-toggle" aria-label="Open menu">
<button class="mobile-menu-close" aria-label="Close menu">
<form id="apiSignupForm">
<input type="email" id="email" name="email" required placeholder="Enter your email">
<button type="button" id="continueBtn" class="btn btn-primary btn-lg form-submit">
<button type="button" id="changeEmailBtn" class="change-email-btn">
<input type="text" id="firstName" name="first_name" placeholder="Jane">
<input type="text" id="lastName" name="last_name" placeholder="Developer">
<input type="password" id="password" name="password" placeholder="Create a password" minlength="6">
<button type="submit" class="btn btn-primary btn-lg form-submit">
Thinking…

Sign-up is possible with just email address + name + password. No CAPTCHA to be found? Let me check for hCaptcha/reCAPTCHA and find the JS submission endpoint. But even if I could sign up, an actual email address is needed for verification/linking purposes. I don't have a mailbox. This is a genuine blocker for obtaining a working key.

To check whether a key can be obtained immediately, let me quickly check the sign-up JS endpoint. No wait, let's not create an account with a fake email address—that would be creating a garbage account. Hmm. But the task is "to create a simple sample using the DoorProfit API." The reasonable deliverable is: a working sample script + a note that a key is required (free signup, instant).

Actually, let me check whether sign-up requires email verification. Let me look at the JS.

Also, I still need pricing info. There's no public pricing page! The docs mention a "pricing page" but it 404s. It might be inside the dashboard after login. Let me check the main site's homepage further and search for pricing info via login/dashboard. Should I also check DoorProfit on RapidAPI? Let me search.

Let me grep the sign-up HTML for the fetch endpoint and CAPTCHA.

cd /tmp && grep -oiE 'captcha|recaptcha|hcaptcha|turnstile' dp_signup.html | sort -u; echo "--- endpoints ---"; grep -oE "(fetch|axios|url:|action=)[^,;)]{0,120}" dp_signup.html | grep -iE "api|signup|register|/v1|auth" | head -20; echo "--- inline js signup ---"; python3 - <<'EOF'
import re
s=open('/tmp/dp_signup.html',encoding='utf-8',errors='replace').read()
for m in re.findall(r'<script(?![^>]*src)[^>]*>(.*?)</script>',s,flags=re.S):
    if 'signup' in m.lower() or 'fetch' in m:
        print(m[:3000]); print('-----')
EOF
--- endpoints ---
--- endpoints ---
fetch('/api/check_email'
fetch('/api/process_signup'
fetch('/api/resend_verification'
--- inline js signup ---

        const form = document.getElementById('apiSignupForm');
        const step1 = document.getElementById('step1');
        const step2 = document.getElementById('step2');
        const emailInput = document.getElementById('email');
        const emailDisplay = document.getElementById('emailDisplay');
        const continueBtn = document.getElementById('continueBtn');
        const changeEmailBtn = document.getElementById('changeEmailBtn');
        const errorDiv = document.getElementById('errorMessage');
        
        // Step 1: Continue button - validate email, check if exists, then show step 2
        continueBtn.addEventListener('click', async function() {
            const email = emailInput.value.trim();
            
            // Hide any previous errors
            errorDiv.classList.remove('show');
            errorDiv.textContent = '';
            
            // Validate email
            if (!email) {
                errorDiv.textContent = 'Please enter your email address';
                errorDiv.classList.add('show');
                emailInput.focus();
                return;
            }
            
            // Basic email validation
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            if (!emailRegex.test(email)) {
                errorDiv.textContent = 'Please enter a valid email address';
                errorDiv.classList.add('show');
                emailInput.focus();
                return;
            }
            
            // Check if email already exists
            continueBtn.textContent = 'Checking...';
            continueBtn.disabled = true;
            
            try {
                const formData = new FormData();
                formData.append('email', email);
                
                const response = await fetch('/api/check_email', {
                    method: 'POST',
                    body: formData
                });
                
                const result = await response.json();
                
                if (result.success && result.exists) {
                    // Email already exists - show appropriate message
                    let message = 'An account with this email already exists. ';
                    if (result.has_google) {
                        message += '<a href="https://accounts.google.com/o/oauth2/v2/auth?response_type=code&amp;access_type=online&amp;client_id=809086630649-sota4q4mnj76jcamdq640nkld757rf7a.apps.googleusercontent.com&amp;redirect_uri=https%3A%2F%2Fapi.doorprofit.com%2Fsignup%2F&amp;state&amp;scope=profile%20email&amp;approval_prompt=auto">Log in with Google</a>';
                    } else {
                        message += '<a href="/login/">Log in here</a>';
                    }
                    errorDiv.innerHTML = message;
                    errorDiv.classList.add('show');
                    continueBtn.textContent = 'Continue';
                    continueBtn.disabled = false;
                  
-----
echo "=== rapidapi search ==="; timeout 20 curl -sS "https://rapidapi.com/search/api?searchTerm=doorprofit" 2>&1 | head -c 400; echo; echo "=== google-ish: check /login for dashboard links ==="; cd /tmp && timeout 20 curl -sSL "https://api.doorprofit.com/login/" -o dp_login.html -w "HTTP:%{http_code}\n"; grep -oiE 'pricing|plan|\$[0-9]+' dp_login.html | sort -u | head -20
=== rapidapi search ===
=== rapidapi search ===
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/hub/_next/static/css/bcef81b5af7295c2.css" data-precedence="next"/><link rel="stylesheet" href="/hub/_next/static/css/f4eaed85f393ab80.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/hub/_next/static/c
=== google-ish: check /login for dashboard links ===
HTTP:200
Thinking…

Since there's a /api/resend_verification endpoint, signup requires email verification. Without a mailbox I can't receive verification emails → can't obtain an API key myself. This is a real (partial) blocker for running the example, but not a blocker for building it.

Pricing: there's no public pricing page (the docs link is broken / 404). However, from /v1/usage and the rate-limit table in the docs, I know the plan tiers: Free (10/month), Starter (300/month), Growth (600/month, $50/month per the usage example), Pro (1,200/month), Enterprise (custom). Overage is $0.10 per call (per the usage example). There's also an embed-view quota: 5,000 free views/month across all plans.

Let me double-check the npm package 'doorprofit' one more time (the hero code on the homepage suggests an SDK).

After that, build the example. Make it a small Python (and/or Node) CLI that wraps the three endpoints + usage, reads DOORPROFIT_API_KEY from env vars, and has caching + graceful error handling. Keep it light — no local server, no long-running commands.

Let me verify that the npm package really doesn't exist, and check whether there's an official SDK on GitHub (the IlijaAngelov/Doorprofit repo looked unrelated).

Let me do these checks and then write the code.

echo "=== npm registry direct ==="; timeout 20 curl -sS -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/doorprofit; echo "=== pypi direct ==="; timeout 20 curl -sS -o /dev/null -w "%{http_code}\n" https://pypi.org/project/doorprofit/; echo "=== github repo check ==="; timeout 20 curl -sS "https://api.github.com/search/repositories?q=doorprofit" | python3 -c "import sys,json;d=json.load(sys.stdin);[print(i['full_name'],'|',i.get('description'),'|stars',i['stargazers_count'],'|',(i.get('language') or '-')) for i in d['items']]"
=== npm registry direct ===
=== npm registry direct ===
404
=== pypi direct ===
200
=== github repo check ===
IlijaAngelov/Doorprofit | None |stars 0 | -
shawndoor/doorprofit-mcp | None |stars 0 | JavaScript
shawndoor/doorprofit-claude-skills | roduces a structured due-diligence report for any US property address using the DoorProfit connector: crime safety score, recent incidents, demographics, income, home values, rent, disaster risk, and registered-offender search. |stars 0 | -
cd /tmp && grep -oE 'href="[^"]*"' dp_docs.html | sed 's/href="//;s/"$//' | sort -u | grep -viE 'fonts|\.png|\.webp|\.ico|manifest|#' | head -30; echo "=== docs nav anchors for pricing ==="; grep -oiE '[^>]{0,60}pricing[^<]{0,60}' dp_docs.html | head -10
/
/
/billing
/billing/
/dashboard
/docs/
/login/
/signup/
https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css
https://www.doorprofit.com/
https://www.doorprofit.com//crime-map/
mailto:support@doorprofit.com
=== docs nav anchors for pricing ===
            <!-- Embed: Views &amp; Pricing -->
pricing page
pricing page
 for plan details and pricing.
Thinking…

Interesting! pypi.org/project/doorprofit/ returned 200! But pip index versions doorprofit said there's no matching distribution. Let me check — maybe it's a page that exists but has no files, or maybe pip is using a different index. Let me fetch it.

Also, the docs link to /billing (probably behind a login).

cd /tmp && timeout 25 curl -sSL "https://pypi.org/project/doorprofit/" -o dp_pypi.html -w "HTTP:%{http_code}\n"; python3 - <<'EOF'
import re,html
s=open('/tmp/dp_pypi.html',encoding='utf-8',errors='replace').read()
s2=re.sub(r'<script.*?</script>','',s,flags=re.S|re.I)
s2=re.sub(r'<style.*?</style>','',s2,flags=re.S|re.I)
s2=re.sub(r'<[^>]+>',' ',s2); s2=html.unescape(s2); s2=re.sub(r'[ \t]+',' ',s2); s2=re.sub(r'\n\s*\n+','\n',s2)
print(s2.strip()[:4000])
EOF
HTTP:200
HTTP:200
Client Challenge 
 JavaScript is disabled in your browser. 
 Please enable JavaScript to proceed. 
 A required part of this site couldn’t load. This may be due to a browser
 extension, network issues, or browser settings. Please check your
 connection, disable any ad blockers, or try using a different browser.
timeout 20 curl -sS "https://pypi.org/pypi/doorprofit/json" | head -c 1500; echo; echo "=== pip retry verbose ==="; timeout 40 pip download doorprofit --no-deps -d /tmp/dp2 2>&1 | tail -8
{"message": "Not Found"}
{"message": "Not Found"}
=== pip retry verbose ===
ERROR: Could not find a version that satisfies the requirement doorprofit (from versions: none)
ERROR: No matching distribution found for doorprofit
Thinking…

That 200 was a JS challenge page, not an actual project. Confirmed: there is no official doorprofit SDK on PyPI or npm. The import DoorProfit from 'doorprofit' snippet on the homepage is aspirational/pseudocode. Therefore, REST (or the hosted MCP server) is the only means.

Let me quickly check the two GitHub repos — shawndoor/doorprofit-mcp and doorprofit-claude-skills. "shawndoor" might be the owner of DoorProfit! Let's check.

After that, I'll build an example.

for r in shawndoor/doorprofit-mcp shawndoor/doorprofit-claude-skills; do echo "=== $r ==="; timeout 20 curl -sS "https://api.github.com/repos/$r" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('full_name'),'|',d.get('description'),'| pushed:',d.get('pushed_at'),'| default:',d.get('default_branch'))"; timeout 20 curl -sS "https://api.github.com/repos/$r/git/trees/HEAD?recursive=1" | python3 -c "import sys,json;d=json.load(sys.stdin);[print(' ',t['path']) for t in d.get('tree',[])[:25]]" 2>/dev/null; done; echo "=== user shawndoor ==="; timeout 20 curl -sS "https://api.github.com/users/shawndoor" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('name'),d.get('blog'),d.get('bio'))"
=== shawndoor/doorprofit-mcp ===
=== shawndoor/doorprofit-mcp ===
shawndoor/doorprofit-mcp | None | pushed: 2026-08-03T18:10:00Z | default: main
  .github
  .github/workflows
  .github/workflows/ci.yml
  .gitignore
  Dockerfile
  LICENSE
  README.md
  glama.json
  package-lock.json
  package.json
  scripts
  scripts/smoke-test.js
  scripts/sync-tools.js
  server.json
  src
  src/index.js
  src/remote.js
  src/tools.json
=== shawndoor/doorprofit-claude-skills ===
shawndoor/doorprofit-claude-skills | roduces a structured due-diligence report for any US property address using the DoorProfit connector: crime safety score, recent incidents, demographics, income, home values, rent, disaster risk, and registered-offender search. | pushed: 2026-06-11T03:11:19Z | default: main
  LICENSE
  README.md
  property-due-diligence
  property-due-diligence/SKILL.md
=== user shawndoor ===
None  None
cd /tmp && timeout 25 curl -sS -X POST "https://api.doorprofit.com/mcp" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}' -w "\nHTTP:%{http_code}\n" 2>&1 | head -c 1200
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"doorprofit","title":"DoorProfit Crime & Neigh
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"doorprofit","title":"DoorProfit Crime & Neighborhood Data","version":"1.0.0"},"instructions":"DoorProfit provides US crime, safety, neighborhood, and registered-offender data. Authentication: a DoorProfit API key is required for tool calls, sent as \"Authorization: Bearer <key>\" (get a free key at https://api.doorprofit.com/signup). Quotas: every data tool call consumes 1 API call from the key's monthly/daily plan quota; get_api_usage is free and reports remaining quota. If a call fails with QUOTA_EXCEEDED or DAILY_LIMIT_EXCEEDED, tell the user their DoorProfit quota is exhausted and they can upgrade at https://api.doorprofit.com/billing. The address \"500 Main St, Dallas, TX 75202\" is a free test address that never consumes quota. Docs: https://api.doorprofit.com/docs/"}}
HTTP:200
Thinking…

Excellent! The hosted MCP server works and can respond even without authentication (initialize). Tool calls require a key. This is a developer workflow that I can actually handle — I can communicate with it directly.

Let me try listing tools via MCP without a key (tools/list might work even without authentication).

Next, build an example. What to create:

  1. A small Python client (doorprofit_example/) that wraps REST: crime, neighborhood, offenders, usage. Read the key from an environment variable, add caching, do graceful error handling, and a CLI that generates a property due diligence report.
  2. MCP client configuration + a script that communicates with the hosted MCP server.
  3. A README documenting pricing and setup.

First, let me check tools/list.

cd /tmp && timeout 25 curl -sS -X POST "https://api.doorprofit.com/mcp" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' 2>&1 | python3 -c "
import sys,json
raw=sys.stdin.read()
raw=raw.split('data: ')[-1] if raw.startswith('event') else raw
try:
    d=json.loads(raw)
    for t in d['result']['tools']:
        print('*',t['name'],'::',(t.get('description') or '')[:90])
        print('   params:',list((t.get('inputSchema') or {}).get('properties',{}).keys()), 'required:',(t.get('inputSchema') or {}).get('required'))
except Exception as e:
    print('ERR',e); print(raw[:800])
"
* get_crime_data :: Crime safety grade and nearby incident counts for one US address or coordinate pair. Retur
* get_crime_data :: Crime safety grade and nearby incident counts for one US address or coordinate pair. Retur
   params: ['address', 'lat', 'lng'] required: None
* get_neighborhood_data :: Profile of the neighborhood (or census block group) containing one US address or coordinat
   params: ['address', 'lat', 'lng'] required: None
* search_offenders :: Search US registered offenders in one of two modes. Location mode: pass address or lat+lng
   params: ['address', 'lat', 'lng', 'radius', 'first_name', 'last_name', 'dob', 'city', 'state', 'zipcode', 'page', 'limit'] required: None
* get_api_usage :: Report the plan, monthly and daily call usage, remaining quota, reset times, rate limit an
   params: [] required: None
cd /tmp && timeout 25 curl -sS -X POST "https://api.doorprofit.com/mcp" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_crime_data","arguments":{"address":"500 Main St, Dallas, TX 75202"}}}' -w "\nHTTP:%{http_code}\n" 2>&1 | head -c 900
{"jsonrpc":"2.0","id":null,"error":{"code":-32001,"message":"Unauthorized: authenticate via OAuth, or supply a DoorProfit API key as \"Authorization: Bearer <ke
{"jsonrpc":"2.0","id":null,"error":{"code":-32001,"message":"Unauthorized: authenticate via OAuth, or supply a DoorProfit API key as \"Authorization: Bearer <key>\". Get a free key at https://api.doorprofit.com/signup"}}
HTTP:401
Thinking…

Confirmed: everything is gated behind an API key that requires account registration + email verification. Without a mailbox, I can't obtain a key.

So: the product actually has a developer workflow that I can engage with (hosted REST + hosted MCP). The only obstacle to live execution is the API key (email verification required). What needs to be done:

  1. Clearly state the pricing structure.
  2. Build an example (REST client + MCP configuration + CLI report) — a dry-run/offline mode so that the structure can be verified without a key, with the ability to run live when DOORPROFIT_API_KEY is provided.
  3. Tell them whether it's easy for me to manage.
  4. Clearly state the obstacle: can't obtain an API key myself (email verification required), so live calls are blocked until they paste in a key.

Let's build it. Keep it lightweight: pure standard library Python (no dependencies) + Node variant + MCP configuration. Keep it to a minimal but actually usable form.

Let me check the repo's .gitignore and Python version.

cd /sandbox/repo && cat .gitignore; echo "--- python ---"; python3 -V; echo "--- node ---"; node -v
# Keep generated and vendored files out of source diffs.
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/

# Rust
target/

# Go / PHP / Ruby vendored deps
vendor/
.bundle/

# Java / Kotlin / Scala
.gradle/

# .NET
bin/
obj/

# Elixir
_build/
deps/

# Dart / Flutter
.dart_tool/
--- python ---
Python 3.12.3
--- node ---
v22.23.2
Thinking…

Alright, let's create a sample. Structure:

/sandbox/repo/doorprofit-example/
  README.md
  doorprofit.py          # Small REST client using only the standard library (Python 3.9+)
  report.py              # CLI: property due diligence report (crime + district + offender + usage)
  report.mjs             # Node version (fetch-based, no dependencies)
  mcp-config.json        # MCP config for hosted MCP server
  crime-map-widget.html  # Example embeddable widget
  .env.example

Keep it concise. Let's write doorprofit.py properly: errors, retry on 429 (with backoff), respect rate limits, disk cache to save quota (docs recommend caching), headers for quota tracking.

Let's write the files.

Everything checks out. Now let me build the example.

File edits

1 file

Completed
Writtendoorprofit-example/doorprofit.pyView changes

/sandbox/repo/doorprofit-example/doorprofit.py

Written content · previous contents were not recorded

"""Minimal DoorProfit API client.
DoorProfit is a hosted REST API (no local stack, no official SDK on PyPI/npm),
so this is a thin stdlib-only wrapper over:
GET https://api.doorprofit.com/v1/crime
GET https://api.doorprofit.com/v1/neighborhood
GET https://api.doorprofit.com/v1/offenders
GET https://api.doorprofit.com/v1/usage (free, never counts against quota)
Docs: https://api.doorprofit.com/docs/
Design notes (driven by how the product meters you):
* Every data call costs 1 credit from a small monthly quota (10/month free),
so responses are cached on disk by default. Re-running is free.
* The address "500 Main St, Dallas, TX 75202" is a documented free test
address that never consumes quota -- used as the default demo target.
* 429s are retried with backoff; QUOTA_EXCEEDED is raised, not retried.
Requires Python 3.9+. No third-party dependencies.
"""
from __future__ import annotations
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any, Dict, Optional
BASE_URL = "https://api.doorprofit.com/v1"
#: Documented free test address -- calls against it do not consume quota.
FREE_TEST_ADDRESS = "500 Main St, Dallas, TX 75202"
USER_AGENT = "doorprofit-example/1.0 (+stdlib urllib)"
class DoorProfitError(Exception):
"""Base error. Carries the structured error body the API returns."""
def __init__(self, message: str, *, code: Optional[str] = None,
status: Optional[int] = None, doc_url: Optional[str] = None):
super().__init__(message)
self.code = code
self.status = status
self.doc_url = doc_url
@property
def retryable(self) -> bool:
return self.code == "RATE_LIMIT_EXCEEDED" or self.status == 429
@property
def out_of_quota(self) -> bool:
return self.code in {"QUOTA_EXCEEDED", "DAILY_LIMIT_EXCEEDED"}
class DoorProfit:
"""Tiny client for the three DoorProfit data endpoints plus /usage."""
def __init__(
self,
api_key: Optional[str] = None,
*,
cache_dir: Optional[os.PathLike | str] = ".doorprofit-cache",
max_retries: int = 3,
timeout: float = 20.0,
) -> None:
self.api_key = api_key or os.environ.get("DOORPROFIT_API_KEY", "")
if not self.api_key:
raise DoorProfitError(
"No API key. Set DOORPROFIT_API_KEY or pass api_key=... "
"(free key: https://api.doorprofit.com/signup/)"
)
self.cache_dir = Path(cache_dir) if cache_dir else None
self.max_retries = max_retries
self.timeout = timeout
self.last_usage_headers: Dict[str, str] = {}
# ------------------------------------------------------------------ HTTP
def _request(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
query = {k: v for k, v in params.items() if v is not None}
query["key"] = self.api_key
url = f"{BASE_URL}/{path.lstrip('/')}?{urllib.parse.urlencode(query)}"
attempt = 0
while True:
attempt += 1
req = urllib.request.Request(
url, headers={"Accept": "application/json", "User-Agent": USER_AGENT}
)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
body = resp.read().decode("utf-8")
self.last_usage_headers = {
k.lower(): v for k, v in resp.headers.items()
if k.lower().startswith(("x-",))
}
return json.loads(body)
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
err = _parse_error(raw, exc.code)
if err.retryable and attempt <= self.max_retries:
time.sleep(min(2 ** attempt, 8)) # back off, then retry
continue
raise err from None
except urllib.error.URLError as exc:
if attempt <= self.max_retries:
time.sleep(min(2 ** attempt, 8))
continue
raise DoorProfitError(f"Network error: {exc.reason}") from None
# ----------------------------------------------------------------- cache
def _cache_key(self, path: str, params: Dict[str, Any]) -> Optional[Path]:
if self.cache_dir is None:
return None
clean = {k: v for k, v in sorted(params.items()) if v is not None}
digest = urllib.parse.urlencode(clean)
self.cache_dir.mkdir(parents=True, exist_ok=True)
return self.cache_dir / f"{path.strip('/')}--{urllib.parse.quote(digest, safe='')}.json"
def _get(self, path: str, params: Dict[str, Any], *, use_cache: bool = True) -> Dict[str, Any]:
slot = self._cache_key(path, params)
if use_cache and slot and slot.exists():
return json.loads(slot.read_text())
data = self._request(path, params)
if slot and data.get("success"):
slot.write_text(json.dumps(data, indent=2))
return data
# ------------------------------------------------------------- endpoints
@staticmethod
def _location(address: Optional[str], lat: Optional[float],
lng: Optional[float]) -> Dict[str, Any]:
if address:
return {"address": address}
if lat is not None and lng is not None:
return {"lat": lat, "lng": lng}
raise DoorProfitError("Provide either address= or both lat= and lng=")
def crime(self, address: Optional[str] = None, *, lat: Optional[float] = None,
lng: Optional[float] = None, use_cache: bool = True) -> Dict[str, Any]:
"""Safety grade (A+..F), numeric score, and nearby incident counts."""
return self._get("/crime", self._location(address, lat, lng), use_cache=use_cache)
def neighborhood(self, address: Optional[str] = None, *, lat: Optional[float] = None,
lng: Optional[float] = None, use_cache: bool = True) -> Dict[str, Any]:
"""Demographics, income, housing, education, cost of living, rent, weather."""
return self._get("/neighborhood", self._location(address, lat, lng), use_cache=use_cache)
def offenders(self, address: Optional[str] = None, *, lat: Optional[float] = None,
lng: Optional[float] = None, radius: float = 1.0,
first_name: Optional[str] = None, last_name: Optional[str] = None,
city: Optional[str] = None, state: Optional[str] = None,
zipcode: Optional[str] = None, page: int = 1, limit: int = 50,
use_cache: bool = True) -> Dict[str, Any]:
"""Registered offenders near a location, or by name. Each page = 1 credit."""
params: Dict[str, Any] = {"radius": radius, "page": page, "limit": limit}
if address or (lat is not None and lng is not None):
params.update(self._location(address, lat, lng))
for key, val in (("first_name", first_name), ("last_name", last_name),
("city", city), ("state", state), ("zipcode", zipcode)):
if val:
params[key] = val
return self._get("/offenders", params, use_cache=use_cache)
def usage(self) -> Dict[str, Any]:
"""Plan, quota, and remaining calls. FREE -- never cached, never metered."""
return self._request("/usage", {})
# ------------------------------------------------------------ niceties
def quota_summary(self) -> str:
"""One-line human-readable quota readout from /v1/usage."""
data = self.usage().get("data", {})
plan = data.get("plan", {})
monthly = data.get("monthly", {})
daily = data.get("daily", {})
overage = data.get("overage", {})
bits = [
f"plan={plan.get('name', '?')}",
f"${plan.get('price_monthly', 0)}/mo",
f"month={monthly.get('used', '?')}/{monthly.get('limit', '?')}",
f"today={daily.get('used', '?')}/{daily.get('limit', '?')}",
f"rate={data.get('rate_limit', {}).get('requests_per_second', '?')} req/s",
]
if overage.get("enabled"):
bits.append(f"overage=${overage.get('rate_per_call', '?')}/call")
return " ".join(bits)
def _parse_error(raw: str, status: int) -> DoorProfitError:
try:
body = json.loads(raw)
err = body.get("error", {}) or {}
return DoorProfitError(
err.get("message") or raw[:200] or f"HTTP {status}",
code=err.get("code"),
status=status,
doc_url=err.get("documentation_url"),
)
except (json.JSONDecodeError, AttributeError):
return DoorProfitError(raw[:200] or f"HTTP {status}", status=status)
Thinking…

I need to investigate this further. Let me confirm the details.

Next is the report CLI. This calls crime, neighborhood, and offenders — that's 3 credits. The free tier is 10 per month. I'll warn about the cost and add a --usage-only mode and a --dry-run mode that runs without a key (displaying the request contents). Alright, let's create it.

File edits

1 file

Completed
Writtendoorprofit-example/report.pyView changes

/sandbox/repo/doorprofit-example/report.py

Written content · previous contents were not recorded

#!/usr/bin/env python3
"""Property due-diligence report built on the DoorProfit API.
Usage:
export DOORPROFIT_API_KEY=your_key
python3 report.py "500 Adams St, Tulsa, OK"
python3 report.py --usage-only # free, never touches quota
python3 report.py --dry-run "..." # show requests, spend 0 credits
python3 report.py --json "..." # machine-readable output
Cost: one full report = 3 API credits (crime + neighborhood + offenders).
The free plan is 10 credits/month, so results are cached on disk and re-runs
against the same address are free. The documented test address
"500 Main St, Dallas, TX 75202" never consumes quota.
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.parse
from doorprofit import (BASE_URL, FREE_TEST_ADDRESS, DoorProfit, DoorProfitError)
GRADE_SCALE = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-",
"D+", "D", "D-", "F"]
def bar(value: float, width: int = 20) -> str:
"""Render a 0..1 score as a little bar; lower is safer."""
filled = max(0, min(width, round(value * width)))
return "[" + "#" * filled + "." * (width - filled) + "]"
def fmt_money(n) -> str:
return f"${n:,.0f}" if isinstance(n, (int, float)) else "n/a"
def print_report(crime: dict, hood: dict, offenders: dict, address: str) -> None:
loc = crime.get("location") or hood.get("location") or {}
where = loc.get("address") or address
print("=" * 68)
print(f" DoorProfit due diligence: {where}")
if loc.get("lat") is not None:
print(f" {loc.get('lat')}, {loc.get('lng')} | "
f"{loc.get('city', '')}, {loc.get('state', '')} "
f"{loc.get('zipcode', '')}".rstrip())
print("=" * 68)
# ---- Crime -----------------------------------------------------------
grade = crime.get("crime_score", "?")
numeric = crime.get("crime_numeric")
print("\nSAFETY")
idx = GRADE_SCALE.index(grade) if grade in GRADE_SCALE else None
rank = f"{idx + 1}/{len(GRADE_SCALE)} (A+ safest)" if idx is not None else ""
print(f" Grade {grade} {rank}")
if numeric is not None:
print(f" Crime score {numeric:.2f} / 1.00 {bar(numeric)} (lower = safer)")
print(f" Source {crime.get('crime_score_source', 'n/a')}")
print(f" Description {crime.get('crime_description', 'n/a')}")
incidents = crime.get("incidents")
if incidents:
print(f"\n INCIDENTS within {incidents.get('radius_feet', '?')} ft, "
f"{incidents.get('date_from', '?')} -> {incidents.get('date_to', '?')}")
print(f" Total {incidents.get('count', 0)}")
by_type = incidents.get("by_type") or {}
top = sorted(by_type.items(), key=lambda kv: kv[1], reverse=True)
for name, count in top:
if count:
print(f" {name.replace('_', ' '):<18}{count:>5}")
zeroed = [n for n, c in by_type.items() if not c]
if zeroed:
print(f" none reported: {', '.join(z.replace('_', ' ') for z in zeroed)}")
else:
print("\n INCIDENTS not published for this city (incidents: null)")
# ---- Neighborhood ----------------------------------------------------
nb = hood.get("neighborhood") or {}
if nb:
demo = nb.get("demographics") or {}
income = nb.get("income") or {}
housing = nb.get("housing") or {}
rent = nb.get("rent") or {}
edu = nb.get("education") or {}
col = nb.get("cost_of_living") or {}
risk = ((nb.get("weather") or {}).get("natural_disaster_risk") or {})
print("\nNEIGHBORHOOD")
print(f" Name {nb.get('name', 'n/a')} ({nb.get('type', '?')})")
print(f" Population {demo.get('population', 'n/a'):,} "
f"| density {demo.get('population_density', 'n/a')}/sq mi "
f"| median age {demo.get('median_age', 'n/a')}"
if isinstance(demo.get("population"), (int, float)) else
f" Population {demo.get('population', 'n/a')}")
print(f" Median income {fmt_money(income.get('median'))}")
print(f" Median home {fmt_money(housing.get('median_home_value'))}")
print(f" Median rent {fmt_money(rent.get('median'))}/mo", end="")
by_bed = rent.get("by_bedroom") or {}
if by_bed:
print(" (" + ", ".join(f"{k} {fmt_money(v)}" for k, v in by_bed.items()) + ")")
else:
print()
print(f" Bachelor's+ {edu.get('bachelors_pct', 'n/a')}% "
f"| grad {edu.get('graduate_pct', 'n/a')}%")
print(f" Cost of living {col.get('overall_index', 'n/a')} (100 = US avg)")
if risk:
print(f" Disaster risk overall {risk.get('overall_index', 'n/a')}"
f" tornado {risk.get('tornado', 'n/a')}"
f" hurricane {risk.get('hurricane', 'n/a')}"
f" earthquake {risk.get('earthquake', 'n/a')}")
# ---- Offenders -------------------------------------------------------
print("\nREGISTERED OFFENDERS")
if offenders.get("success") is False:
print(f" {(offenders.get('error') or {}).get('message', 'unavailable')}")
else:
count = offenders.get("offenders_count")
radius = offenders.get("radius_miles") or offenders.get("radius") or 1
print(f" Within {radius} mi {count if count is not None else 'n/a'} record(s)")
for o in (offenders.get("offenders") or [])[:5]:
name = o.get("name") or " ".join(
filter(None, [o.get("first_name"), o.get("last_name")])) or "?"
dist = o.get("distance_miles", o.get("distance"))
line = f" - {name}"
if o.get("offense"):
line += f" | {o['offense']}"
if dist is not None:
line += f" | {dist} mi"
print(line)
remaining = (count or 0) - len(offenders.get("offenders") or [])
if remaining > 0:
print(f" ... and {remaining} more (paginate with page=/limit=)")
print("\n" + "=" * 68)
print(" Data: DoorProfit (https://www.doorprofit.com). "
"Verify before making decisions.")
print("=" * 68)
def dry_run(address: str, radius: float) -> None:
"""Print exactly what would be sent, and what it would cost. Spends nothing."""
key = "YOUR_API_KEY"
calls = [
("crime", {"address": address}),
("neighborhood", {"address": address}),
("offenders", {"address": address, "radius": radius, "page": 1, "limit": 50}),
]
free = address.strip().rstrip(".").lower() == FREE_TEST_ADDRESS.lower()
print("# DRY RUN -- no requests sent, 0 credits used\n")
print(f"# Free quota check: GET {BASE_URL}/usage?key=REDACTED")
for name, params in calls:
q = dict(params, key=key)
print(f"\n# {name} (1 credit)")
print(f'curl -G "{BASE_URL}/{name}" \\')
for k, v in q.items():
print(f' --data-urlencode "{k}={v}" \\')
print(' -H "Accept: application/json"')
print("\n# Total cost: 3 credits"
+ (" (0 -- this is the free test address)" if free else "")
+ "\n# Free plan = 10 credits/month, 50/day, 2 req/sec.")
def main() -> int:
ap = argparse.ArgumentParser(description="DoorProfit property due-diligence report")
ap.add_argument("address", nargs="?", default=FREE_TEST_ADDRESS,
help=f"US street address (default: free test address "
f"'{FREE_TEST_ADDRESS}')")
ap.add_argument("--radius", type=float, default=1.0,
help="offender search radius in miles (max 5, default 1)")
ap.add_argument("--lat", type=float, help="query by latitude instead of address")
ap.add_argument("--lng", type=float, help="query by longitude instead of address")
ap.add_argument("--usage-only", action="store_true",
help="print plan/quota only (free endpoint)")
ap.add_argument("--dry-run", action="store_true",
help="print the curl commands and credit cost without calling")
ap.add_argument("--no-cache", action="store_true", help="bypass the on-disk cache")
ap.add_argument("--json", action="store_true", help="emit raw JSON instead of a report")
args = ap.parse_args()
if args.dry_run:
dry_run(args.address, args.radius)
return 0
try:
api = DoorProfit()
except DoorProfitError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
try:
if args.usage_only:
print(api.quota_summary())
return 0
cache = not args.no_cache
if args.lat is not None and args.lng is not None:
kw = {"lat": args.lat, "lng": args.lng}
else:
kw = {"address": args.address}
crime = api.crime(use_cache=cache, **kw)
if not crime.get("success"):
print(f"error: crime lookup failed: {json.dumps(crime)[:300]}", file=sys.stderr)
return 1
hood = api.neighborhood(use_cache=cache, **kw)
# Offender data is optional garnish -- don't fail the whole report on it.
try:
offenders = api.offenders(radius=args.radius, use_cache=cache, **kw)
except DoorProfitError as exc:
offenders = {"success": False, "error": {"message": str(exc)}}
if args.json:
print(json.dumps({"crime": crime, "neighborhood": hood,
"offenders": offenders}, indent=2))
else:
print_report(crime, hood, offenders, args.address)
print(f"\nquota: {api.quota_summary()}")
return 0
except DoorProfitError as exc:
detail = f" [{exc.code}]" if exc.code else ""
print(f"error: {exc}{detail}", file=sys.stderr)
if exc.out_of_quota:
print("hint: quota exhausted -- upgrade at "
"https://api.doorprofit.com/billing", file=sys.stderr)
if exc.doc_url:
print(f"docs: {exc.doc_url}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
Thinking…

Next up is the Node version, MCP config, widget HTML, .env.example, and the README. Let's keep the Node one compact.

Wait, if we're going to "keep it lightweight"—maybe we can omit the Node version? But the Node version has value, because the hero snippet on the homepage suggested a JS SDK (which doesn't actually exist). Let's include a small dependency-free Node script that demonstrates the actual (fetch-based) integration, and note that the doorprofit npm package doesn't exist. It's small, so let's keep it.

Alright, let's write these files.

File edits

1 file

Completed
Writtendoorprofit-example/report.mjsView changes

/sandbox/repo/doorprofit-example/report.mjs

Written content · previous contents were not recorded

#!/usr/bin/env node
/**
* DoorProfit API -- dependency-free Node example (Node 18+ for global fetch).
*
* NOTE: the `import DoorProfit from 'doorprofit'` snippet on the DoorProfit
* homepage is illustrative -- no `doorprofit` package exists on npm or PyPI.
* The real integration is plain HTTPS GETs, like this.
*
* DOORPROFIT_API_KEY=your_key node report.mjs "500 Adams St, Tulsa, OK"
* node report.mjs --usage-only # free, never touches quota
*
* Each data endpoint costs 1 credit; the free plan is 10 credits/month.
* The address "500 Main St, Dallas, TX 75202" is a free test address.
*/
const BASE_URL = 'https://api.doorprofit.com/v1';
const FREE_TEST_ADDRESS = '500 Main St, Dallas, TX 75202';
const API_KEY = process.env.DOORPROFIT_API_KEY;
class DoorProfitError extends Error {
constructor(message, { code, status } = {}) {
super(message);
this.code = code;
this.status = status;
}
}
async function get(path, params, retries = 3) {
const url = new URL(`${BASE_URL}/${path}`);
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null) url.searchParams.set(k, v);
}
url.searchParams.set('key', API_KEY);
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, {
headers: { Accept: 'application/json', 'User-Agent': 'doorprofit-example/1.0' },
});
const body = await res.json().catch(() => ({}));
if (res.ok) return { data: body, headers: res.headers };
const err = body.error || {};
// 429 = slow down and retry; quota exhaustion is terminal.
if (res.status === 429 && err.code === 'RATE_LIMIT_EXCEEDED' && attempt <= retries) {
await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 8000)));
continue;
}
throw new DoorProfitError(err.message || `HTTP ${res.status}`, {
code: err.code,
status: res.status,
});
}
}
const location = (address, lat, lng) =>
address ? { address } : lat != null && lng != null ? { lat, lng } : (() => {
throw new DoorProfitError('Provide either address or lat+lng');
})();
const crime = (loc) => get('crime', loc);
const neighborhood = (loc) => get('neighborhood', loc);
const offenders = (loc, radius = 1) => get('offenders', { ...loc, radius, limit: 50 });
const usage = () => get('usage', {});
async function main() {
const argv = process.argv.slice(2);
const usageOnly = argv.includes('--usage-only');
const address = argv.find((a) => !a.startsWith('--')) || FREE_TEST_ADDRESS;
if (!API_KEY) {
console.error('error: DOORPROFIT_API_KEY is not set.');
console.error('Get a free key at https://api.doorprofit.com/signup/');
process.exit(2);
}
try {
if (usageOnly) {
const { data } = await usage();
const d = data.data || {};
console.log(
`plan=${d.plan?.name} $${d.plan?.price_monthly}/mo ` +
`month=${d.monthly?.used}/${d.monthly?.limit} ` +
`today=${d.daily?.used}/${d.daily?.limit} ` +
`rate=${d.rate_limit?.requests_per_second} req/s`,
);
return;
}
const loc = location(address);
// Sequential on purpose: free plan allows 2 req/sec.
const { data: c } = await crime(loc);
const { data: n } = await neighborhood(loc);
let o = { success: false, error: { message: 'skipped' } };
try {
o = (await offenders(loc)).data;
} catch (e) {
o = { success: false, error: { message: e.message } };
}
const where = c.location?.address || address;
console.log(`\n=== ${where} ===`);
console.log(`Safety grade : ${c.crime_score} (score ${c.crime_numeric}, lower = safer)`);
console.log(`Description : ${c.crime_description}`);
console.log(`Score source : ${c.crime_score_source}`);
if (c.incidents) {
const top = Object.entries(c.incidents.by_type || {})
.filter(([, v]) => v > 0)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
console.log(
`Incidents : ${c.incidents.count} within ${c.incidents.radius_feet} ft ` +
`(${c.incidents.date_from} -> ${c.incidents.date_to})`,
);
if (top.length) console.log(` top types : ${top.map(([k, v]) => `${k}=${v}`).join(', ')}`);
} else {
console.log('Incidents : not published for this city');
}
const nb = n.neighborhood || {};
console.log(`Neighborhood : ${nb.name || 'n/a'}`);
console.log(`Population : ${nb.demographics?.population ?? 'n/a'}`);
console.log(`Median income : $${nb.income?.median ?? 'n/a'}`);
console.log(`Median home : $${nb.housing?.median_home_value ?? 'n/a'}`);
console.log(`Median rent : $${nb.rent?.median ?? 'n/a'}/mo`);
console.log(`Cost of living : ${nb.cost_of_living?.overall_index ?? 'n/a'} (100 = US avg)`);
console.log(
`Disaster risk : ${nb.weather?.natural_disaster_risk?.overall_index ?? 'n/a'} (100 = US avg)`,
);
console.log(`Offenders : ${o.offenders_count ?? 'n/a'} within ${o.radius_miles ?? 1} mi`);
for (const off of (o.offenders || []).slice(0, 3)) {
console.log(` - ${off.name || '?'} | ${off.offense || '?'} | ${off.distance_miles ?? '?'} mi`);
}
const { data: u } = await usage();
const m = u.data?.monthly || {};
console.log(`\nquota : ${m.used}/${m.limit} used this month (${m.remaining} left)`);
} catch (err) {
console.error(`error: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
if (['QUOTA_EXCEEDED', 'DAILY_LIMIT_EXCEEDED'].includes(err.code)) {
console.error('hint: quota exhausted -- upgrade at https://api.doorprofit.com/billing');
}
process.exit(1);
}
}
main();
Thinking…

From here on: MCP configuration, mcp_probe.py (a small script that communicates with the hosted MCP server — this is the part that's actually executable as an agent), widget HTML, .env.example, README.

I'll write a small MCP client script that initializes, retrieves the tool list, and optionally calls get_api_usage. This is genuinely useful and lightweight.

File edits

1 file

Completed
Writtendoorprofit-example/mcp_probe.pyView changes

/sandbox/repo/doorprofit-example/mcp_probe.py

Written content · previous contents were not recorded

#!/usr/bin/env python3
"""Talk to the *hosted* DoorProfit MCP server -- no local process to run.
DoorProfit runs its MCP server for you at https://api.doorprofit.com/mcp,
so an AI agent (Claude, ChatGPT, Cursor) can query it directly with no code.
This script is the same thing over plain HTTP, useful for smoke-testing a key
before wiring it into a client.
python3 mcp_probe.py --list # no key needed
DOORPROFIT_API_KEY=key python3 mcp_probe.py --usage # free tool call
DOORPROFIT_API_KEY=key python3 mcp_probe.py --crime "500 Adams St, Tulsa, OK"
Auth: `Authorization: Bearer <key>` (or OAuth in a real MCP client).
Metering: get_crime_data / get_neighborhood_data / search_offenders each cost
1 API credit, exactly like the REST endpoints. get_api_usage is free.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
MCP_URL = "https://api.doorprofit.com/mcp"
def rpc(method: str, params: dict | None = None, api_key: str | None = None,
req_id: int = 1) -> dict:
payload = {
"jsonrpc": "2.0",
"id": req_id,
"method": method,
"params": params or {},
}
headers = {
"Content-Type": "application/json",
# The server may answer with either JSON or an SSE stream.
"Accept": "application/json, text/event-stream",
"User-Agent": "doorprofit-mcp-probe/1.0",
}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(
MCP_URL, data=json.dumps(payload).encode(), headers=headers, method="POST"
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
try:
body = json.loads(raw)
msg = body.get("error", {}).get("message", raw[:200])
except json.JSONDecodeError:
msg = raw[:200]
return {"error": {"message": f"HTTP {exc.code}: {msg}"}}
# Unwrap an SSE frame ("event: message\ndata: {...}") if we got one.
if raw.lstrip().startswith("event:") or "\ndata:" in raw:
raw = next((l[5:].strip() for l in raw.splitlines()
if l.startswith("data:")), raw)
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"error": {"message": f"Unparseable response: {raw[:200]}"}}
def text_of(result: dict) -> str:
"""Pull the human-readable text out of an MCP tools/call result."""
out = []
for item in (result.get("result") or {}).get("content") or []:
if item.get("type") == "text":
out.append(item.get("text", ""))
else:
out.append(json.dumps(item, indent=2))
return "\n".join(out) or json.dumps(result, indent=2)
def main() -> int:
ap = argparse.ArgumentParser(description="Probe the hosted DoorProfit MCP server")
ap.add_argument("--list", action="store_true", help="list tools (no key required)")
ap.add_argument("--usage", action="store_true", help="call get_api_usage (free)")
ap.add_argument("--crime", metavar="ADDRESS", help="call get_crime_data (1 credit)")
ap.add_argument("--neighborhood", metavar="ADDRESS",
help="call get_neighborhood_data (1 credit)")
args = ap.parse_args()
key = os.environ.get("DOORPROFIT_API_KEY", "")
needs_key = args.usage or args.crime or args.neighborhood
if needs_key and not key:
print("error: DOORPROFIT_API_KEY is not set "
"(free key: https://api.doorprofit.com/signup/)", file=sys.stderr)
return 2
init = rpc("initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "doorprofit-mcp-probe", "version": "1.0"},
}, api_key=key or None)
if "error" in init:
print(f"error: initialize failed: {init['error']['message']}", file=sys.stderr)
return 1
info = (init.get("result") or {}).get("serverInfo") or {}
print(f"connected: {info.get('title', '?')} v{info.get('version', '?')} @ {MCP_URL}")
if args.list or not needs_key:
tools = rpc("tools/list", {}, api_key=key or None, req_id=2)
if "error" in tools:
print(f"error: {tools['error']['message']}", file=sys.stderr)
return 1
print("\ntools:")
for t in (tools.get("result") or {}).get("tools") or []:
props = list(((t.get("inputSchema") or {}).get("properties") or {}).keys())
print(f" - {t['name']}({', '.join(props)})")
desc = (t.get("description") or "").strip().split("\n")[0]
print(f" {desc[:150]}")
if not needs_key:
return 0
calls = []
if args.usage:
calls.append(("get_api_usage", {}))
if args.crime:
calls.append(("get_crime_data", {"address": args.crime}))
if args.neighborhood:
calls.append(("get_neighborhood_data", {"address": args.neighborhood}))
for i, (name, arguments) in enumerate(calls, start=3):
print(f"\n--- {name} {json.dumps(arguments) if arguments else ''} ---")
res = rpc("tools/call", {"name": name, "arguments": arguments},
api_key=key, req_id=i)
if "error" in res:
print(f"error: {res['error']['message']}", file=sys.stderr)
return 1
result = res.get("result") or {}
if result.get("isError"):
print("tool error:", text_of(res), file=sys.stderr)
return 1
print(text_of(res))
return 0
if __name__ == "__main__":
sys.exit(main())
Thinking…

With this, we now have the MCP client configuration, widget HTML, .env.example, and README all set up.

File edits

1 file

Completed
Writtendoorprofit-example/mcp-config.jsonView changes

/sandbox/repo/doorprofit-example/mcp-config.json

Written content · previous contents were not recorded

{
"_comment": "DoorProfit's MCP server is HOSTED -- nothing to install or run locally. Paste this into your MCP client (Claude Desktop, ChatGPT connector, Cursor) and replace YOUR_API_KEY. Or use OAuth and drop the headers block entirely.",
"mcpServers": {
"doorprofit": {
"url": "https://api.doorprofit.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}

File edits

1 file

Completed
Writtendoorprofit-example/crime-map-widget.htmlView changes

/sandbox/repo/doorprofit-example/crime-map-widget.html

Written content · previous contents were not recorded

<!doctype html>
<!--
DoorProfit embeddable crime-map widget.
This is the cheapest way to display DoorProfit data: widget page-views draw
from a SEPARATE "embed views" quota (5,000 views/month free on every plan),
not your API-call quota, and map tiles are free.
Metering rules worth knowing:
* 1 view = 1 unique browser per calendar month per API key
* bots/crawlers are excluded and never count
* multiple widgets on one page count as ONE view
* geocoded addresses are cached server-side -> repeat loads cost 0 API calls
Two constraints:
* The "Crime data by DoorProfit" attribution link must stay visible,
or the map disables itself.
* Optional but recommended: lock the key to your domains in the dashboard
("Allowed domains", one hostname per line, ".example.com" for wildcards).
Requests from other domains get HTTP 403.
Swap data-key for your own, or inject it server-side so it never ships in
client source.
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DoorProfit crime map widget example</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem auto; max-width: 720px; }
h1 { font-size: 1.25rem; }
</style>
</head>
<body>
<h1>Crime map: 500 Adams St, Tulsa, OK</h1>
<!-- Interactive crime heatmap. MapLibre GL, no Mapbox/Google token needed. -->
<div id="doorprofit-crime-map"
data-address="500 Adams St, Tulsa, OK"
data-key="YOUR_API_KEY"
data-zoom="13"
data-height="400px"></div>
<script src="https://api.doorprofit.com/v1/embed/crime-map.js" async></script>
<!-- Crawlable fallback so non-JS visitors and search engines still get a link. -->
<noscript>
<a href="https://www.doorprofit.com/crime-map/" target="_blank" rel="noopener">
View crime data for this area on DoorProfit
</a>
</noscript>
</body>
</html>
Thinking…

Next up is .env.example and README. Before writing the pricing section in the README, let me try once more to find the actual pricing page — probably at api.doorprofit.com/billing (login required). Let me check if it redirects.

Let me check /billing and /dashboard.

for u in "https://api.doorprofit.com/billing/" "https://api.doorprofit.com/dashboard/" "https://api.doorprofit.com/dashboard"; do printf "%-45s " "$u"; timeout 15 curl -sS -o /tmp/b.html -w "%{http_code} %{size_download} -> %{redirect_url}\n" "$u" 2>&1|tail -1; done; echo "=== grep pricing words in billing/dashboard page ==="; grep -oiE '(starter|growth|pro|enterprise|free)[^<]{0,40}\$[0-9]+|\$[0-9]+[^<]{0,40}(month|mo)' /tmp/b.html 2>/dev/null | head -20
https://api.doorprofit.com/billing/ 200 33313 ->
https://api.doorprofit.com/billing/           200 33313 -> 
https://api.doorprofit.com/dashboard/         302 0 -> https://api.doorprofit.com/login/
https://api.doorprofit.com/dashboard          302 0 -> https://api.doorprofit.com/login/
=== grep pricing words in billing/dashboard page ===
cd /tmp && grep -oE '"price[^"]*"|[0-9]+ ?/ ?month|\$[0-9]+' dp_api.html dp_www.html dp_docs.html 2>/dev/null | sort -u | head -25
dp_api.html:$17
dp_api.html:$17
dp_api.html:$52
dp_api.html:$850
dp_docs.html:"price_monthly"
dp_www.html:"price"
dp_www.html:"priceCurrency"
dp_www.html:$8
Thinking…

/billing/ returns a 200 page of 33KB without a redirect — this might be a public pricing page! Let's try parsing it.

cd /tmp && timeout 25 curl -sSL "https://api.doorprofit.com/billing/" -o dp_billing.html && python3 - <<'EOF'
import re,html
s=open('/tmp/dp_billing.html',encoding='utf-8',errors='replace').read()
s=re.sub(r'<script.*?</script>','',s,flags=re.S|re.I)
s=re.sub(r'<style.*?</style>','',s,flags=re.S|re.I)
s=re.sub(r'<[^>]+>',' ',s); s=html.unescape(s); s=re.sub(r'[ \t]+',' ',s); s=re.sub(r'\n\s*\n+','\n',s)
t=s.strip()
print(len(t)); print(t[:6000])
EOF
2529
2529
Pricing - DoorProfit API 
 DoorProfit API
 Home 
 Docs 
 Main Site 
 Login 
 Get Your API Key 
 DoorProfit API
 Home 
 Docs 
 Main Site 
 Login 
 Get Your API Key 
 Simple, transparent pricing 
 Choose the plan that fits your needs. All plans include overage billing so you never hit a wall. 
 Free 
 $0 
 10 
 API calls / month 
 5,000 
 Embed views / month 
 Perfect for testing and evaluation 
 Rate limit: 2 req/sec 
 Daily limit: 50/day 
 All API endpoints 
 Full documentation 
 Email support 
 Crime map embed widget 
 No overage (upgrade required) 
 Current Plan 
 Starter 
 $25 /mo 
 300 
 API calls / month 
 50,000 
 Embed views / month 
 Great for small applications 
 Rate limit: 5 req/sec 
 Daily limit: 1,500/day 
 All API endpoints 
 Full documentation 
 Priority email support 
 Crime map embed widget 
 $0.15/call API overage 
 $0.50/1K embed overage 
 Sign Up 
 Most Popular 
 Growth 
 $50 /mo 
 600 
 API calls / month 
 200,000 
 Embed views / month 
 Best value for growing apps 
 Rate limit: 10 req/sec 
 Daily limit: 3,000/day 
 All API endpoints 
 Full documentation 
 Priority support 
 Usage analytics 
 Crime map embed widget 
 $0.10/call API overage 
 $0.50/1K embed overage 
 Sign Up 
 Pro 
 $100 /mo 
 1,200 
 API calls / month 
 500,000 
 Embed views / month 
 For high-volume applications 
 Rate limit: 20 req/sec 
 Daily limit: 6,000/day 
 All API endpoints 
 Full documentation 
 Priority support 
 Usage analytics 
 Crime map embed widget 
 $0.08/call API overage 
 $0.50/1K embed overage 
 Sign Up 
 What are embed views? 
 Every plan includes the embeddable crime map widget — add an interactive crime heatmap to any website with just two lines of HTML. Embed views are billed separately from API calls so you can put crime maps on as many pages as you need without eating into your API quota. 
 Monthly Active Users 
 Each unique visitor counts once per month. Repeat visits from the same browser don't increase your bill. 
 Bots excluded 
 Googlebot, Bingbot, SEO crawlers, and social previewers are automatically filtered. Only real humans count. 
 Free tiles & geocoding 
 Map tiles and cached geocodes cost nothing. Most page loads use zero API calls. 
 2 lines of code 
 Paste a <div> and a <script> tag — the widget handles the rest. See the docs . 
 Need more? Contact us at support@doorprofit.com for enterprise pricing. 
 All plans billed monthly. Overage charges billed at $25 threshold or end of billing period. 
 © 2026 DoorProfit API. All rights reserved.  |  Terms  |  Privacy
Thinking…

Now I've got the full text of the official pricing page (publicly available at https://api.doorprofit.com/billing/, the docs have a broken link to /pricing).

Pricing: | Plan | Price | API calls/month | Embed views/month | Rate limit | Daily limit | API overage | Embed overage | | Free | $0 | 10 | 5,000 | 2 req/sec | 50/day | none (upgrade required) | — | | Starter | $25/month | 300 | 50,000 | 5 req/sec | 1,500/day | $0.15/call | $0.50/1K | | Growth | $50/month | 600 | 200,000 | 10 req/sec | 3,000/day | $0.10/call | $0.50/1K | | Pro | $100/month | 1,200 | 500,000 | 20 req/sec | 6,000/day | $0.08/call | $0.50/1K | | Enterprise | Custom | Custom | Custom | Custom | Custom | — | — |

All plans billed monthly. Overage charges are billed at the point when they hit the $25 threshold, or at the end of the billing period. Embed views = MAU (unique visitors per browser per month), bots excluded, tiles and cached geocoding are free.

Effective cost per call: Free = $0 but capped at 10 calls. Starter = $25/300 = $0.083/call. Growth = $50/600 = $0.083/call. Pro = $100/1200 = $0.083/call. Interesting — the per-call unit price is identical across all paid plans. You're only buying rate limits and daily caps. This is a useful insight.

Alright, now let me write the README with the pricing table, run tests (dry run, no-key path, MCP probe --list), and verify everything works.

Let me write the README and .env.example, then test.

Found the real pricing page. Now the README:

File edits

1 file

Completed
Writtendoorprofit-example/.env.exampleView changes

/sandbox/repo/doorprofit-example/.env.example

Written content · previous contents were not recorded

# Copy to .env and fill in. Free key: https://api.doorprofit.com/signup/
# Never commit a real key -- .gitignore already excludes .env.
DOORPROFIT_API_KEY=

File edits

1 file

Completed
Writtendoorprofit-example/README.mdView changes

/sandbox/repo/doorprofit-example/README.md

Written content · previous contents were not recorded

# DoorProfit API example
A minimal, dependency-free example against the **hosted** DoorProfit API.
Nothing to install, no local stack, no server to run — it is plain HTTPS GETs
to `https://api.doorprofit.com/v1`.
DoorProfit is a US location-intelligence API: crime safety grades and incident
counts, neighborhood demographics/income/housing/rent/weather/disaster risk,
and registered-offender search — all keyed by street address or lat/lng.
## What is here
| File | What it does |
| --- | --- |
| `doorprofit.py` | Stdlib-only Python client (`crime`, `neighborhood`, `offenders`, `usage`) with disk caching, 429 backoff, and typed errors |
| `report.py` | CLI that prints a property due-diligence report. Has `--dry-run` (spends 0 credits) and `--usage-only` (free) |
| `report.mjs` | Same thing in Node 18+ (`fetch`, no packages) |
| `mcp_probe.py` | Talks to DoorProfit's **hosted MCP server** — `--list` needs no key |
| `mcp-config.json` | Drop-in MCP client config for Claude / ChatGPT / Cursor |
| `crime-map-widget.html` | The embeddable crime heatmap, which bills to a separate quota |
| `.env.example` | Template for `DOORPROFIT_API_KEY` |
## Setup
There is no SDK. The `import DoorProfit from 'doorprofit'` snippet on the
DoorProfit homepage is illustrative — **no `doorprofit` package exists on npm
or PyPI**. You call REST directly (or use their hosted MCP server).
```bash
cd doorprofit-example
export DOORPROFIT_API_KEY=your_key # free key: https://api.doorprofit.com/signup/
python3 report.py --dry-run "500 Adams St, Tulsa, OK" # 0 credits, no key needed
python3 report.py --usage-only # free endpoint
python3 report.py "500 Adams St, Tulsa, OK" # costs 3 credits
python3 report.py --json "500 Adams St, Tulsa, OK" | jq .crime.crime_score
node report.mjs "500 Adams St, Tulsa, OK"
python3 mcp_probe.py --list # no key needed
DOORPROFIT_API_KEY=$DOORPROFIT_API_KEY python3 mcp_probe.py --crime "500 Adams St, Tulsa, OK"
```
Zero dependencies, so there is no install step. Python 3.9+ / Node 18+.
## The four endpoints
```
GET /v1/crime safety grade A+..F, numeric score, incident counts by type
GET /v1/neighborhood demographics, income, housing, education, cost of living, rent, weather, disaster risk
GET /v1/offenders registered offenders by location+radius, or by name (paginated; each page = 1 credit)
GET /v1/usage your plan, quota, remaining calls -- FREE, never metered
```
Auth is a `key` query param on every request. Each endpoint takes either
`address=` or `lat=` + `lng=`.
> **Free test address:** `500 Main St, Dallas, TX 75202` is documented as never
> consuming quota. Use it to validate an integration without spending credits.
## How pricing works
Pricing is **per-plan monthly call quota**, not per-call. Source:
<https://api.doorprofit.com/billing/> (the docs' "pricing page" links point at
`/pricing`, which 404s — `/billing` is the live page).
| Plan | Price | API calls/mo | Daily cap | Rate limit | Embed views/mo | API overage | Embed overage |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Free | $0 | **10** | 50/day | 2 req/s | 5,000 | none — hard 429 | — |
| Starter | $25/mo | 300 | 1,500/day | 5 req/s | 50,000 | $0.15/call | $0.50/1K |
| Growth | $50/mo | 600 | 3,000/day | 10 req/s | 200,000 | $0.10/call | $0.50/1K |
| Pro | $100/mo | 1,200 | 6,000/day | 20 req/s | 500,000 | $0.08/call | $0.50/1K |
| Enterprise | custom | custom | custom | custom | custom | — | — |
Things that actually matter when budgeting:
- **Effective price per call is ~$0.083 on every paid tier** ($25/300,
$50/600, $100/1,200 all work out the same). Upgrading buys you a higher
*rate limit* and *daily cap*, not cheaper data. Pick the tier by
requests/sec and burst needs, not by unit price.
- **Overage is opt-in and tier-dependent.** Paid plans keep working past the
cap at $0.08–$0.15/call; the Free plan has no overage and just returns 429
`QUOTA_EXCEEDED`. Overage is billed once it hits $25 or at period end.
- **One report = 3 credits.** Crime, neighborhood, and offenders are separate
metered calls — there is no combined endpoint. On the free plan that is
**3 reports per month**. `report.py` caches to `.doorprofit-cache/` so
re-running the same address costs nothing.
- **Offender pagination costs extra** — each `page=` request is another credit.
- **Embed views are a separate, much cheaper bucket.** The widget bills by
monthly *unique visitor* (same browser all month = 1 view), excludes bots,
and map tiles plus cached geocodes are free. Displaying data on a public
page is far cheaper via the widget than via API calls.
- `/v1/usage` is free and uncounted, so you can poll it freely to self-monitor.
Every response also carries `X-Monthly-Remaining`, `X-Daily-Remaining`,
`X-RateLimit-Limit`, and `X-Plan` headers.
## Agent / MCP workflow
DoorProfit hosts an MCP server at `https://api.doorprofit.com/mcp` — there is
nothing to install or run locally. Tools: `get_crime_data`,
`get_neighborhood_data`, `search_offenders` (1 credit each), `get_api_usage`
(free). Auth via OAuth or `Authorization: Bearer <key>`. MCP calls are metered
exactly like REST calls.
See `mcp-config.json`, or run `python3 mcp_probe.py --list` (works with no key
at all — `initialize` and `tools/list` are unauthenticated).
## Gotchas
- Keys in query strings leak into logs. Prefer server-side calls, and for the
widget set **Allowed domains** in the dashboard so a stolen key can't render
embeds elsewhere (non-allowlisted origins get 403).
- The widget's "Crime data by DoorProfit" attribution link is **mandatory** —
removing it disables the map.
- `incidents` is `null` in cities that don't publish incident-level data; the
grade then falls back to block-group statistics. Check
`crime_score_source` (`"incidents"` vs `"block_group"`) before trusting the
granularity.
- `incidents` are **counts within 1,000 ft**, over a `date_from`→`date_to`
window that varies by city. There is no list of individual incidents, despite
what some marketing copy implies.
- Offender data is sensitive PII. Handle it accordingly and check the
[terms](https://api.doorprofit.com/terms/) for permitted use.
Thinking…

Alright, let's test everything that can be run without a key:

  1. python3 report.py --dry-run "..."
  2. python3 report.py without a key → error path (exit 2)
  3. python3 mcp_probe.py --list → works without a key
  4. node report.mjs --usage-only without a key → error path
  5. Python syntax check

Also, should I add .doorprofit-cache and .env to .gitignore? The repo's .gitignore doesn't have .env or the cache directory. It's something to consider, but the .gitignore is at the repo root. I could add entries. Let me check whether the README says ".gitignore already excludes .env" — needs verification! The root .gitignore doesn't have .env. I'll add it (small addition, keeps the claim correct).

Let's run the tests.

cd /sandbox/repo/doorprofit-example && echo "=== compile ===" && python3 -m py_compile doorprofit.py report.py mcp_probe.py && echo OK && node --check report.mjs && echo "node OK" && echo && echo "=== dry-run (0 credits) ===" && python3 report.py --dry-run "500 Adams St, Tulsa, OK"
=== compile ===
=== compile ===
OK
node OK

=== dry-run (0 credits) ===
# DRY RUN -- no requests sent, 0 credits used

# Free quota check:  GET https://api.doorprofit.com/v1/usage?key=REDACTED

# crime  (1 credit)
curl -G "https://api.doorprofit.com/v1/crime" \
  --data-urlencode "address=500 Adams St, Tulsa, OK" \
  --data-urlencode "key=YOUR_API_KEY" \
  -H "Accept: application/json"

# neighborhood  (1 credit)
curl -G "https://api.doorprofit.com/v1/neighborhood" \
  --data-urlencode "address=500 Adams St, Tulsa, OK" \
  --data-urlencode "key=YOUR_API_KEY" \
  -H "Accept: application/json"

# offenders  (1 credit)
curl -G "https://api.doorprofit.com/v1/offenders" \
  --data-urlencode "address=500 Adams St, Tulsa, OK" \
  --data-urlencode "radius=1.0" \
  --data-urlencode "page=1" \
  --data-urlencode "limit=50" \
  --data-urlencode "key=YOUR_API_KEY" \
  -H "Accept: application/json"

# Total cost: 3 credits
# Free plan = 10 credits/month, 50/day, 2 req/sec.
cd /sandbox/repo/doorprofit-example && echo "=== no-key error path (report.py) ===" && env -u DOORPROFIT_API_KEY python3 report.py "500 Adams St, Tulsa, OK"; echo "exit=$?"; echo; echo "=== no-key error path (report.mjs) ==="; env -u DOORPROFIT_API_KEY node report.mjs "500 Adams St, Tulsa, OK"; echo "exit=$?"; echo; echo "=== bad key -> real 401 from API ==="; DOORPROFIT_API_KEY=invalid python3 report.py --usage-only; echo "exit=$?"
=== no-key error path (report.py) ===
=== no-key error path (report.py) ===
error: No API key. Set DOORPROFIT_API_KEY or pass api_key=... (free key: https://api.doorprofit.com/signup/)
exit=2

=== no-key error path (report.mjs) ===
error: DOORPROFIT_API_KEY is not set.
Get a free key at https://api.doorprofit.com/signup/
exit=2

=== bad key -> real 401 from API ===
error: The API key provided is invalid or inactive. [INVALID_API_KEY]
docs: https://api.doorprofit.com/docs/
exit=1
Thinking…

All the key-not-required paths work, and the actual 401 from the live API is correctly parsed by my error handler. Next, let's test the MCP probe without a key.

Also, I'll test the print_report function with a mocked/synthetic payload to verify the rendering (since I can't call the live API). Let me inject a fixture based on the documented example response and run print_report.

I'll do both.

cd /sandbox/repo/doorprofit-example && echo "=== MCP probe --list (no key, hits hosted server) ===" && python3 mcp_probe.py --list; echo "exit=$?"
=== MCP probe --list (no key, hits hosted server) ===
=== MCP probe --list (no key, hits hosted server) ===
connected: DoorProfit Crime & Neighborhood Data v1.0.0 @ https://api.doorprofit.com/mcp

tools:
  - get_crime_data(address, lat, lng)
      Crime safety grade and nearby incident counts for one US address or coordinate pair. Returns a letter score (A+ safest through F), a short description
  - get_neighborhood_data(address, lat, lng)
      Profile of the neighborhood (or census block group) containing one US address or coordinate pair: demographics, median household income, home values, 
  - search_offenders(address, lat, lng, radius, first_name, last_name, dob, city, state, zipcode, page, limit)
      Search US registered offenders in one of two modes. Location mode: pass address or lat+lng, plus an optional radius in miles (default 1, capped at 3);
  - get_api_usage()
      Report the plan, monthly and daily call usage, remaining quota, reset times, rate limit and overage status for the DoorProfit API key authenticating t
exit=0
cd /sandbox/repo/doorprofit-example && cat > /tmp/render_test.py <<'PY'
# Verify report rendering using the documented example payloads (no API calls).
import json, sys
sys.path.insert(0, '.')
from report import print_report

crime = json.loads('''{"success":true,"location":{"address":"500 Main St, Dallas, TX 75202, USA","city":"Dallas","state":"TX","zipcode":"75202","county":"Dallas County","lat":32.78014,"lng":-96.800454},"crime_score":"C+","crime_numeric":0.21,"crime_score_source":"incidents","crime_description":"Higher than average crime","incidents":{"radius_feet":1000,"date_from":"2025-04-01","date_to":"2026-04-01","count":245,"by_type":{"murder":0,"shooting":3,"assault":74,"sexual_offense":7,"robbery":5,"kidnapping":1,"weapons":1,"theft":109,"burglary":5,"drug_offense":1,"vandalism":39,"arson":0}}}''')
hood = json.loads('''{"success":true,"location":{"address":"500 Main St, Dallas, TX 75202, USA","lat":32.78014,"lng":-96.800454},"neighborhood":{"name":"West End Historic District","type":"neighborhood","demographics":{"population":4280,"population_density":1200.5,"median_age":34.2},"income":{"median":52400},"housing":{"median_home_value":285000},"rent":{"median":1150,"by_bedroom":{"1br":950,"2br":1150,"3br":1400,"4br":1600}},"education":{"bachelors_pct":18.3,"graduate_pct":6.1},"cost_of_living":{"overall_index":95.2},"weather":{"natural_disaster_risk":{"overall_index":110.4,"earthquake":12.0,"hurricane":40.2,"tornado":280.1}}}}''')
off = json.loads('''{"success":true,"offenders_count":5,"radius_miles":1,"offenders":[{"name":"J*** D**","offense":"Sexual Assault (2019)","distance_miles":0.3},{"name":"A*** B**","offense":"Indecency with a child (2017)","distance_miles":0.6}]}''')
print_report(crime, hood, off, "500 Main St, Dallas, TX 75202")

print("\n\n########## incidents:null + offender failure variant ##########\n")
crime2 = dict(crime); crime2["incidents"]=None; crime2["crime_score"]="A-"; crime2["crime_numeric"]=0.08; crime2["crime_score_source"]="block_group"
print_report(crime2, {"success":True,"location":crime["location"],"neighborhood":{}}, {"success":False,"error":{"message":"not available in this state"}}, "Somewhere")
PY
python3 /tmp/render_test.py
====================================================================
====================================================================
  DoorProfit due diligence: 500 Main St, Dallas, TX 75202, USA
  32.78014, -96.800454  |  Dallas, TX 75202
====================================================================

SAFETY
  Grade           C+   7/13 (A+ safest)
  Crime score     0.21 / 1.00  [####................]  (lower = safer)
  Source          incidents
  Description     Higher than average crime

  INCIDENTS within 1000 ft, 2025-04-01 -> 2026-04-01
  Total           245
    theft               109
    assault              74
    vandalism            39
    sexual offense        7
    robbery               5
    burglary              5
    shooting              3
    kidnapping            1
    weapons               1
    drug offense          1
    none reported: murder, arson

NEIGHBORHOOD
  Name            West End Historic District (neighborhood)
  Population      4,280 | density 1200.5/sq mi | median age 34.2
  Median income   $52,400
  Median home     $285,000
  Median rent     $1,150/mo   (1br $950, 2br $1,150, 3br $1,400, 4br $1,600)
  Bachelor's+     18.3%  | grad 6.1%
  Cost of living  95.2 (100 = US avg)
  Disaster risk   overall 110.4  tornado 280.1  hurricane 40.2  earthquake 12.0

REGISTERED OFFENDERS
  Within 1 mi      5 record(s)
    - J*** D** | Sexual Assault (2019) | 0.3 mi
    - A*** B** | Indecency with a child (2017) | 0.6 mi
    ... and 3 more (paginate with page=/limit=)

====================================================================
  Data: DoorProfit (https://www.doorprofit.com). Verify before making decisions.
====================================================================


########## incidents:null + offender failure variant ##########

====================================================================
  DoorProfit due diligence: 500 Main St, Dallas, TX 75202, USA
  32.78014, -96.800454  |  Dallas, TX 75202
====================================================================

SAFETY
  Grade           A-   3/13 (A+ safest)
  Crime score     0.08 / 1.00  [##..................]  (lower = safer)
  Source          block_group
  Description     Higher than average crime

  INCIDENTS       not published for this city (incidents: null)

REGISTERED OFFENDERS
  not available in this state

====================================================================
  Data: DoorProfit (https://www.doorprofit.com). Verify before making decisions.
====================================================================
Thinking…

Everything is rendering correctly. One inconsistency I noticed: MCP's search_offenders states that the radius is "capped at 3," but the REST documentation says "Max: 5." A minor documentation inconsistency worth noting in the README's caveats section. I'll add it.

Also, fix the .gitignore so it excludes .env and .doorprofit-cache/ so that the README's descriptions are accurate.

Let's do both.

File edits

1 file

Completed
Modified.gitignore+6−0View changes

/sandbox/repo/.gitignore

Replacement excerpt

# Logs / OS cruft
*.log
.DS_Store
+
+# Secrets & local API response caches
+.env
+.env.*
+!.env.example
+.doorprofit-cache/

File edits

1 file

Completed
Modifieddoorprofit-example/README.md+3−0View changes

/sandbox/repo/doorprofit-example/README.md

Replacement excerpt

+- Docs disagree on the offender `radius` cap: the REST docs say max **5**
+ miles, the MCP tool description says capped at **3**. Stay at ≤3 to be safe
+ on both paths.
- Offender data is sensitive PII.
Thinking…

I need to investigate this further. Let me check the details.

I'll do a final check: run git status, list the files, and verify that the cache directory hasn't been created (it shouldn't exist since I haven't made any successful API calls). Also, I'll clean up pycache.

Let's verify.

cd /sandbox/repo && rm -rf doorprofit-example/__pycache__ && ls -la doorprofit-example && echo "=== git status ===" && git status --short && echo "=== ignore check ===" && mkdir -p doorprofit-example/.doorprofit-cache && touch doorprofit-example/.doorprofit-cache/x.json doorprofit-example/.env && git status --short --ignored | grep -E 'doorprofit-cache|\.env' ; rm -rf doorprofit-example/.doorprofit-cache doorprofit-example/.env
total 68
total 68
drwx------ 2 root root  4096 Oct 18 02:44 .
drwxr-xr-x 4 root root  4096 Oct 18 02:42 ..
-rw------- 1 root root   156 Oct 18 02:44 .env.example
-rw------- 1 root root  6483 Oct 18 02:44 README.md
-rw------- 1 root root  1960 Oct 18 02:43 crime-map-widget.html
-rw------- 1 root root  8954 Oct 18 02:42 doorprofit.py
-rw------- 1 root root   414 Oct 18 02:43 mcp-config.json
-rw------- 1 root root  5719 Oct 18 02:43 mcp_probe.py
-rw------- 1 root root  5680 Oct 18 02:42 report.mjs
-rw------- 1 root root 10244 Oct 18 02:42 report.py
=== git status ===
 M .gitignore
?? doorprofit-example/
=== ignore check ===
!! doorprofit-example/.doorprofit-cache/
!! doorprofit-example/.env
Thinking…

Everything is clean. .env.example is tracked (not ignored) as intended.

Now, let me write the final answer. I need to cover the following:

  1. What I built (with paths)
  2. The pricing structure
  3. Whether this product is easy for me to manage
  4. That I got blocked → clearly: I can't get an API key myself (email verification required), so live calls are blocked. But since a developer workflow exists, I shouldn't stop — I built everything I could.

Actually — an important nuance regarding the instructions: "If this product doesn't have a developer workflow you can work on, state so clearly and stop." DoorProfit does have a developer workflow I can work on (hosted REST + hosted MCP, and I actually worked on them — initialize/tools/list succeeded). So I shouldn't stop. But I hit a partial blocker: I can't create an account myself (email verification). I have to state that clearly.

Also, I'll note that I didn't start a local stack or run any long-running commands — I complied.

Let me write it concisely and completely.

Done. Built and verified what I could without a key.

What I found

DoorProfit is a real hosted product — a US location-intelligence REST API (crime grades + incident counts, neighborhood demographics/income/housing/rent/weather/disaster risk, registered-offender search), keyed by address or lat/lng. It does have a developer workflow I can act on, so I proceeded:

  • https://api.doorprofit.com/v1/{crime,neighborhood,offenders,usage} — live, I got real 401s back with structured error bodies
  • https://api.doorprofit.com/mcp — a hosted MCP server. I completed initialize and tools/list against it successfully (both unauthenticated), confirming 4 tools
  • No local stack, nothing long-running. Zero install steps.

There is no SDK. The homepage's import DoorProfit from 'doorprofit' is decorative — no doorprofit package exists on npm or PyPI (I checked both registries directly; the PyPI 200 was a JS-challenge page). Integration is plain HTTPS GETs.

Files — /sandbox/repo/doorprofit-example/

FileNotes
doorprofit.pyStdlib-only client: 4 endpoints, disk cache, 429 backoff, typed errors
report.pyDue-diligence CLI; --dry-run (0 credits), --usage-only (free), --json
report.mjsNode 18+ equivalent, fetch, no packages
mcp_probe.pyHits the hosted MCP server; --list needs no key
mcp-config.jsonDrop-in config for Claude/ChatGPT/Cursor
crime-map-widget.htmlEmbed widget (separate, cheaper quota)
README.mdPricing table, gotchas, budgeting notes

Verified: both languages compile, dry-run prints correct curl + credit cost, no-key paths exit 2 with a signup hint, a bad key returns the live API's INVALID_API_KEY parsed correctly, mcp_probe.py --list works, and report rendering was tested against the documented payloads (including the incidents: null and offender-failure variants). Added .env / .doorprofit-cache/ to the root .gitignore.

How pricing works

Per-plan monthly call quota, not per-call. Live page is api.doorprofit.com/billing/ — the docs' "pricing page" links point at /pricing, which 404s.

PlanPriceCalls/moDailyRateEmbed views/moAPI overage
Free$010502/s5,000none — hard 429
Starter$253001,5005/s50,000$0.15/call
Growth$506003,00010/s200,000$0.10/call
Pro$1001,2006,00020/s500,000$0.08/call
Enterprisecustomcustomcustomcustomcustom—

Embed overage is $0.50/1K on all paid tiers; overage bills at a $25 threshold or period end.

The non-obvious parts:

  • Effective price per call is ~$0.083 on every paid tier ($25/300 = $50/600 = $100/1200). Upgrading buys rate limit and daily burst headroom, not cheaper data. Choose by req/sec.
  • One report = 3 credits — no combined endpoint. Free plan ⇒ 3 reports/month. That's why the client caches.
  • Offender pagination costs a credit per page.
  • /v1/usage is free and uncounted; every response also carries X-Monthly-Remaining / X-Plan headers, so you can self-monitor at no cost.
  • Embed views bill per unique browser per month, exclude bots, and tiles + cached geocodes are free — displaying data publicly is far cheaper through the widget than via API calls. Attribution link is mandatory or the map self-disables.
  • 500 Main St, Dallas, TX 75202 is a documented free test address that never consumes quota.

Is it easy for me to manage?

Yes — unusually so. Stateless GETs, one auth param, clean JSON, structured error codes with documentation_url, a free uncounted usage endpoint, quota headers on every response, and a hosted MCP server I reached with zero setup. It's about as agent-friendly as an API gets. Two frictions: the small free quota means I must budget calls deliberately (hence the caching and --dry-run), and there's a real docs bug trail — broken /pricing links, a phantom npm SDK, and a radius cap the REST docs say is 5 miles while the MCP tool says 3.

Blocked

I could not obtain an API key, so no live data calls were made. Signup at api.doorprofit.com/signup/ is a two-step form (email → name/password) that requires email verification — there's a /api/resend_verification endpoint, confirming it. I have no mailbox, and I won't register an account against a fabricated address. Every endpoint rejects unauthenticated requests with 401, including the free test address, so that escape hatch doesn't work either.

Hand me a key (export DOORPROFIT_API_KEY=...) and python3 report.py --usage-only will confirm it for free, then a full report runs for 3 credits. Until then, --dry-run and mcp_probe.py --list both work with no key.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge