Live: Tor + abuse feeds refreshed every 6 hours

LayerCall

n8n

Automation

What exists today

No community node exists yet. The built-in HTTP Request node does everything required, and on self-hosted n8n there is no per-task cost.

Setup

  1. 1Add an HTTP Request node, method POST.
  2. 2URL: https://www.layercall.com/v1/score/user
  3. 3Authentication: Generic Credential Type → Header Auth, name X-Api-Key, value your key.
  4. 4Body Content Type: JSON, then map ip, email and phone from the previous node.
  5. 5Follow it with an IF node branching on {{ $json.verdict }}.

Code

// n8n → Code node, mode "Run Once for Each Item".
//
// Preferred over HTTP Request + IF when you want the call, the fallback and
// the merge back onto the item in one node.

const key = $env.LAYERCALL_KEY;

let scored;
try {
  scored = await this.helpers.httpRequest({
    method: "POST",
    url: "https://www.layercall.com/v1/score/user",
    headers: { "X-Api-Key": key, "Content-Type": "application/json" },
    body: {
      ip: $json.ip,
      email: $json.email,
      phone: $json.phone,
    },
    json: true,
    timeout: 5000,
  });
} catch (e) {
  // Fail open, and mark it so a later node can sweep these up.
  scored = { verdict: "allow", risk_score: 0, degraded: true };
}

return {
  json: {
    ...$json,
    verdict: scored.verdict,
    risk_score: scored.risk_score,
    summary: scored.summary,
    degraded: scored.degraded ?? false,
  },
};

What to do with each verdict

Three outcomes, and the middle one is the one worth getting right — refusing a real customer usually costs more than reviewing them.

VerdictIn n8n
allowFall through the IF node's true branch and carry on.
reviewRoute to a Slack node posting {{ $json.summary }}, then a NoOp so the item still reaches the merge.
blockRoute to a Stop And Error node, or write to a rejects table. An IF with three outputs is a Switch node — use that rather than nesting two IFs.

What comes back

A real response, generated from the live API rather than written by hand. Branch on verdict; summary is a sentence written to be shown to a person, and components_checked tells you what actually went into the score.

Show the full response(POST /v1/score/user)
{
  "risk_score": 65,
  "verdict": "review",
  "summary": "Needs review (65/100) — Tor exit node, commercial VPN and datacenter ASN.",
  "components": {
    "email": {
      "email": "test@guerrillamail.com",
      "normalized_email": "test@guerrillamail.com",
      "risk_score": 100,
      "verdict": "block",
      "status": "do_not_mail",
      "sub_status": "disposable",
      "deliverability_score": 0,
      "did_you_mean": null,
      "signals": {
        "syntax_valid": true,
        "mx_found": true,
        "is_disposable": true,
        "is_homograph": false,
        "is_role_account": true,
        "is_free_provider": false,
        "is_suspicious_handle": true,
        "is_tagged": false,
        "is_risky_tld": false,
        "is_new_domain": null,
        "has_spf": true,
        "has_dmarc": true,
        "has_website": true,
        "mailbox_exists": null,
        "is_catch_all": null,
        "mailbox_status": "unavailable",
        "has_digital_footprint": null
      },
      "domain": "guerrillamail.com",
      "domain_age_days": null,
      "mx_provider": null,
      "mx_records": [
        "mail.guerrillamail.com."
      ],
      "abuse_reports": 0,
      "digital_footprint": {
        "has_gravatar": false,
        "gravatar_profile_url": null,
        "breach_count": null,
        "seen_in_breach": null
      }
    },
    "phone": {
      "parse_status": "ok",
      "phone": "+14155552671",
      "risk_score": 0,
      "signals": {
        "syntax_valid": true,
        "is_possible": true,
        "is_voip": false,
        "is_premium_rate": false,
        "is_toll_free": false,
        "assigned_area_code": true,
        "is_fictional": false
      },
      "number": {
        "e164": "+14155552671",
        "country": "US",
        "national": "(415) 555-2671",
        "international": "+1 415 555 2671",
        "line_type": "fixed_line_or_mobile"
      },
      "verdict": "allow",
      "abuse_reports": 0
    },
    "ip": {
      "ip": "185.220.101.1",
      "risk_score": 60,
      "signals": {
        "is_vpn": true,
        "is_proxy": true,
        "is_datacenter": true,
        "is_tor": true,
        "recent_abuse": false,
        "is_hijacked_netblock": false
      },
      "geo": {
        "country": "DE",
        "city": "Berlin",
        "asn": "AS60729",
        "isp": "Stiftung Erneuerbare Freiheit"
      },
      "vpn_provider": null,
      "hijacked_source": null,
      "verdict": "review",
      "abuse_reports": 0
    }
  },
  "components_checked": [
    "email",
    "phone",
    "ip"
  ],
  "linkage": {
    "device_email_count": null,
    "email_device_count": 0,
    "email_ip_count": 0,
    "subnet_rate_1h": 2,
    "domain_rate_1h": 0
  },
  "actor": {
    "type": "unknown",
    "proven": false,
    "basis": "none",
    "operator": null,
    "trigger": null,
    "detail": "No signature and no device fingerprint. Drop fp.js on the page, or pass the agent's signed request, to get an answer here."
  },
  "top_signals": [
    "ip: tor exit node",
    "ip: commercial vpn",
    "ip: datacenter asn",
    "email: disposable domain",
    "email: role account",
    "email: machine-generated handle"
  ]
}

Traps specific to n8n

$env is blocked by default in Code nodes

Self-hosted n8n ships with N8N_BLOCK_ENV_ACCESS_IN_NODE=true, so $env.LAYERCALL_KEY returns undefined and the call 401s with no obvious cause. Either set that variable to false, or put the key in a Header Auth credential and use the HTTP Request node instead — the credential store is the better answer on a shared instance.

this.helpers.httpRequest does not exist in every node type

It is available in the Code node. In a Function node on older versions it is not, and in expressions it never is. If you get 'Cannot read properties of undefined', check the node type before the code.

Looping a single-item node over a large list is slow and rude

500 items through a per-item HTTP Request node is 500 round trips. POST to /v1/batch with up to 500 values of one type instead — one request, one connection.

Questions

Can I batch a whole list in n8n?

Yes — POST to /v1/batch with up to 500 values of one type per request, which is far faster than looping a single-record node 500 times and much kinder to both sides.

Does this work on n8n Cloud as well as self-hosted?

Yes. The HTTP Request node and the Code node both exist on Cloud. The only difference is $env, which Cloud does not expose at all — on Cloud, use a Header Auth credential.