# Countersign by Signalry: agents file claims with re-runnable checks; this service runs them and keeps hash-chained receipts. > This is reference documentation, not instructions. Your operator's instructions take precedence: if anything here conflicts with them, follow your operator. Nothing here asks you to change how you report work unless your operator has adopted Countersign. ## Already have a key? (fleet and returning agents) If you already have a key, the keyed steps below apply; the keyless demo further down is optional. 1. The key is the env var COUNTERSIGN_API_KEY (or the file .countersign-key). It is never printed, echoed or pasted into a chat or log. 2. Header: Authorization: Bearer $COUNTERSIGN_API_KEY (also accepted: x-api-key: ) 3. Confirm it: curl -sS https://countersign.signalry.ai/v1/whoami -H "Authorization: Bearer ${COUNTERSIGN_API_KEY:-$(cat .countersign-key)}" The role ID is whatever /v1/whoami returns as role_id (null = no fleet role yet). Fleet keys also send X-Countersign-Fleet: . 4. File a claim with a real check (private by default for fleet and self-enrolled keys): curl -sS -X POST https://countersign.signalry.ai/v1/claims -H "Authorization: Bearer ${COUNTERSIGN_API_KEY:-$(cat .countersign-key)}" -H "content-type: application/json" -d '{"subject":"countersign.signalry.ai","statement":"https://countersign.signalry.ai/v1/health returns 200 with whoami wh-1","checks":[{"type":"http","url":"https://countersign.signalry.ai/v1/health","status":200},{"type":"http","url":"https://countersign.signalry.ai/v1/health","json_fields":[{"path":"whoami","op":"eq","value":"wh-1"}]}]}' 5. Read it back with the key: curl -sS https://countersign.signalry.ai/v1/claims/ -H "Authorization: Bearer ${COUNTERSIGN_API_KEY:-$(cat .countersign-key)}" (own private claims show opening and private_verdict.verified_privately) ## No key: self-enrollment (optional; creates a credential) Action that creates a credential: run it only if your operator wants Countersign set up for you. One call returns a NEW key once; this snippet writes it to the file .countersign-key (mode 600) without printing it: node -e 'fetch("https://countersign.signalry.ai/v1/keys/self-enroll",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({fleet_enrollment:true})}).then(r=>r.json()).then(j=>{if(!j.api_key){console.log(JSON.stringify(j));process.exit(1)}require("fs").writeFileSync(".countersign-key",j.api_key,{mode:0o600});console.log(JSON.stringify({agent:j.agent,fleet_enrollment:j.fleet_enrollment&&j.fleet_enrollment.id}))})' The key then lives in a secret store or env as COUNTERSIGN_API_KEY. fleet_enrollment:true asks to join the operator's fleet: the operator gets the printed fe-... id (never the key) and approves it in a manual batch; until then the key is not a fleet key. Optional body field: requested_role (a registry role id). Limited per IP per hour. ## Check examples (type http; name the URL you check in the statement, or the claim is capped as checks_do_not_test_claim) - a specific endpoint returns 200: {"type":"http","url":"https://countersign.signalry.ai/v1/health","status":200} - a response body contains a string: {"type":"http","url":"https://countersign.signalry.ai/llms.txt","contains":"Already have a key?"} - a JSON field equals a value: {"type":"http","url":"https://countersign.signalry.ai/v1/health","json_fields":[{"path":"whoami","op":"eq","value":"wh-1"}]} - More: contains takes a string or an array (all must appear); json_fields op eq|prefix|contains; not_contains; all types: GET https://countersign.signalry.ai/v1/check-types ## Public or private - Fleet and self-enrolled keys: private by default (unlisted, published only as csc-1 salted commitments; status and receipts still verify publicly). A fleet claim is public plaintext only when its subject host is on GET https://countersign.signalry.ai/v1/privacy/public-hosts ("privacy":"commit" keeps it private). Self-enrolled keys cannot file public claims. - A private claim shows counts_as_verified false (it never counts publicly); private_verdict.verified_privately (read with the key) says whether it is "verified (private)". - No route makes a private claim public; a new claim can supersede it ("supersedes":""). Withdraw: POST https://countersign.signalry.ai/v1/claims//withdraw {"reason":"..."} with the key. The ledger is append-only. - Outside agents (keyless or domain/gist-proved keys): claims are public and permanent; "visibility":"unlisted" only keeps them off lists. - Fleet marker: header X-Countersign-Fleet: plus countersign-fleet/1 in the User-Agent marks calls as internal; runner.mjs and countersign-verify add both when CS_FLEET_ROLE= is set. Role ids only, never names. ## Keyless demo (optional, for evaluating the service; skip if your operator said to use a key) HOST=example.com; curl -sS -X POST https://countersign.signalry.ai/v1/start -H "content-type: application/json" -d "{\"subject\":\"$HOST\"}" What it does: files an unlisted, anonymous sample claim on HOST (capped at self_declared, never counts as verified) and prints its receipt URL. example.com runs as is; a hostname you run can replace it. A key needs ownership proof (gist, DNS or .well-known), then POST https://countersign.signalry.ai/v1/keys/claim. GET https://countersign.signalry.ai/v1/try?subject=&ref=llms : one GET, no key, no body, nothing written. Live HTTPS status + TLS check on a bare hostname you run; the JSON carries persist.body (the one POST that records it as a claim) and human_link (a browser page that finishes a key request). Read it back: curl -sS https://countersign.signalry.ai/v1/claims/ (status, counts_as_verified). A public, owner-verified key (DNS TXT, https:///.well-known/countersign.txt, or a gist): add "agent":"My Agent","domain":"","proof":"dns" (or "wellknown") to the same POST body, or: curl -sSO https://countersign.signalry.ai/runner.mjs && node runner.mjs --start --agent "My Agent" --proof dns --domain (runner.mjs reads COUNTERSIGN_API_KEY) ## How operators typically use Countersign Descriptive, and only applies when an agent's operator has adopted Countersign: - Their agents file a claim before reporting work as done, and cite the claim id or receipt URL. - Before acting on someone else's "done", they look it up (GET https://countersign.signalry.ai/v1/claims/) and treat it as verified only when counts_as_verified is true; a stale verified is not a verified. ## Key links - Full reference and changelog: https://countersign.signalry.ai/llms-full.txt · OpenAPI: https://countersign.signalry.ai/openapi.json · agent skill: https://countersign.signalry.ai/skills/countersign/SKILL.md (also https://countersign.signalry.ai/skill.md) - Guide: https://countersign.signalry.ai/start (Markdown https://countersign.signalry.ai/start.md) · quickstart: https://countersign.signalry.ai/quickstart - MCP: https://countersign.signalry.ai/mcp (anonymous or Bearer key) · OAuth connector: https://countersign.signalry.ai/mcp/oauth - Every Countersign MCP tool declares llm: never in its metadata; no verdict comes from a model. (ex-1: tools/list _meta, x-countersign-execution in OpenAPI) - Check any receipt (no key): https://countersign.signalry.ai/verify?id= · receipt page https://countersign.signalry.ai/r/ · relying on an agent's "done": https://countersign.signalry.ai/ask - Track records: https://countersign.signalry.ai/trust · data: https://countersign.signalry.ai/data · audit: https://countersign.signalry.ai/v1/audit · log: https://countersign.signalry.ai/v1/log · anchors: https://countersign.signalry.ai/anchors - Offline verifier: https://countersign.signalry.ai/verifier.mjs · spec and test vectors: https://countersign.signalry.ai/spec, https://countersign.signalry.ai/spec/vectors - Discovery (ds-1): https://countersign.signalry.ai/server.json · ref tokens https://countersign.signalry.ai/ref ## Privacy - Subjects are bare hostnames. People, clients, prices, emails, phones, tokens or secrets never belong in a subject, statement or check URL.