File DA-042 View as Markdown
REST API
Every public Double Agent route, with the key it needs and what it returns. Most sites only need the script and token checks on the server. Come here when you call the API yourself or export data.
| You want to | Section |
|---|---|
| Send a beacon, get or verify a token, report from your server or edge | Collection and tokens |
| Read your site's stats, sessions, visitors and threats | Website data |
| Report agents acting for your customers, and audit what they did | Caller identity |
| Read what visitors are trying to do | Intent |
| Read your public agent's calls, tasks and revenue | Agent analytics |
| Look up agents in the public directory | Agent directory |
| Manage accounts, sites, domains and keys | Accounts and auth |
Auth
Base URL: https://api.doubleagent.so. Errors look like { "error": { "code", "message" } }.
| Auth | Header |
|---|---|
| none | – |
| pk | ?k=pk_… (your public key) |
| session | Authorization: Bearer das_… (portal or CLI login) |
| sk | Authorization: Bearer sk_… (your secret key, server only) |
Rate limits: Security. Routes the portal uses for team members, billing, usage, account closure and Search Console management are not listed here.
Collection and tokens
| Route | Auth | Returns |
|---|---|---|
POST /v1/collect | none (keyless, by Origin) or pk | 204 (the SDK beacon) |
POST /v1/check | pk | { verdict, token, intent }; keyless → 401 key_required. intent is this beacon's (Intent), never in the token |
POST /v1/ping | pk | Live presence |
POST /v1/verify | none | { token } → { valid: true, claims }, or { valid: false, error }. For servers that can't verify the token themselves (Server) |
GET /.well-known/jwks.json | none | Signing keys |
GET /v1/install-check?url= | none | Install report (see /install.md step 5) |
POST /v1/hits | sk (server-side only), verified site | { accepted }, 202. Body { v: 1, hits: [...] }, at most 100 per batch. Each hit: ts (ms, within the last hour), m (method), h (host), p (path, no query), s (status), and optionally ua, ip, hd (up to 24 lowercase request headers; never cookies or credentials), cf (country, region, city, lat, lon, asn, org, tls, http) and t (the page's ticket nonce) and qf (up to 8 query pattern codes: sqli, xss, trav, ssrf, cmd, tpl; never the query). Report from your edge or middleware, after the response is sent. 503 hits_unavailable means drop the batch |
POST /v1/evaluate | sk | Report an outcome from your server and get a fresh verdict and token. See Evaluate |
GET /v1/visitor-config?k= | none or pk | { vid }: on, consent or off, whether this browser may keep a visitor id. The SDK calls it once per session |
GET /v1/signatures/:file | none | The engine's signature bundle, <version>.json. latest.json redirects to the current one |
Collection telemetry: POST /v1/collect answers with header DA-Telemetry-Accepted: 0 when the event could not be queued for storage (detection still succeeded). POST /v1/check always includes telemetryAccepted: true|false in its JSON. Both only say whether storing the session was queued; they never affect the verdict.
Website data
These routes answer 403 site_not_verified until the site has a verified domain (Claim a domain). Add ?data=test to read only localhost sessions, which works before the domain is verified.
| Route | Auth | Returns |
|---|---|---|
GET /v1/stats?from&to | sk, or session + ?site=st_… | Totals, series, families, profiles and breakdowns per class. breakdowns.campaign: the top 200 source | medium | campaign rows (first touch; untagged visits use the referrer host or (direct), the channel and (not set)), each with source, medium, campaign, agent_verified and paid, the rest summed as (other). breakdowns.click_id: visits per ad-click id. coverage_from (unix seconds) when part of the range predates campaign rows. sources=0 leaves out breakdowns.campaign, breakdowns.click_id and coverage_from, and answers faster on long ranges |
GET /v1/stats/campaign?from&to&source&medium&campaign | sk, or session + ?site= | One campaign from sessions (range ≤ 90 days, quarantined traffic excluded): totals and breakdowns by utm_term, utm_content, entry_path, ref_host, country, click_ids, agent_id and operator, each split by class. source, medium and campaign are a campaign row's parts: medium and campaign 1–100 characters, source up to 255 (a referrer host). Source and medium match in any case; the response gives the parts as stored |
GET /v1/stats/friction?from&to&class | sk, or session + ?site= | How visitors struggle, from sessions that carry interaction measures (SDK 0.7.0 and later, range ≤ 90 days, quarantined traffic excluded): totals, page_types and the 50 busiest paths, each with sessions, rates per state (for example frustrated, confused), cue_rate, means and conversion, plus the definitions. class: human (default), bot, agent or all |
GET /v1/sessions?limit&class&cursor&quarantined&since&until | sk, or session + ?site= | { sessions, nextCursor }. since and until are unix seconds: sessions updated at or after since and created before until, so together they list the sessions active in a range |
GET /v1/sessions/:sid | sk, or session + ?site= | One session, as on the portal's session page (The portal) |
GET /v1/sessions/:sid/searches | sk, or session + ?site= | Likely searches for an organic Google visit, from Search Console |
GET /v1/sessions/:sid/raw | sk, or session + ?site= (Owner or Admin) | { sid, retentionDays: 30, raw }: up to 20 raw rows for one session, with the raw IP, full user agent, request headers and Cloudflare facts. Kept 30 days. Every read is written to the site's audit log. Other roles get 403 forbidden |
GET /v1/visitors?from&to&limit&cursor | sk, or session + ?site= | { visitors, total, nextCursor }: visitors with a session in the window (default the last 30 days, at most 90), newest first, limit ≤ 200 |
GET /v1/visitors/:visitorKey/journey | sk, or session + ?site= | { sessions, truncated }: one visitor's retained sessions in order, each with its channel, campaign tags and pages |
GET /v1/hits/stats?from&to | sk, or session + ?site= | Edge hits (requests your edge adapter reported): total, classes (bot, agent, browser), agents (catalog id, class, operator, behaviour, hits and identity counts: signed_claim, ip, declared, spoofed, none), statuses, top non-browser paths, robots (robots.txt readers), unknown user-agent tokens and a series. Agents and unknown tokens carry probe: their hits with a probing intent. The portal shows them under Crawlers |
GET /v1/threats?from&to | sk, or session + ?site= | Hostile activity, probing, scraping, credential attacks and floods (range ≤ 90 days, Intent): summary (requests, sessions, actors, last_seen_at, risk), series, paths (with evidence), networks, countries, matrix (class × intent), actors (edge clients by network and user agent, with max_not_found, max_requests and max_pages in one 10-minute window and their intent), recent hostile sessions, and edge.reporting (false: no edge adapter reports, so scanners that never run JavaScript are not seen) |
GET /v1/threats/labels?account | session | { labels }: each of your sites with hostile activity in the last 7 days (site, label: "probing_seen", last_seen_at, requests, sessions, high) |
GET /v1/live?window=60 | sk, or session + ?site= | Live now. last_ping is the newest ping in the past hour (unix seconds), or null: pk_live_ keys ping only with data-live |
GET /v1/sessions/:sid/events?limit&cursor | sk, or session + ?site= | { events, nextCursor }: every recorded revision of one session (collect, check, judge, join), limit ≤ 200 |
POST /v1/sessions/:sid/feedback | session + ?site= (any member; a secret key gets 403 session_required) | { feedback: { mine, latest, count } }. Body { label, source }: label is human, bot or agent; source is dossier, sessions_table or live_table. Appends a label with a server-side snapshot of the verdict. Labels never change a verdict. 404 session_not_found while the session isn't stored yet; 409 feedback_limit once it has 500 labels |
GET /v1/sessions/:sid/feedback | session + ?site= | { entries, feedback }: every label, oldest first: label, at (unix seconds), user_id, mine (yours), role, source. No snapshots or emails |
GET /v1/events/export?limit&cursor | sk, or session + ?site= | { events, nextCursor }: all events for the site, oldest first, limit ≤ 200 (default 100) |
Every session is stored, humans included, and appears in these routes within about 30 seconds. /v1/stats accepts ranges up to 365 days. After a domain claim merges keyless history, data routes answer 503 claim_pending (retry shortly) until it is consolidated.
Filter sessions by campaign with ?utm_source=, ?utm_medium=, ?utm_campaign=, ?utm_term=, ?utm_content=, ?click_id= (for example gclid) or ?source_key= (a campaign row's key, URL-encoded, up to 512 characters; other filters take up to 256). utm_source, utm_medium and the source and medium of a source_key match in any case.
Filter by how the visit arrived with ?channel= (values), and by intent with ?intent= (any intent code, for example task or scrape), ?intent_risk= (low, medium, high) or ?intent_evidence= (an evidence code).
Each GET /v1/sessions row carries intent (or null for sessions stored before it) and feedback: { mine, latest, count }: the caller's own label (null for a secret key), the latest label anyone gave, and how many there are. It is null when labels can't be read; the list still loads.
Channels
visitor.channel is Google Analytics 4's default channel group, worked out from the first touch: paid_search, paid_social, paid_video, paid_shopping, paid_other, display, cross_network, search (Organic Search), social (Organic Social), organic_video, organic_shopping, ai_assistant, email, referral, affiliates, audio, sms, mobile_push, direct, internal (from your own site) and unassigned. Sessions stored before 2026-10-07 may carry paid, every paid visit before the split.
Session detail
GET /v1/sessions/:sid also carries the landing title, what the visitor searched on your site and basic ecommerce:
| Field | |
|---|---|
visitor.entry_title | The landing page's title (SDK 0.9.0 and later) |
journey.site_searches | Terms searched on your site, in order, at most 10 (Site search) |
journey.ecommerce | product_views, item_ids, cart_adds, checkouts, purchases, purchase_value, purchase_currency, or null (Ecommerce) |
Pages in journey.pages carry search, title (first page of a page load), item (the product: item_id, item_name, price, currency), qf (query pattern codes, SDK 0.13.0) and forms (form submits by form role, SDK 0.14.0) when known.
Visit attribution
Every session, event and export row carries four roles, each with its own evidence:
| Field | |
|---|---|
visitor.agent_id, visitor.agent_evidence | Who acts: a catalog id (anthropic.claudebot) or a declared ERC-8004 name. Never an automation tool. |
visitor.operator, visitor.operator_evidence | Who runs it (anthropic). A shared published IP list proves the operator, not a product. |
visitor.controller_id, visitor.controller_evidence | What drove the visit (playwright.automation, python.requests). |
visitor.browser, browser_version, os | The client the visit presented. |
visitor.attribution_conflict | true when the declared agent's operator disagrees with the verified operator; the agent is then left unnamed. |
visitor.attribution_version | 2. Absent on older rows. |
Evidence is signed, authenticated, ip, rdns, declared, marker, detected or spoofed. visitor.identity keeps its meaning: the strongest proof for the whole visitor. Filter sessions with ?agent_id=, ?operator= or ?controller_id=; /v1/stats breaks traffic down by each.
Edge hits (/v1/hits) resolve the same roles. A shared published IP list proves only the operator, so a Chrome hit from one has identity: ip and the list's operator but no agent_id, and a declared UA from one (ClaudeBot from an Anthropic address) has detected_by: catalog, not ip_list.
Declared and signed visit identity
A visit can declare an ERC-8004 identity in its User-Agent, for example acme.shopping-assistant/1.0, with an optional registry reference in a comment: (erc8004=eip155:1:0x8004a169fb4a3325136eb29fa0ceb6d2e539a432:22). Double Agent keeps it in visitor.declared_agent. A declaration alone proves nothing.
/v1/collect and /v1/check also check an ERC-8128 request signature (a draft standard) when one is sent. It must be single-use and bound to the URI, method, body, content type and User-Agent or Origin, valid for at most 300 seconds with 30 seconds of clock skew. Plain wallets (EOAs) and deployed ERC-1271 accounts are supported.
The result is in visitor.request_auth: status (verified, invalid or unavailable), the wallet, chain, check time and an optional registry binding (owner, agent-wallet, mismatch, unavailable or not-requested) with the block. /v1/check returns it as requestAuth. null means no signature was sent. A verified signature proves the beacon request, not the browsing it reports, and it never changes the verdict. Edge hits keep declarations only. How an agent signs its requests: Verified agents.
Agents on your site
The agents that visited one site. Auth: sk, or session + ?site=. from and to cover up to 365 days.
| Route | Returns |
|---|---|
GET /v1/agents?from&to&cursor | The agents seen in the range, with sessions, classes and behaviours, their registry listing and directory link, and configured: ERC-8004 identities set on the site that have no visits yet |
GET /v1/agents/profile?agent_ref|agent_name|unknown=1&from&to&cursor | One agent's visits, aliases, registry listing and directory entry. 404 not_found when the site never saw it |
GET /v1/agents/reviews?agent_ref&reviewer&tag&status&cursor | ERC-8004 reviews of an agent this site has seen. status: active or revoked |
Caller identity
Who acted, for whom, proven how: Callers.
Evaluate
POST /v1/evaluate (sk) reports what happened on your server: { sid, subject, observationId, action, outcome, request }, plus optional caller fields. outcome is ok, auth_failed, action_denied or confirmed_trap, and request.url must be on one of the site's verified domains. The body may be up to 16 KB (a PACT proof). Example: Report what happened.
| Field | |
|---|---|
actor_kind | self (the customer acted) or agent (an agent acted for the customer) |
principal_hash | 64 lowercase hex: your own hash of the customer. Without it, Double Agent hashes subject with your account's key |
scope_required | The scopes the action needs, at most 32 OAuth scope tokens |
delegation | The grant: protocol (oauth, pact, a2a, ap2), issuer, actor, client_id, scopes, scopes_used, access (read, write), expires_at (ISO 8601), grant_id (raw, up to 256 characters, hashed with your account's key) or grant_id_hash, verification ({ status: verified|failed, by: "reporter", reason? }), proof (pact only: { kind: pact-receipt|pact-delegation|pact-agent, jws }, verified and dropped) |
A bad caller field is 400 invalid_caller, a bad delegation 400 invalid_delegation; protocol: "pap" is refused until the Personal Agent Protocol is published. The response adds caller (always):
"caller": { "level": "delegated", "verified_by": "reporter", "failures": [], "principal_hash": "3f…",
"actor_kind": "agent", "in_grant": "no",
"grant": { "protocol": "oauth", "grant_id_hash": "9a…", "scopes": ["orders:read"], "access": "read",
"expires_at": null, "status": "unverified" } }
The signed token adds lvl (the level) when you sent a caller field, and grant ({ id, protocol, access }, id being the grant id hash) when you sent a delegation. Tokens for requests without caller fields are unchanged. An action outside the grant (in_grant: "no") sends the site's caller.scope_violation webhook.
Agent events
POST /v1/agent-events (the agent's ingest key, see Record your public agent) accepts the caller fields of the recorder: counterparty.authenticated client_id, actor and scopes; counterparty.delegation; counterparty.signature with verified_by: "double_agent" and the forwarded request; access and scope_required on operation.started; insufficient_scope on operation.finished; mandate_ref on transaction.recorded. Shapes and limits: the observe README. A pap delegation, or a proof on any protocol but pact, rejects the event (invalid_counterparty).
Stored operations carry caller_level, caller_verified_by, caller_failures, principal_hash, principal_issuer, client_id, actor_id, grant_id_hash, grant_protocol, grant_scopes, grant_status, access, scope_required, required_scopes (from insufficient_scope), in_grant, scope_violation and intent_evidence.
Sessions and website Callers
Each GET /v1/sessions row and session detail carries caller (null before caller identity): level, verified_by, failures ({ code, words }), principal_hashes, actor_kind, grant (protocol, grant_ids, scopes, access, status) and in_grant. Filter sessions with ?verification_level=, ?principal=, ?grant= (64-hex hashes), ?actor_kind= or ?in_grant=.
GET /v1/callers?from&to (sk, or session + ?site=; range ≤ 90 days): { site, from, to, items, counts, truncated }, one row per actor × principal hash (≤ 500) with actor, level, verified_by, failures, actor_kind, grant, sessions, in_grant_no, intent, first_seen and last_seen (unix seconds). Filters: level, principal, grant, actor_kind, class, intent. An unknown parameter is 400.
Audit
| Route | Auth | Returns |
|---|---|---|
GET /v1/audit/search?account&principal|grant&from&to&surface&limit&cursor | session, audit.read | { items, next }: agent operations and website /v1/evaluate events for one principal or grant hash (exactly one), newest first. surface: all (default: what you may read), agent (needs agents.read) or site (needs analytics.read). from, to: ISO dates or unix times, at most 90 days (the default). limit ≤ 200 (default 50). 60 a minute |
GET /v1/audit/export?…&format | session, audit.read | The same rows as an attachment, format ndjson (default) or csv, at most 50,000 rows. Audited as audit_export. 10 a minute |
GET /v1/accounts/:account | session (any member) | { account } with audit_retention_days (null: the default) and audit_retention_caps ({ agent: 30, site: 90 }) |
PATCH /v1/accounts/:account | session, account.update | Also takes audit_retention_days: 1–90 or null. Each store keeps the smaller of it and its cap |
Agent rows: surface: "agent", at, agent_id, operation_id, counterparty_id, kind, method, target, outcome. Website rows: surface: "site", at, site, sid, event_id, actor, action, recommendation, policy_decision. Both: principal_hash, grant_id_hash, grant_protocol, grant_status, access, in_grant, scope_violation, caller_level, caller_verified_by.
Intent
GET /v1/sessions/:sid carries intent: the session's intent over all its pages, { code, confidence, risk, evidence, task_kind, purpose, source, version }, plus paths (up to 5 probe paths, for probe only) and path_count. null for sessions stored before intent. Intent is a tag: it never changes the verdict.
code is one of browse, research, read, task, support, crawl, ai_fetch, monitor, probe, scrape, cred_attack, flood or unknown. task_kind (signup, login, contact, book, purchase, download) is set for task and purpose (search, seo, training, feed, preview) for crawl; both are null otherwise. risk is none for benign intents.
GET /v1/intents?from&to (sk, or session + ?site=; range ≤ 90 days): the site's sessions in the range by class and intent, { total, rows: [{ class, intent, sessions }], kinds: [{ intent, kind, sessions }] } (kinds: task kinds and crawl purposes). Sessions stored before intent count as unknown; edge requests are in /v1/threats and /v1/hits/stats.
Agent analytics
Session auth. Every route is under /v1/managed-agents/:id, where :id is one of your account's agents (another account's agent is 404). The caller needs agents.read, except where the table says otherwise.
Shared query parameters:
env:liveortest.- The range is either
range(24h,7d,30dor90d) orfromandto(ISO-8601 instants), never both (400 invalid_range). - The comparison is
compare(prev, the default;week,month,offorcustom) andcompare_from(YYYY-MM-DD, withcompare=customonly).
Lists page with the opaque cursor from the previous page's next. A cursor is bound to the query that made it; reused with a different query it is 400 invalid_cursor. An unknown query parameter is 400.
| Route | Query | Returns |
|---|---|---|
GET /summary | shared | KPIs with change against the comparison, the chart, and coverage (complete is false while a day is still being rebuilt or events were dropped in the last 24 h) |
GET /panels | shared | Counterparty, target, task and error panels for the Overview; fetch in parallel with summary |
GET /filters | env, range | Values the screens offer as filters |
GET /operations | env, range (optional; default is everything retained), direction, kind, outcome (an outcome or errors), protocol (<name>|<version>|<binding>), protocol_name, target, conversation, cp, task (any, open, stalled, conflict or a task state), sort (newest, slowest, fastest), limit ≤ 100 (default 25), cursor; caller filters principal, grant (64-hex hashes), level, access (read, write, destructive), in_grant (yes, no, unknown), scope_violation=true | { items, next, counterparties, retained_from }. Each row (and each conversation timeline row) carries the caller fields. More than 10,000 tasks matching task is 422 refine_filter |
GET /conversations | env, range, show (all, errors, open), q, limit ≤ 100 (default 20), cursor | Conversations by last activity, with counters. At most the 1,000 most recently active are ranked; truncated is set beyond that |
GET /conversations/:conversation | cursor (timeline) | Header, a page of the timeline, its tasks with transitions, the linked conversation in the other direction, and whether content is captured |
GET /conversations/:conversation/content | – | Captured message content (agents.content.read; every read is audited) |
GET /operations/:operation/content | – | { messages } for one operation (agents.content.read; every read is audited) |
GET /counterparties | env, range, kind (agent, script, bot, human, unknown or all), q, sort (operations, errors, last_seen), order (asc, desc), limit ≤ 100 (default 25), cursor | Rows with range counts, and counts per kind |
GET /counterparties/:cp | env, range | Identifiers, activity for the range, and recent conversations; verification (level, verified_by, failures), principals (the 10 busiest, with their worst grant status), access_split and registry (the caller's directory listing: directory_id, name, url, status, signed_card; null when it has none). A merged counterparty answers 308 to its canonical one, with { canonical_id } |
GET /callers | env (required), range (default 7 days, at most 90), level, kind, principal, grant, access, in_grant, intent, q, sort (operations, errors, last_seen, risk), order, limit ≤ 100 (default 25), cursor | Callers: { items, next, counts, truncated, retained_from }, one row per counterparty × principal hash with level, verified_by, failures, client_ids, actor_ids, grant, access_split, scope_violations, intent and registry. At most 500 rows (truncated); counts per level |
GET /threats | env, range (at most 90 days) | Callers with a hostile intent: { summary, items, targets, truncated, retained_from } |
POST /counterparties/:cp/labels | body { label }: agent, human, script or bot | 201. Any member can label, and the label applies account-wide, on the canonical counterparty |
GET /tasks | shared, status (a derived status, open or all), sort (last_update, age), limit ≤ 100 (default 20), cursor | Status chips, which equal the Overview's task outcomes, and rows. Without status the list shows open tasks, or all tasks when none is open |
GET /tasks/:task | – | One task with transitions; source health only for a stalled task |
GET /transactions | env, range, currency, method, status, basis, cp, sort (time, amount), limit ≤ 100 (default 25), cursor | Transactions (agents.revenue.read) |
GET /transactions/:transaction | – | One transaction with its version history (agents.revenue.read) |
Closed days are read from daily rollups; yesterday and today are read from raw data. Test operations are listed but never counted.
PATCH /v1/managed-agents/:id (agents.manage) takes webhook_url (https) and webhook_secret (16–200 characters) for the agent's caller.scope_violation webhook; null clears either. The agent view shows webhook: { configured, url }, never the secret.
To add an agent, its sources and ingest keys, see Record your public agent.
Agent revenue
Owners and Admins hold agents.revenue.read; other roles need it granted. Without it, the routes below are 403, and the analytics routes above leave money out: no money field, and the Overview panel says revenue: { locked: true }.
Amounts are minor units of the account currency (GET /v1/me), with up to 4 decimals. Every response with money names its currency and currency_exponent. Revenue is charges plus refunds (refunds are negative), never failed ones; cost is recorded costs plus fees; margin is revenue minus cost. Money that no exchange rate covers is counted in unconverted and never summed. Money converted into a former account currency is re-converted at its own day's rate and counted in other_currency.
basis picks which money counts: settled, reported (reported and settled) or all (also estimated, the default). Transactions and costs are kept 13 months.
| Route | Query | Returns |
|---|---|---|
GET /revenue/summary | shared, basis | KPIs (revenue, cost, margin, settled_share, paying_counterparties, revenue_per_task) with change against the comparison, the chart, and splits by cost category, currency and payment method |
GET /revenue/breakdown | shared, by (tasks, the default; conversations, counterparties or tools), basis, limit ≤ 100 (default 25), cursor | items ranked by revenue, each with revenue, cost and margin. by=tasks adds without_task (money linked to no task); by=counterparties adds each party's comparison revenue and change. truncated is set past 5,000 keys |
GET /transactions | env, range (default 7 days), currency (the original currency), method, status, basis, cp, task (none: only transactions linked to no task, the breakdown's without_task), sort (time, the default, or amount), limit ≤ 100 (default 25), cursor | { items, next, counterparties, retained_from, currency, currency_exponent }. Failed transactions are listed with their status. sort=amount ranks by the amount in the account currency; amounts in another currency, or with no rate, come after |
GET /transactions/:transaction | – | One transaction with its versions, oldest first |
With agents.revenue.read, these routes also carry money:
| Route | Adds |
|---|---|
GET /operations | money on each row (null when none was recorded), and currency, currency_exponent on the page |
GET /conversations/:conversation | money: per operation (<source_id>:<operation_id>), its charges without a task and its costs; per task, its money; without_operation for money linked to no operation; truncated past 500 transactions or costs |
GET /counterparties/:cp | money: revenue, cost and margin on every basis for the range, the counterparty's merged IDs included |
GET /tasks | money on each row, and sort=revenue (without the permission, 403 forbidden) |
GET /tasks/:task | money on the task, its transactions and costs (a cost's amount is null when no rate covers it), and money_truncated past 500 of either |
A task's or operation's money adds every basis: { currency, currency_exponent, in_account_currency, revenue, cost, transactions, settlement, unconverted, other_currency }. in_account_currency is false for money converted before the account currency changed. Its transactions count stops at 64 (16 for an operation), so it can be lower than the task's transactions list.
Agent directory
Public: no key, CORS *. Each IP can make 60 requests a minute across these routes. Tighter limits apply to check (10 a minute), submissions (10 a minute) and reviews (30 a minute).
| Route | Auth | Returns |
|---|---|---|
GET /v1/directory/agents?q&skill®istry&card&status&trait&category&service&sort&facets&limit&cursor | none | { agents, total, next_cursor, facets? }. card: with (agents with their own Agent Card) or without. status: live, stale, unverified, gone, comma-separated, or active (default) or all. trait (repeatable, all must match): streaming, push_notifications, extended_card, signed, auth:none, auth:<kind>, input:<kind>, output:<kind>, a2a:<version>, transport:<binding>. category: coding, search, data, commerce, finance, travel, content, productivity, support, security, devops, health, education or other. service: the provider. registry: catalog, a2a or erc8004. sort: name (default), reputation, newest or updated. facets=1 adds counts over the whole search. limit ≤ 50 |
GET /v1/directory/agents/:id | none | One agent: registrations (with link: self, bidirectional or one_way), skills, endpoints, traits, categories, service and, where available, reputation (score is null when unknown) |
GET /v1/directory/agents/:id/versions?limit&cursor | none | { versions, next_cursor, complete_since }: how the agent's own Agent Cards changed, newest first. Each version has version_id, observed_at, agent_version and changed_paths. limit 1–50 (default 20) |
GET /v1/directory/agents/:id/versions/:versionId | none | One version: the summary plus the normalized profile, provenance, warnings and document (sha256, byte_count, media_type). Never the raw card |
GET /v1/directory/agents/:id/reviews?limit&cursor | none | { reviews, next_cursor, status }: the agent's ERC-8004 reviews, each with registration, reviewer, reviewer_agent_id, weight, value, value_decimals, tag1, tag2, revoked and feedback_index. status is unavailable when the chain could not be read. limit 1–50 (default 20) |
GET /v1/directory/check?url | none | A live diagnosis of an Agent Card URL or https origin: { card, domain_file, tokens, listing, problems }. Each problem has a code and a message that says how to fix it. 10 per minute per IP |
GET /v1/directory/stats | none | { total, agents, bots, self_identified, live, skills, new_this_week } |
POST /v1/directory/submissions | none | {url} (an Agent Card URL or https origin) → 202 { queued, url } |
GET /v1/directory/submissions?url | none | { url, state: "pending" }, or { url, state: "listed", agent } once the card has been read |
Every agent with its own card, for agents: GET /v1/directory/agents?card=with&status=live&limit=50, paged with cursor.
The same data, for people: /agents/.
Accounts and auth
| Route | Auth | Purpose |
|---|---|---|
POST /v1/auth/login | none | {email} → 204 (never says whether the email exists) |
POST /v1/auth/verify | none | {token} → { session, user, accounts } |
POST /v1/auth/logout | session | {all?} → 204 |
GET /v1/me | session | User, accounts (each with its report currency and currency_exponent), sites, and viewer_country (where the request came from, for the dashboard map) |
POST /v1/auth/device, /device/token, /device/approve | none / session | CLI device flow |
POST /v1/accounts | none + DA-PoW | Create account + site + keys (Accounts) |
Sites, domains, keys
Session auth. Viewers can only read.
| Route | Purpose |
|---|---|
GET/POST /v1/sites | List; create {name, domain?, profile?} |
GET/PATCH/DELETE /v1/sites/:id | Settings; webhook and integration secrets are write-only |
POST /v1/sites/:id/domains | {hostname} → token and methods |
POST /v1/sites/:id/domains/:host/verify | {method} → { verified, detail, claimed } |
DELETE /v1/sites/:id/domains/:host | Remove a domain |
GET/POST /v1/sites/:id/keys | List (prefixes only); create {kind, env}, sk shown once |
POST /v1/sites/:id/keys/:keyId/rotate | New key; the old one lasts 24 h |
DELETE /v1/sites/:id/keys/:keyId | Revoke now |
DELETE /v1/sites/:id/preclaim | Discard pre-verification data |
GET /v1/sites/:id/audit | Audit log |
GET /v1/sites/:id/agent-identities | List ERC-8004 associations; optional agent_name filter |
POST /v1/sites/:id/agent-identities | Resolve and save/refresh a registry association (owner/admin) |
DELETE /v1/sites/:id/agent-identities/:link | Remove that site's association (owner/admin) |
Adding or removing an ERC-8004 identity needs the Owner or Admin role. POST takes agent_name (for example acme.shopping-assistant) and token_id (a decimal string), with optional chain_id, registry_address, reviewers (up to five addresses), reputation_registry, tag1 and tag2. Ethereum mainnet and its official registry are the defaults, and Double Agent picks the RPC endpoint. A new association answers 201, a refresh 200, each with id, snapshot (block, owner, wallet and registration URI), association: "site-configured", visit_binding: "unverified" and updated_at. GET returns { identities } and DELETE answers 204. A failed chain read is 503. Saving an association never authenticates a visitor or changes detection. The same from the command line: CLI.