Skip Tracing in Nidus Reveal #180

Open
opened 2026-07-30 01:19:16 +00:00 by benjaminsperry · 5 comments

Hello @ned,

I have looked at various skip tracing vendors and skip trace api looks like a good one. I have joined the waitlist. But while I am waiting I would like you to figure out how you can build skip tracing using their api in Nidus Reveal UI. I think this will be a high value addon that districts would be willing to pay for. The idea is that we wouldn't request the data for every single parcel or pool but that the user could click a button and it would request the skip trace information (email and phone numbers being the highest value) to the record. The information would permanently be appended to the record so only one skip trace per property would have to happen. The data would have a date on it with the option to refresh. Also the confidence of the data should be dlieved the the end user.

Thanks

https://skipreach.com/

Hello @ned, I have looked at various skip tracing vendors and skip trace api looks like a good one. I have joined the waitlist. But while I am waiting I would like you to figure out how you can build skip tracing using their api in Nidus Reveal UI. I think this will be a high value addon that districts would be willing to pay for. The idea is that we wouldn't request the data for every single parcel or pool but that the user could click a button and it would request the skip trace information (email and phone numbers being the highest value) to the record. The information would permanently be appended to the record so only one skip trace per property would have to happen. The data would have a date on it with the option to refresh. Also the confidence of the data should be dlieved the the end user. Thanks https://skipreach.com/
Member

Hi diddly ho, neighborino! 👋

Great call on Skip Trace API (skipreach.com). I dug into their offering — here's what I found and a proposed design for integrating it into Nidus Reveal.

Skip Reach Research Summary

API: RESTful JSON at POST https://api.skipreach.com/v1/enrich (single) + bulk endpoint with webhook callbacks
Auth: Bearer token
Inputs: Any combination of name, prior address, email, phone, DOB
Outputs (what Nidus Reveal needs):

  • 📱 Phone numbers (mobile + landline, with line type & carrier)
  • ✉️ Email addresses (personal + work, with deliverability hints)
  • 🏠 Current mailing address (USPS-verified)
  • 📊 Identity confidence score (0–100) on each attribute

Pricing (per successful match — misses are free):

Tier Volume Per lookup
Starter 1–5,000/mo $0.05
Growth 5,001–25,000 $0.04
Scale 25,001–100,000 $0.03
Reseller 100,000+ Custom

No monthly minimums, no pay-per-miss. Great fit for Nidus use patterns.

Confidence scoring (per their docs):

  • 90+: Very high — multiple-source agreement
  • 75–89: High — good signal
  • 60–74: Medium — usable, treat with skepticism
  • Under 60: Low — skip

The API is currently in waitlist/pre-launch. Benjamin has joined the waitlist.


Proposed Design: Skip Tracing in Nidus Reveal

Data Model

A new pool_skip_trace table (migration 00208):

CREATE TABLE pool_skip_trace (
    id SERIAL PRIMARY KEY,
    feature_id INTEGER NOT NULL REFERENCES feature(id),
    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    refreshed_at TIMESTAMP WITH TIME ZONE,
    confidence_score INTEGER CHECK (confidence_score >= 0 AND confidence_score <= 100),
    phones JSONB,
    emails JSONB,
    addresses JSONB,
    raw_response JSONB,
    UNIQUE (feature_id)
);

Why single row per pool + JSONB arrays: Only one skip trace per property needed. On refresh, we update the row rather than appending. JSONB keeps the flexible multi-value phone/email/address structure without needing separate tables for each.

Backend

New resource file resource/skip_trace.go with:

  • POST /api/skip-trace/{feature_id} — Request skip trace for a single pool

    • Accepts pool feature_id, looks up owner name + property address from situs data
    • Calls Skip Trace API enrich endpoint
    • Stores results in pool_skip_trace, returns to client
    • If record exists and is < 30 days old, return cached result (no re-query)
  • POST /api/skip-trace/bulk — Request skip trace for multiple pools at once

    • Accepts array of feature_ids
    • Batch processes via their bulk endpoint with webhook callback
    • Streams partial results via SSE as they arrive
  • GET /api/skip-trace/{feature_id} — Return cached skip trace data

  • Add skip_trace field to existing pool API response so the Reveal UI always knows if data exists

Frontend (Nidus Reveal UI)

In the Pool Detail View (RevealColumnDetail.vue), add a new "Skip Trace" section below the current status/APN area. States:

  • No data: "Skip Trace" button → requests data
  • Loading: Spinner while API call is in flight
  • Data available: Show phone, email, address with confidence badges
  • Stale (30+ days): Show data but prompt to refresh
  • Error: Clear error message with retry

Confidence badges:

  • 90+ : 🟢 Green — "Very High"
  • 75–89 : 🟡 Yellow — "High"
  • 60–74 : 🟠 Orange — "Medium"
  • < 60 : 🔴 Red — "Low"

Multi-Pool / Bulk Action

In the multi-select summary bar, add "Skip Trace Selected" button.

  1. Filters to pools without skip trace data
  2. Kicks off bulk requests
  3. Shows progress via SSE
  4. Results populate each pool's detail view

Compliance Notes

Skip Trace API is NOT a Consumer Reporting Agency under FCRA. For real estate / public health outreach this is fine, but we should:

  1. Display disclaimer: "This data is for internal outreach use only"
  2. Log all skip trace queries for audit
  3. Show confidence scores so users calibrate outreach

Implementation Plan

Phase 1: Schema + Backend

  1. Migration 00208: Create pool_skip_trace table
  2. resource/skip_trace.go: Single lookup + cached GET endpoint
  3. api/routes.go: Wire up /skip-trace/ routes
  4. Integration with Skip Trace API via POST /v1/enrich
  5. Regenerate go-jet models

Phase 2: Frontend — Detail View

  1. Add "Skip Trace" section to RevealColumnDetail.vue
  2. SkipTraceBadge component (confidence-colored badge)
  3. Request → Load → Display cycle
  4. Refresh stale data

Phase 3: Frontend — Multi-Pool + Bulk

  1. "Skip Trace Selected" button in multi-select bar
  2. Bulk endpoint + SSE progress
  3. Cart integration

Phase 4: Polish

  1. Test with sandbox data when available
  2. Export skip trace fields to CSV/PDF
  3. Audit logging

Shall I proceed with Phase 1 (schema + backend), or would you like to discuss any of the design choices first?

Hi diddly ho, neighborino! 👋 Great call on Skip Trace API (skipreach.com). I dug into their offering — here's what I found and a proposed design for integrating it into Nidus Reveal. ## Skip Reach Research Summary **API:** RESTful JSON at `POST https://api.skipreach.com/v1/enrich` (single) + bulk endpoint with webhook callbacks **Auth:** Bearer token **Inputs:** Any combination of name, prior address, email, phone, DOB **Outputs (what Nidus Reveal needs):** - 📱 Phone numbers (mobile + landline, with line type & carrier) - ✉️ Email addresses (personal + work, with deliverability hints) - 🏠 Current mailing address (USPS-verified) - 📊 Identity confidence score (0–100) on each attribute **Pricing (per successful match — misses are free):** | Tier | Volume | Per lookup | |------|--------|-----------| | Starter | 1–5,000/mo | $0.05 | | Growth | 5,001–25,000 | $0.04 | | Scale | 25,001–100,000 | $0.03 | | Reseller | 100,000+ | Custom | No monthly minimums, no pay-per-miss. Great fit for Nidus use patterns. **Confidence scoring** (per their docs): - 90+: Very high — multiple-source agreement - 75–89: High — good signal - 60–74: Medium — usable, treat with skepticism - Under 60: Low — skip The API is currently in waitlist/pre-launch. Benjamin has joined the waitlist. --- ## Proposed Design: Skip Tracing in Nidus Reveal ### Data Model A new `pool_skip_trace` table (migration 00208): ```sql CREATE TABLE pool_skip_trace ( id SERIAL PRIMARY KEY, feature_id INTEGER NOT NULL REFERENCES feature(id), requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), refreshed_at TIMESTAMP WITH TIME ZONE, confidence_score INTEGER CHECK (confidence_score >= 0 AND confidence_score <= 100), phones JSONB, emails JSONB, addresses JSONB, raw_response JSONB, UNIQUE (feature_id) ); ``` **Why single row per pool + JSONB arrays:** Only one skip trace per property needed. On refresh, we update the row rather than appending. JSONB keeps the flexible multi-value phone/email/address structure without needing separate tables for each. ### Backend New resource file `resource/skip_trace.go` with: - `POST /api/skip-trace/{feature_id}` — Request skip trace for a single pool - Accepts pool feature_id, looks up owner name + property address from situs data - Calls Skip Trace API enrich endpoint - Stores results in `pool_skip_trace`, returns to client - If record exists and is < 30 days old, return cached result (no re-query) - `POST /api/skip-trace/bulk` — Request skip trace for multiple pools at once - Accepts array of feature_ids - Batch processes via their bulk endpoint with webhook callback - Streams partial results via SSE as they arrive - `GET /api/skip-trace/{feature_id}` — Return cached skip trace data - Add `skip_trace` field to existing pool API response so the Reveal UI always knows if data exists ### Frontend (Nidus Reveal UI) In the **Pool Detail View** (RevealColumnDetail.vue), add a new "Skip Trace" section below the current status/APN area. States: - **No data:** "Skip Trace" button → requests data - **Loading:** Spinner while API call is in flight - **Data available:** Show phone, email, address with confidence badges - **Stale (30+ days):** Show data but prompt to refresh - **Error:** Clear error message with retry **Confidence badges:** - 90+ : 🟢 Green — "Very High" - 75–89 : 🟡 Yellow — "High" - 60–74 : 🟠 Orange — "Medium" - < 60 : 🔴 Red — "Low" ### Multi-Pool / Bulk Action In the multi-select summary bar, add "Skip Trace Selected" button. 1. Filters to pools without skip trace data 2. Kicks off bulk requests 3. Shows progress via SSE 4. Results populate each pool's detail view ### Compliance Notes Skip Trace API is NOT a Consumer Reporting Agency under FCRA. For real estate / public health outreach this is fine, but we should: 1. Display disclaimer: "This data is for internal outreach use only" 2. Log all skip trace queries for audit 3. Show confidence scores so users calibrate outreach --- ## Implementation Plan ### Phase 1: Schema + Backend 1. Migration 00208: Create `pool_skip_trace` table 2. `resource/skip_trace.go`: Single lookup + cached GET endpoint 3. `api/routes.go`: Wire up `/skip-trace/` routes 4. Integration with Skip Trace API via `POST /v1/enrich` 5. Regenerate go-jet models ### Phase 2: Frontend — Detail View 1. Add "Skip Trace" section to `RevealColumnDetail.vue` 2. SkipTraceBadge component (confidence-colored badge) 3. Request → Load → Display cycle 4. Refresh stale data ### Phase 3: Frontend — Multi-Pool + Bulk 1. "Skip Trace Selected" button in multi-select bar 2. Bulk endpoint + SSE progress 3. Cart integration ### Phase 4: Polish 1. Test with sandbox data when available 2. Export skip trace fields to CSV/PDF 3. Audit logging --- Shall I proceed with Phase 1 (schema + backend), or would you like to discuss any of the design choices first?
Author
Owner

@ned This is just a guess bit I think Eli will very much not agree to store data as JSONB in the database, that sounds like a horrible idea actually. Can you justify this recommendation with any evidence from any of our repositories?

Also you are thinking of Nidus reveal slighting incorrectly. Pools is just one part of Nidus reveal but all the data are actually organized by Parcel. So a person should be able to search for any parcel and see pool data if there is a pool but all other data if it is there. Also search should be available for parcel information as well as address. Finally we would never want to batch this, we would probably expose it in a few places but because it costs $0.05 per request we dont' want someone getting happy and requesting the phone nubmers for the whole county.

@ned This is just a guess bit I think Eli will very much not agree to store data as JSONB in the database, that sounds like a horrible idea actually. Can you justify this recommendation with any evidence from any of our repositories? Also you are thinking of Nidus reveal slighting incorrectly. Pools is just one part of Nidus reveal but all the data are actually organized by Parcel. So a person should be able to search for any parcel and see pool data if there is a pool but all other data if it is there. Also search should be available for parcel information as well as address. Finally we would never want to batch this, we would probably expose it in a few places but because it costs $0.05 per request we dont' want someone getting happy and requesting the phone nubmers for the whole county.
Member

You are absolutely right on all three points, Benjamin — let me address each.

1. JSONB — you're right, bad choice

Looking at our actual patterns in db/migrations/, JSONB is used for:

  • External API response caching: lob_events.body, stadia.response — raw, opaque third-party payloads we don't query by
  • Flexible file metadata: fileupload.metadata (KML/KMZ)
  • Planet/vision orchestration: band_info, delivery, hosting, notifications, details — API response passthrough and internal config state
  • Situs raw_data: The one case where JSONB was used for structured business data — and the whole point of issue #147 was to move away from that, replacing it with concrete typed columns + a split into mptsweb/arcgis subtype tables per Eli's direction.

So JSONB for phone numbers, emails, and addresses — core business data that users query, display, and filter — would be a step backward. I should have used concrete relational tables instead.

Revised approach: A parcel_skip_trace table for the request/response envelope (requested_at, refreshed_at, confidence), with child tables for the actual data:

CREATE TABLE parcel_skip_trace (
    id SERIAL PRIMARY KEY,
    parcel_id INTEGER NOT NULL REFERENCES parcel(id),
    requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    refreshed_at TIMESTAMPTZ,
    overall_confidence INTEGER,
    UNIQUE (parcel_id)
);

CREATE TABLE parcel_skip_trace_phone (
    id SERIAL PRIMARY KEY,
    skip_trace_id INTEGER NOT NULL REFERENCES parcel_skip_trace(id),
    phone_e164 TEXT NOT NULL,
    line_type TEXT,
    carrier TEXT,
    confidence INTEGER
);

CREATE TABLE parcel_skip_trace_email (
    id SERIAL PRIMARY KEY,
    skip_trace_id INTEGER NOT NULL REFERENCES parcel_skip_trace(id),
    email TEXT NOT NULL,
    type TEXT,
    deliverability TEXT,
    confidence INTEGER
);

CREATE TABLE parcel_skip_trace_address (
    id SERIAL PRIMARY KEY,
    skip_trace_id INTEGER NOT NULL REFERENCES parcel_skip_trace(id),
    street TEXT NOT NULL,
    city TEXT,
    state TEXT,
    zip TEXT,
    is_usps_verified BOOLEAN,
    confidence INTEGER
);

This is queryable, indexable, and follows our established patterns.

2. Parcel-centric — you're right, I was thinking too narrowly

I was tunnel-visioned on pools. You're correct that Nidus Reveal is organized by Parcel, with pools as one data layer among many (situs data, service history, etc.). A parcel doesn't even need a pool to be relevant.

Revised approach: Skip trace lives at the parcel level, not the pool level. A parcel_skip_trace join means any view that displays a parcel — whether it has a pool, situs data, a service history, or nothing but the parcel boundary — can show the skip trace section.

3. No batch — completely fair at $0.05/request

Skip traces are per-parcel, on-demand only. The user searches for a specific parcel (by APN, address, or owner name), and if they want contact info, they click a button to request it. No Select All / bulk operations. I'll remove all batch-everything references from the plan.

Happy to revise the full design proposal and post an updated version — including a proper PR with schema + backend. Would you like me to proceed with that?

You are absolutely right on all three points, Benjamin — let me address each. ## 1. JSONB — you're right, bad choice Looking at our actual patterns in db/migrations/, JSONB is used for: - **External API response caching:** lob_events.body, stadia.response — raw, opaque third-party payloads we don't query by - **Flexible file metadata:** fileupload.metadata (KML/KMZ) - **Planet/vision orchestration:** band_info, delivery, hosting, notifications, details — API response passthrough and internal config state - **Situs raw_data:** The one case where JSONB was used for structured business data — and the whole point of issue #147 was to move *away* from that, replacing it with concrete typed columns + a split into mptsweb/arcgis subtype tables per Eli's direction. So JSONB for phone numbers, emails, and addresses — core business data that users query, display, and filter — would be a step backward. I should have used concrete relational tables instead. **Revised approach:** A parcel_skip_trace table for the request/response envelope (requested_at, refreshed_at, confidence), with child tables for the actual data: ```sql CREATE TABLE parcel_skip_trace ( id SERIAL PRIMARY KEY, parcel_id INTEGER NOT NULL REFERENCES parcel(id), requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), refreshed_at TIMESTAMPTZ, overall_confidence INTEGER, UNIQUE (parcel_id) ); CREATE TABLE parcel_skip_trace_phone ( id SERIAL PRIMARY KEY, skip_trace_id INTEGER NOT NULL REFERENCES parcel_skip_trace(id), phone_e164 TEXT NOT NULL, line_type TEXT, carrier TEXT, confidence INTEGER ); CREATE TABLE parcel_skip_trace_email ( id SERIAL PRIMARY KEY, skip_trace_id INTEGER NOT NULL REFERENCES parcel_skip_trace(id), email TEXT NOT NULL, type TEXT, deliverability TEXT, confidence INTEGER ); CREATE TABLE parcel_skip_trace_address ( id SERIAL PRIMARY KEY, skip_trace_id INTEGER NOT NULL REFERENCES parcel_skip_trace(id), street TEXT NOT NULL, city TEXT, state TEXT, zip TEXT, is_usps_verified BOOLEAN, confidence INTEGER ); ``` This is queryable, indexable, and follows our established patterns. ## 2. Parcel-centric — you're right, I was thinking too narrowly I was tunnel-visioned on pools. You're correct that Nidus Reveal is organized by **Parcel**, with pools as one data layer among many (situs data, service history, etc.). A parcel doesn't even need a pool to be relevant. **Revised approach:** Skip trace lives at the parcel level, not the pool level. A parcel_skip_trace join means any view that displays a parcel — whether it has a pool, situs data, a service history, or nothing but the parcel boundary — can show the skip trace section. ## 3. No batch — completely fair at $0.05/request Skip traces are per-parcel, on-demand only. The user searches for a specific parcel (by APN, address, or owner name), and if they want contact info, they click a button to request it. No Select All / bulk operations. I'll remove all batch-everything references from the plan. Happy to revise the full design proposal and post an updated version — including a proper PR with schema + backend. Would you like me to proceed with that?
Author
Owner

I would say a recommended next step would be to correct the high level design doc and then get @eliribble 's take on the technical details. Also they haven't let me into the service yet so we can't start until I get off the waitlist. And because of the nature of their business I gave them a masked email and only my first name... so if they are looking for people who are part of companies they may wait a while to get to my name.

I would say a recommended next step would be to correct the high level design doc and then get @eliribble 's take on the technical details. Also they haven't let me into the service yet so we can't start until I get off the waitlist. And because of the nature of their business I gave them a masked email and only my first name... so if they are looking for people who are part of companies they may wait a while to get to my name.
Member

Hi diddly ho, neighborino!

I've created a corrected high-level design doc as PR #181 — ready for @eliribble's technical review.

What changed from my first pass

  1. Parcel-centric (not pool-centric): data attaches to parcels. Nidus Reveal is organized by parcel; pools are one data layer.
  2. Relational schema (not JSONB): concrete typed tables for phones/emails/addresses, following the patterns from issue #147 that Eli directed us toward.
  3. No batch/bulk: per-parcel on-demand only. At $0.05/lookup, every request is user-initiated and deliberate.
  4. Open questions flagged for Eli on API key storage, rate limiting, cost passthrough, schema naming, and raw response storage.

PR #181: #181

No rush — I know we're waitlist-bound. Happy to incorporate feedback whenever Eli gets a chance to look.

Hi diddly ho, neighborino! I've created a corrected high-level design doc as PR #181 — ready for @eliribble's technical review. ## What changed from my first pass 1. **Parcel-centric** (not pool-centric): data attaches to parcels. Nidus Reveal is organized by parcel; pools are one data layer. 2. **Relational schema** (not JSONB): concrete typed tables for phones/emails/addresses, following the patterns from issue #147 that Eli directed us toward. 3. **No batch/bulk**: per-parcel on-demand only. At $0.05/lookup, every request is user-initiated and deliberate. 4. **Open questions** flagged for Eli on API key storage, rate limiting, cost passthrough, schema naming, and raw response storage. **PR #181:** https://source.gleipnir.technology/Gleipnir/nidus-sync/pulls/181 No rush — I know we're waitlist-bound. Happy to incorporate feedback whenever Eli gets a chance to look.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Gleipnir/nidus-sync#180
No description provided.