Overview
SuperMegaMind matches your list against a licensed database of 264.7M U.S. consumers and up to 307 attributes each, then returns aggregate statistics for the people who matched. Every attribute comes back as percentages and an index, where 100 is the national rate. 457 means your customers are 4.57 times as likely as the average American to have that trait.
| You send | You get back | You never get |
|---|---|---|
| Hashed emails, phones, or name + ZIP | Percentages and index scores per attribute, plus a private report link | Any individual's record, including your own customers' |
Two ways in, one implementation behind both: an MCP server for AI agents and assistants, and a REST API for your own code.
Use cases
Profile a client's customers
Send a client's customer list and get back who is actually on it, attribute by attribute: hobbies, home, household, geography, purchasing. Use it before the media plan, the creative brief or the pitch.
Check a synthetic audience
Generate personas cheaply with a model, then test them against real people. Keep the traits that hold up, rebuild the ones that don't, and show the client an audience backed by real data.
Build it into your AI app
Give your agent or client-facing assistant a tool that answers "who are these customers?" with real statistics instead of a guess. Your app hashes the list, so we never receive an email address.
Quickstart
- 1. Get a key. The API is in pilot and keys are issued by hand. Write to help@supermegamind.com. Your key starts with
smm_live_and is shown once. - 2. Hash your list. On your own systems, using the exact rules in Hashing. Raw values are rejected.
- 3. Connect. Add the MCP server to your agent, or call the REST endpoint from your code.
- 4. Profile. Call
profile_listwith 200 to 100,000 hashed records. You get the profile back, plus a link to open it as a report.
Connect over MCP
Streamable HTTP, stateless, authenticated with your key on every request.
| Setting | Value |
|---|---|
| URL | https://supermegamind.com/api/mcp |
| Header | Authorization: Bearer smm_live_... |
| Tools | describe_attributes, profile_list |
Claude Code
claude mcp add --transport http supermegamind https://supermegamind.com/api/mcp \
--header "Authorization: Bearer smm_live_YOUR_KEY"Cursor
In .cursor/mcp.json:
{
"mcpServers": {
"supermegamind": {
"url": "https://supermegamind.com/api/mcp",
"headers": { "Authorization": "Bearer smm_live_YOUR_KEY" }
}
}
}Claude Desktop
Through the mcp-remote bridge, in claude_desktop_config.json:
{
"mcpServers": {
"supermegamind": {
"command": "npx",
"args": ["mcp-remote", "https://supermegamind.com/api/mcp", "--header", "Authorization:${SMM_AUTH}"],
"env": { "SMM_AUTH": "Bearer smm_live_YOUR_KEY" }
}
}
}Your own app (TypeScript)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-agency-app", version: "1.0.0" });
await client.connect(
new StreamableHTTPClientTransport(new URL("https://supermegamind.com/api/mcp"), {
requestInit: { headers: { Authorization: `Bearer ${process.env.SMM_KEY}` } },
})
);
const result = await client.callTool({
name: "profile_list",
arguments: { records }, // [{ email: "<sha256>" }, ...], hashed on your side
});Connecting a model straight to the MCP server works for up to 200 to 300 records. For real customer lists, see Sending a real list.
Sending a real list
When a model calls an MCP tool, it writes every argument itself, one token at a time. Each hash is a 64-character string, so a model can only practically send 200 to 300 records, and it can miscopy one without anyone noticing. A model also can’t compute SHA-256 on its own, so in a plain chat app it needs a hashing tool before it can call profile_list at all.
| How you connect | Records per run | Best for |
|---|---|---|
| Model calls our MCP server directly | 200 to 300 | Trying it out, describe_attributes, small test lists |
| Your app wraps our REST API (recommended) | Up to 100,000 | Agencies and enterprises profiling real customer lists |
Wrap us
Give your model a tool of your own, such as profile_customer_file. Your tool reads the file, hashes it on your systems, calls our REST endpoint, and hands the model only the result. The model passes a file reference in and gets a profile out. The hashes never pass through it, so the list can be as large as a run allows, and your customers’ data stays inside your app until it’s hashed.
// A tool in YOUR app. The model passes a file reference and gets a profile back.
// The hashes never pass through the model, so the list can be up to 100,000 records.
import { readFileSync } from "node:fs";
import { parse } from "csv-parse/sync";
import { hashEmail, hashPhone, hashNameZip } from "./smm-hash"; // from the Hashing section
export async function profileCustomerFile(path: string) {
const rows: Record<string, string>[] = parse(readFileSync(path), { columns: true });
const records = rows
.map((r) => ({
email: hashEmail(r.email),
phone: hashPhone(r.phone),
namezip: hashNameZip(r.first_name, r.last_name, r.zip),
}))
.map((r) => Object.fromEntries(Object.entries(r).filter(([, v]) => v)))
.filter((r) => Object.keys(r).length > 0)
.slice(0, 100_000); // one run is at most 100,000 records
const res = await fetch("https://supermegamind.com/api/v1/profiles", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SMM_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ records }),
});
return res.json(); // give the model this: matched, attributes, claimUrl, usage
}One run, one profile
A run is one call to profile_list with up to 100,000 records, and it returns one profile and one report. There is no batching during the pilot: runs are not combined. To profile the next 100,000 records, start a new run, which gives a separate profile.
Tools
describe_attributes
Lists every attribute a profile can return: name, label, category, kind, and values (code to label) where the attribute is coded. No input. Not metered.
profile_list
Profiles one list. Input is records, an array of 200 to 100,000 objects. Each carries at least one of email, phone, namezip, and every value is a 64-character lowercase hex SHA-256.
{
"records": [
{ "email": "1a7694688f8c5a79b1adb2bdd23937bea3c3ebd27ab4108fa5190c105c21a3f2" },
{ "phone": "<sha256>", "namezip": "<sha256>" }
]
}Output, trimmed to one attribute:
{
"slug": "q3Xk9aLm",
"claimUrl": "https://supermegamind.com/r/q3Xk9aLm/claim#t=...",
"claimExpiresAt": "2026-10-21T18:04:11.000Z",
"uploaded": 1200,
"matched": 948,
"matchRate": 79,
"recordsBilled": 1200,
"usage": { "used": 412000, "limit": 500000, "remaining": 88000,
"resets": "2026-11-01T00:00:00.000Z", "overageEnabled": false },
"attributes": [
{
"field": "CAMPING_ENTHUSIAST",
"label": "Camping Enthusiast",
"category": "Enthusiasts",
"coverage": 54.9,
"medianCustomer": null,
"medianNational": null,
"values": [
{ "label": "Y", "count": 520, "pct": 54.9, "index": 457, "decoded": true,
"significant": true, "conservativeIndex": 422 }
]
}
]
}| Field | Meaning |
|---|---|
pct | Share of matched records with this value |
index | Against the national rate, where 100 is average |
significant | False means the difference can't be told apart from noise. Treat it as average |
conservativeIndex | The index at the edge of its confidence interval nearest 100. The safer number to quote |
coverage | Share of matched records with any value for this attribute |
usage | Records used this month on your key, your limit, what remains, and when it resets |
claimUrl | Opens the saved private report in a browser. Works once, within 7 days. Treat it like a password until used |
REST API
Same operations, same key, base URL https://supermegamind.com.
| Method | Path | Does |
|---|---|---|
| GET | /api/v1/attributes | describe_attributes |
| POST | /api/v1/profiles | profile_list, with the input above as the JSON body |
curl https://supermegamind.com/api/v1/attributes \
-H "Authorization: Bearer $SMM_KEY"curl https://supermegamind.com/api/v1/profiles \
-H "Authorization: Bearer $SMM_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonHashing
Each key is the lowercase hex SHA-256 of a normalised UTF-8 string. Normalisation must match exactly: a hash that is off by one character matches nobody, and we can’t tell you why, because we never see the raw value.
| Key | Normalise | Then |
|---|---|---|
email | Trim surrounding whitespace, lowercase | SHA-256 |
phone | Keep ASCII digits only. If 11 digits starting with 1, drop the 1 | SHA-256 of the 10 digits |
namezip | First and last name: uppercase, keep only A-Z and 0-9. ZIP: trim, first 5 characters | SHA-256 of FIRST|LAST|ZIP5 |
Send every key you have for a record. More keys mean more chances to match.
TypeScript
import { createHash } from "node:crypto";
const sha256 = (s: string) => createHash("sha256").update(s, "utf8").digest("hex");
export function hashEmail(email?: string | null): string | null {
const normalized = (email ?? "").trim().toLowerCase();
return normalized ? sha256(normalized) : null;
}
export function hashPhone(phone?: string | null): string | null {
let digits = (phone ?? "").replace(/\D/g, "");
if (digits.length === 11 && digits.startsWith("1")) digits = digits.slice(1);
return digits ? sha256(digits) : null;
}
export function hashNameZip(first?: string | null, last?: string | null, zip5?: string | null): string | null {
const f = (first ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
const l = (last ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
if (!f || !l) return null;
return sha256([f, l, (zip5 ?? "").trim().slice(0, 5)].join("|"));
}Python
import hashlib
import re
def _sha256(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def hash_email(email):
normalized = (email or "").strip().lower()
return _sha256(normalized) if normalized else None
def hash_phone(phone):
digits = re.sub(r"\D", "", phone or "", flags=re.ASCII)
if len(digits) == 11 and digits.startswith("1"):
digits = digits[1:]
return _sha256(digits) if digits else None
def hash_namezip(first, last, zip5):
first = re.sub(r"[^A-Z0-9]", "", (first or "").upper())
last = re.sub(r"[^A-Z0-9]", "", (last or "").upper())
if not first or not last:
return None
zip_ = (zip5 or "").strip()[:5]
return _sha256("|".join([first, last, zip_]))Check your implementation
| Key | Input | Must hash to |
|---|---|---|
email | "andy@example.com" | 1a7694688f8c5a79b1adb2bdd23937bea3c3ebd27ab4108fa5190c105c21a3f2 |
phone | "(201) 555-0123" | 5219e4c445ea51892ac95fb11e51a0c53d792f8aeecfac58f5c1d02c0e7431e5 |
namezip | "John", "Smith", "97201" | f4ee6a17a6e143645da379b79b0387e4480751b9494cfed9b74e9e3a150389f0 |
Errors
Every error has one shape. MCP tools return the same object with isError: true.
{ "error": { "code": "insufficient_matches", "message": "Fewer than 200 records matched, so no profile is returned." } }| Status | Code | Meaning | Billed |
|---|---|---|---|
| 400 | invalid_request | Bad body, raw values, or wrong size | No |
| 401 | unauthorized | Missing, malformed, or revoked key | No |
| 402 | budget_exhausted | This call would pass your monthly record budget | No |
| 422 | insufficient_matches | Fewer than 200 records matched, so no profile is returned | Yes |
| 429 | rate_limited | More than 10 calls a minute | No |
insufficient_matches doesn’t say how many records matched. On a small list, that number is itself information about the people on it.
Limits and billing
| Billing unit | Records submitted, at least 1,000 per profile call |
| Records per run | 200 to 100,000. One run is one profile, and runs are not combined |
| Matched floor | 200 matched records per profile |
| Rate | 10 profile calls a minute per key |
| Budget | Monthly, per key, resets on the 1st (UTC). Calls past it are refused and not charged, unless overage is on |
| Warnings | Emails to your billing contact at 80% and 100% of the month's records |
| describe_attributes | Free |
Plans start at $1,500 a month for 500,000 records, with higher tiers for more volume. Ask about plans.
Privacy and terms
- We receive hashes, never raw emails, phones, or names.
- Output is aggregate only. No record about any individual is ever returned.
- Sensitive attributes (ethnicity, religion, political donor flags, credit, income, net worth, and date of birth) are never available through the API.
- Every call is logged against its key, as counts and ids only.
- Every key is issued under terms that prohibit re-identification attempts, eligibility decisions (credit, employment, housing, insurance), and political use.
See also the privacy policy and terms. Questions: help@supermegamind.com.
