One call. Your agent meets the person instead of a stranger. It carries their word, their rhythm, and how to meet them, always with their consent. The pressure points never leave the person's own read. Patent pending. Yours to import.
| Mode | What it returns | When to use it |
|---|---|---|
system_prompt | Claude-ready system addendum tuned to the user. | Prepend at every conversation start. |
identity_token | ES256 JWT carrying word, rhythm, force. Offline-verifiable, and revocable by the person. | Hand-off between services, minted from the person's own signed-in session. Verify against /.well-known/jwks.json or with /api/identity-verify. For a first-party app that needs its own consent screen, use Sign in with Pupul below. |
scrub | Voice-clean text. No em-dashes, no AI tells. | Run on every model output before showing the user. |
draft | Rewrite text in the user's compressed voice. | When the user asks the agent to write something on their behalf. |
If your agent can do an HTTP request, you can use Noctara.
curl -X POST https://noctaracorp.com/api/mcp/system_prompt \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","style":"full"}'
Returns:
{
"ok": true,
"system_prompt": "# Identity context (Noctara behavioral read).\n\nYou are speaking to one person...",
"has_read": true
}
No account needed. Send the person's answers, with their consent, and get the read back. This is the embed path. one call, nothing stored.
curl -X POST https://noctaracorp.com/api/read \
-H "Content-Type: application/json" \
-H "X-Read-Key: YOUR_KEY" \
-d '{"consent": true, "answers": ["...", "..."]}'
Returns:
{
"ok": true,
"read": { "word": "ARCHITECT", "force": "Independence", "rhythm": "Builder",
"posture": "...", "mirror": "...", "half_truth": "...",
"departure": "...", "risk_line": "...", "lever": "..." }
}
This one endpoint is the exception to self-serve: /api/read runs on a manually issued partner key, so email hello@noctaracorp.com for one. The person being read must consent in your app first. If what you want is a read of a person who already has one, take the self-serve OAuth rail below instead. no email, no key request.
When you do not want the person's contents, only whether there is a person there. This answers four questions from the accruing record and shows the working behind every one.
Run it right now, no key, no signup. This returns the exact response shape from the same code paths, over a fixture record that says so in its first field:
curl "https://noctaracorp.com/api/v1/registry-demo"
Real records need a key and the person's recorded consent to your org:
To test the authed happy-path without a real consented client, query the reserved sandbox tag with your key. It returns a labeled demo:true fixture with visible:true and full claims, so you can wire and parse a positive response before a real person consents:
curl -H "Authorization: Bearer noctara_lk_YOURKEY" "https://noctaracorp.com/api/v1/registry?tag=sandbox"
curl "https://noctaracorp.com/api/v1/registry?tag=CLIENT_TAG" \ -H "Authorization: Bearer noctara_lk_..."
Returns:
{
"ok": true, "tag": "flmd7", "visible": true,
"claims": {
"is_human": true, "is_continuous": true,
"has_deviated": false, "stands_behind": true
},
"provenance": {
"beats": 2, "distinct_weeks": 2, "sources": ["tap_in","palace"],
"first_beat": "...", "last_beat": "...", "drift_observations": 0,
"thresholds": { "is_human": ">= 2 beats, or a measured live typing session",
"is_continuous": ">= 2 distinct weeks" },
"liveness": { "basis": "keystroke_liveness_v1", "score": 0.81,
"calibration": "synthetic corpora; conservative threshold; ..." }
},
"consent": { "granted": true, "scope": "coach:17", "granted_at": "...",
"revocable_at": "https://noctaracorp.com/consent" }
}
A claim we cannot evidence is null, never false. This is the most important line on this page. false means we looked and it is not so. null means we do not know. If our ledger read fails you get a 503 and no claims at all, with a note saying so, because answering is_human: false off a broken query would be us telling you a real person is not one. Expect nulls, and treat them as "insufficient record", never as a negative.
is_human is a presence signal, not a biometric verdict. It says this record holds beats of a kind a present person produces, across more than one sitting, above a threshold we publish in the response so you can judge it yourself. We will not dress that up as identity proof.
The liveness provenance, when present, means the record carries a measured typing session and our scorer read it as live. The scorer's verdicts are live or null, never negative: its calibration basis is stated inside the response, and until real-stream calibration earns published error rates we will not tell you a person is not one. Liveness can only ever add evidence to is_human; a record without it falls back to the beat threshold exactly as before.
No grant, no claims. If the person has not granted your org you get visible: false and nothing else. Not a reduced set, not nulls with hints. They can revoke at any time and this endpoint stops answering the moment they do.
What it costs. Free through 1,000 active records a month, then five cents per active record per month, no minimum and no contract. An active record is one distinct person your key asked about at least once that month, counted once no matter how many times you ask, so re-checking on every session costs nothing extra.
Check the meter yourself. GET /api/v1/usage with the same key returns your active-record count for the month, the free ceiling, the rate, and what we think you owe, along with the working behind it. We count only calls that actually returned claims: a call we refused for want of consent, or could not complete, is logged but never billed. If we cannot read your usage we return 503 and withhold the number rather than showing you a zero we did not verify.
curl -H "Authorization: Bearer noctara_lk_..." https://noctaracorp.com/api/v1/usage
{ "active_records": 2, "free_included": 1000, "billable_records": 0,
"rate_usd_per_active_record": 0.05, "estimated_usd": 0,
"basis": { "unit": "one distinct person you asked about at least once this month,
counted once no matter how many times you ask",
"privacy": "we count salted one-way tokens, not identities" } }
What it cannot answer yet. Measured 2026-08-02: 39 people across our whole base carry the two or more beats is_human requires, and four of five real consented records answered all-nulls. The endpoint is correct and the records under it are thin. The version of this worth paying for is the one where your own users have accrued something worth grading, which takes weeks of them showing up.
Every registry response carries a permitted_use block, and the boundary is not optional. This is a continuity signal, not a consumer report. You may use it to check that the same continuous, consenting identity stands behind an account, a message, or an action. You may not use it for any FCRA-covered decision (credit, employment or hiring, housing, insurance), for background checks, character or risk scoring, or for any purpose the person did not consent to. Noctara is not a consumer reporting agency and these claims are not a consumer report. The person can revoke at any time, and the endpoint stops answering the moment they do. Full terms at /terms.
One block in your MCP config and the eighteen Noctara modes are tools your model can call.
{
"mcpServers": {
"noctara": {
"command": "npx",
"args": ["-y", "noctara-mcp-server"],
"env": {
"NOCTARA_API_BASE": "https://noctaracorp.com/api",
"NOCTARA_USER_EMAIL": "user@example.com",
"NOCTARA_CLIENT_ID": "your-app"
}
}
}
}
Full Claude install guide covers Desktop, Code, Cursor, Cline, and OpenClaw with the exact paths for each.
If you want a typed wrapper instead of raw fetch, Python has one:
pip install noctara
from noctara import Noctara n = Noctara sp = n.system_prompt(email="user@example.com")
The SDK is MIT-licensed. The patent does not encumber the SDK, only the underlying behavioral primitive.
There is no npm package yet. In TypeScript, call the endpoint directly; it is one fetch, and the Claude example below shows the whole shape.
import Anthropic from "@anthropic-ai/sdk";
async function noctaraSystemPrompt(email: string) {
const r = await fetch("https://noctaracorp.com/api/mcp/system_prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, style: "full" }),
});
const j = await r.json();
return j.system_prompt as string;
}
const claude = new Anthropic;
const identity = await noctaraSystemPrompt("user@example.com");
const msg = await claude.messages.create({
model: "claude-opus-4-7",
max_tokens: 1024,
system: identity + "\n\n# Your task.\nYou are a helpful agent for this person.",
messages: [{ role: "user", content: "What should I do tomorrow?" }],
});
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
const identity = await noctaraSystemPrompt(userEmail);
const { text } = await generateText({
model: anthropic("claude-opus-4-7"),
system: identity,
prompt: userMessage,
});
import httpx
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage, HumanMessage
def noctara_system_prompt(email: str) -> str:
r = httpx.post("https://noctaracorp.com/api/mcp/system_prompt",
json={"email": email, "style": "full"})
return r.json["system_prompt"]
llm = ChatAnthropic(model="claude-opus-4-7")
identity = noctara_system_prompt("user@example.com")
resp = llm.invoke([
SystemMessage(content=identity),
HumanMessage(content="What should I do tomorrow?"),
])
If the person is your user and the token has to leave your walls, use the OAuth rail rather than the MCP token below. It carries its own consent screen and its own client registration, so you never have to hold the person's Noctara session. Access tokens from /api/oauth/token are signed ES256 (P-256) by api/_lib/sign.js, and the public half is published at /.well-known/jwks.json. Any third party verifies a Noctara token offline, with the public key alone, never calling us and never holding a secret of ours. Discovery lives at /.well-known/oauth-authorization-server.
curl -X POST https://noctaracorp.com/api/oauth/register \
-H "Content-Type: application/json" \
-d '{"app_name":"My AI App","email":"you@yourapp.com",
"redirect_uris":["https://yourapp.com/callback"]}'
# -> { "ok": true, "client_id": "app_...", "client_secret": "psk_..." }
# then ordinary OAuth: /api/oauth/authorize -> /api/oauth/token -> /api/pupul-context
Self-serve, no sales call, no human in the loop. First 1,000 reads a month are free per app. Full integration docs: pupulcorp.com/identity/docs.
Which crypto is which, plainly. identity_token is an ES256 JWT, signed with the same issuer key as the OAuth rail above, so anyone verifies it offline against /.well-known/jwks.json with the public key alone. There is no shared signing secret. Every token carries a jti, is recorded as a grant on the person's own consent page at /consent, and stops verifying the moment they close that door. /api/identity-verify still accepts it if you would rather we check, and that path checks the revocation list too.
The mint requires the person's own signed-in session. Your server cannot mint a token for a user it merely names, because a token that asserts someone's identity can only be created by that someone. Send the session that person is already holding, as an Authorization: Bearer header, a session_token field in the body, or the same-origin noctara_token cookie. The email or mark you pass has to be the one that session belongs to. Anything else returns 401 unauthorized, including a call carrying only an email.
If your stack has more than one service, mint an identity_token once at the edge the person signed in at, and verify it everywhere else.
// service A: mint, from the session the person is already holding.
// A call with only an email and no session returns 401.
const r = await fetch("https://noctaracorp.com/api/mcp/identity_token", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${userSessionToken}`,
},
body: JSON.stringify({ email: "user@example.com", audience: "my-agent" }),
});
const { token, jti, expires_at } = await r.json();
// service B: verify. Offline against the published JWKS with any ES256 JWT
// library, or ask us, which also checks the jti against the revocation list.
const v = await fetch("https://noctaracorp.com/api/identity-verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const { valid, claims } = await v.json();
// claims.word, claims.rhythm, claims.force, claims.sub, claims.jti
Every model output runs through this before it hits the user.
const r = await fetch("https://noctaracorp.com/api/mcp/scrub", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: modelOutput }),
});
const { text } = await r.json();
Strips em-dashes, en-dashes, smart quotes, ellipsis chars, and the most reliable AI tells. The cleanup is invisible until your user notices their agent stopped sounding like an LLM.
The SDK and reference implementation are on GitHub at github.com/noctara/sdk (MIT). The behavioral identity primitive itself is covered by a US provisional patent application (Systems and Methods for Behavioral Biometric Identity Assessment and Authentication). Non-commercial use is free. Commercial use at scale: calkire@noctaracorp.com.
The registry API is versioned in the path (/api/v1/...). The response shape is the contract: we add fields, we do not remove or rename them within a version. A breaking change means a new version number, and the old version keeps serving.
The promise: when a version is deprecated, we give a minimum of 90 days notice before it stops serving, announced here and by email to every registered key. You will never wake up to a silently changed contract.
liveness.basis, score, verdict (live or null, never negative), with its honest calibration basis stated inline. Additive; existing consumers unaffected.GET /api/v1/registry-demo, no key, no signup, returning the exact production shape over a labeled fixture.Every conversation with your AI starts cold. Yours doesn't have to.