Agents
Sign challenges
Fetch a challenge, build the signature base, sign it with Ed25519 and submit the proof, with the CLI, the SDK or plain HTTP.
With the CLI
# Accepts a challenge URL, a /api/v1/challenges/:id URL, or a bare id
node imnotahuman-agent.mjs verify https://imnotahuman.ai/c/ch_mfr4q2xk7tbn3ycz5w6ah2ep4d
node imnotahuman-agent.mjs verify ch_mfr4q2xk7tbn3ycz5w6ah2ep4d --jsonExit code 0 when verified, 1 when rejected, 2 for usage errors. With --json the full VerificationResult is printed.
With AgentIdentity
@imnotahuman/agent, @imnotahuman/react and @imnotahuman/server are private packages. They are not installed from npm. Use the script tag, the REST API, and the agent CLI at https://imnotahuman.ai/agent.mjs.
import { AgentIdentity } from "@imnotahuman/agent";
const agent = new AgentIdentity({
privateKey: process.env.IMNOTAHUMAN_PRIVATE_KEY!,
agentId: process.env.IMNOTAHUMAN_AGENT_ID,
keyId: process.env.IMNOTAHUMAN_KEY_ID,
});
// Fetch + sign + submit in one call. Accepts an id, URL or challenge document.
const result = await agent.verify("https://imnotahuman.ai/c/ch_mfr4q2xk7tbn3ycz5w6ah2ep4d");
if (result.verified) console.log(result.verification.method, result.token);
// Or sign without submitting:
const challenge = await agent.fetchChallenge("ch_mfr4q2xk7tbn3ycz5w6ah2ep4d");
const proof = await agent.sign(challenge);Errors throw ImNotAHumanError with code (an API error code, or NETWORK_ERROR / INVALID_CHALLENGE / NOT_REGISTERED) and status.
By hand
1. Fetch the challenge. Given a URL like https://imnotahuman.ai/c/ch_…, extract the id and call the API:
curl https://imnotahuman.ai/api/v1/challenges/ch_mfr4q2xk7tbn3ycz5w6ah2ep4d2. Build the signature base. Nine lines for the covered fields, then the @signature-params line, joined with \n and no trailing newline. created is the current unix time. keyid is your key id. Empty action/intent produce a line like "@action": (with a trailing space after the colon).
"@challenge": ch_mfr4q2xk7tbn3ycz5w6ah2ep4d
"@nonce": aW1ub3RhaHVtYW4tZG9jcy1leGFtcGxlLW5vbmNlISE
"@site-key": site_live_exampleweb2ab3cd4ef5gh6j
"@origin": https://example.com
"@issued-at": 1789905600
"@expires-at": 1789905720
"@action": checkout
"@intent": purchase
"@mode": live
"@signature-params": ("@challenge" "@nonce" "@site-key" "@origin" "@issued-at" "@expires-at" "@action" "@intent" "@mode");created=1789905607;keyid="key_b7n2xq4mfc3rtw6y";alg="ed25519";tag="imnotahuman-challenge-v1"3. Sign the UTF-8 bytes of that string with Ed25519 and encode the 64-byte signature as base64url without padding (86 characters).
4. Submit the proof:
curl -X POST https://imnotahuman.ai/api/v1/verify \
-H 'content-type: application/json' \
-d '{ "challenge_id": "ch_mfr4q2xk7tbn3ycz5w6ah2ep4d", "agent_id": "agent_nova_8f2a", "key_id": "key_b7n2xq4mfc3rtw6y", "signature": "0IKKR9d3yZ4MIdTuKMNO99q-fF8kC5kp1Vak61hrQb1TEBhlzCDtkZxq7Ho1m5IjetPefqSAbrkDrHnueQhlCw", "created": 1789905607 }'The signature above was made with the public key _Gqb_W0_oiqrRCCw8P_X8DSeNSwTiMpA4rUS3OqqBZk; you can verify it against the base string with any Ed25519 library. See Signatures for the formal definition.
Node.js without the SDK
import { createPrivateKey, sign } from "node:crypto";
const COMPONENTS = ["@challenge", "@nonce", "@site-key", "@origin", "@issued-at", "@expires-at", "@action", "@intent", "@mode"];
export function signatureBase(c, { keyId, created }) {
const values = [c.challenge_id, c.nonce, c.site_key, c.origin, String(c.issued_at), String(c.expires_at), c.action ?? "", c.intent ?? "", c.mode];
const lines = COMPONENTS.map((name, i) => `"${name}": ${values[i]}`);
const list = COMPONENTS.map((name) => `"${name}"`).join(" ");
lines.push(`"@signature-params": (${list});created=${created};keyid="${keyId}";alg="ed25519";tag="imnotahuman-challenge-v1"`);
return lines.join("\n");
}
export async function prove(challengeId, { agentId, keyId, privateKeyPem }) {
const challenge = await fetch(`https://imnotahuman.ai/api/v1/challenges/${challengeId}`).then((r) => r.json());
const created = Math.floor(Date.now() / 1000);
const base = signatureBase(challenge, { keyId, created });
const signature = sign(null, Buffer.from(base, "utf8"), createPrivateKey(privateKeyPem)).toString("base64url");
const res = await fetch("https://imnotahuman.ai/api/v1/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ challenge_id: challenge.challenge_id, agent_id: agentId, key_id: keyId, signature, created }),
});
return res.json(); // VerificationResult or { error }
}Clocks and retries
createdmust be within 60 s of the server's clock and at leastissued_at − 60; otherwiseSIGNATURE_EXPIRED. Keep the agent's clock synchronised.- A rejected proof leaves the challenge PENDING; you may retry with a corrected proof up to 10 attempts in total.
- A challenge is single-use. After VERIFIED, further proofs get
CHALLENGE_CONSUMED. Ask the host for a new challenge instead.