Skip to content

API Reference

RunTimeAdmin edited this page Nov 2, 2026 · 18 revisions

API Reference

**Referenced Files in This Document** - [index.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/index.js) - [http-helpers.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/http-helpers.js) - [README.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/README.md)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion
  10. Appendices

Introduction

This document describes Sigvara’s oracle REST API endpoints relevant to attestation submission, reputation scoring, and evidence access. It covers:

  • /attest — submit an attestation for a Sigvara agent.
  • /score/:didHash — preview the computed score for an agent without writing to chain.
  • /evidence/:didHash — retrieve the payments behind an agent’s score with Merkle proofs.

It also documents authentication, rate limiting, error handling, security considerations, input validation, and practical request/response examples.

Project Structure

The oracle is implemented as a Node.js HTTP server that routes requests directly in its main handler. Authentication, body parsing, credential loading, and rate limiting are shared helpers.

graph TB
Client["API Consumer"] --> Server["Oracle HTTP Server<br/>index.js"]
Server --> Helpers["HTTP Helpers<br/>http-helpers.js"]
Server --> Chain["Chain Layer"]
Server --> Payments["Payments Verification"]
Server --> Scoring["Scoring Logic"]
Server --> Store["Persistent State"]
Server --> Merkle["Merkle Utilities"]
Server --> Cache["Response Cache"]
Loading

Diagram sources

Section sources

Core Components

  • Request router: Centralized HTTP handler dispatches paths such as /health, /attest, /score/:didHash, /evidence/:didHash, /outcomes, /flag, /flag/resolve, /link, /jobs/<address>, /badge/<address>.svg, /metrics, /epoch, and divergence endpoints. A request target must be a path in canonical form: //evidence/metrics, /evidence/../metrics, absolute-form targets and anything the URL parser would rewrite get 400 bad request target, and an error a route does not handle itself is logged and answered 500 internal error.
  • Authentication: Bearer tokens loaded from environment variables; supports a legacy admin token and named per-service tokens, optionally restricted to a list of scopes.
  • Rate limiting: In-memory fixed-window limiter keyed by client address and by kind of route, with X-Forwarded-For believed only from loopback and from the proxies listed in TRUSTED_PROXIES.
  • Payment verification: Optional verified-payment path where a transaction hash proves payment to the agent’s own address, read from the identity registry.
  • Scoring inputs: Attestations, flags, external ERC-8004 feedback, measured fee volume, activity, and propagation factors.
  • Evidence and Merkle proofs: Payment events are exposed with a Merkle root and leaf structure suitable for on-chain verification.

Section sources

Architecture Overview

The oracle exposes read-only and write endpoints. Read endpoints (/score, /evidence, /outcomes) are intentionally unauthenticated but rate-limited. Write endpoints require bearer-token authorization and additional validation, with one exception: POST /attest accepts a positive attestation backed by a verified payment without a token, and then records the payment and no outcome. It also takes an outcome the payer signed (payerOutcome) without a token, because the signature is the credential.

sequenceDiagram
participant C as "Client"
participant S as "Server index.js"
participant H as "Helpers http-helpers.js"
participant P as "Payments"
participant CH as "Chain"
C->>S : POST /attest { didHash, success, payment, packetId }
S->>H : identifyCaller(headers, credentials)
alt Payments required and success is true
S->>S : no token needed, the payment is the credential for the payment
else Negative, success omitted, or payments off
S->>S : require bearer token
end
alt Payments required
S->>CH : getAgentInfo(didHash)
CH-->>S : agent info
S->>P : verifyPayment(txHash, agentAddress)
P-->>S : credited payer, amount, settledAt
end
S->>S : check cooldown, credit the payment, record the outcome for a token holder
S-->>C : 200 { didHash, attester, totals, payment?, outcomeRecorded? }, or 202 with durable false when the state could not be saved
Loading

Diagram sources

Detailed Component Analysis

Endpoint: POST /attest

Submits an attestation for a Sigvara agent. The endpoint supports two modes:

  • Verified-payment mode (PAYMENT_VERIFICATION=required): Every attestation carries payment.txHash, the settlement of a real payment to the agent’s own address. A positive attestation (success: true) can be submitted without a bearer token: the verified payment is the credential for the payment, and the payer is read from the transfer log. Without a token it is recorded with no outcome (the response says "outcomeRecorded": false), because a settlement hash is public and does not show that the caller paid or saw the work. With a token the outcome is recorded: true, false, or left out for none. Negative attestations, and any whose success is missing or not a boolean, still require a bearer token, unless the request carries the payer's signed outcome in payerOutcome instead of success (below).
  • Token-only mode (PAYMENT_VERIFICATION=off, the default): A bearer token is always required, and the caller must provide an attester identifier.

Request

  • Method: POST
  • Path: /attest
  • Headers:
    • Authorization: Bearer <token> (required unless positive + verified-payment mode)
    • Content-Type: application/json
  • Body fields:
    • didHash (string, required): 32-byte hex identity hash of the agent. Any casing is accepted and it is stored in lowercase.
    • success (boolean): Whether the work was successful. Exactly true is what lets a caller without a token through. A token holder may send true, false, or leave it out for no outcome, and any other value is a 400.
    • payerOutcome (object, optional): An outcome the payer signed, in place of success, taken only where PAYER_OUTCOMES is on. It holds payer, outcome (1 delivered, 2 not delivered), an optional evidence (32 bytes, zero when left out), signedAt and deadline (seconds) and signature, EIP-712 over PaymentOutcome(bytes32 didHash, bytes32 txHash, address payer, uint8 outcome, bytes32 evidence, uint64 signedAt, uint64 deadline) in the domain Sigvara, version 1, on EXPECTED_CHAIN_ID with REPUTATION_ADDRESS as the verifying contract. It needs no token. Sending both success and payerOutcome is a 400.
    • packetId (string, optional): An optional UUID-like identifier used as corroboration when payment verification is enabled. It is kept only from a token holder, or from whoever carries a payer-signed outcome (where it names the CounterAudit packet that sealed that outcome); a stranger's is ignored, as their success is. When a token holder gives an outcome to a payment credited without one, their packetId goes on with it unless the payment already has one. It is stored beside the payment and served from /evidence, and it is never part of the Merkle leaf. A value that is not 36 hex-or-dash characters is ignored.
    • payment.txHash (string, required when payment verification is enabled): Transaction hash used to prove payment to the agent’s own address. A missing or malformed value is a 402 with code bad_tx_hash.
    • attester (string, optional): Unique ID for the attestation source; required when payment verification is disabled, and ignored when it is enabled, because the attester is then the payer taken from the transfer log.

Response

  • Success: 200 OK, the attestation was accepted and saved
    • Fields include didHash, attester, updated attestation totals (successful, total), and, when a payment was credited, payment.txHash, payment.amount (the total of the qualifying senders, in the asset’s base units) and outcomeRecorded. The response to a payer-signed outcome also carries outcomeHash, the EIP-712 hash of what was recorded.
  • Accepted but not saved: 202 Accepted, with the same fields and durable: false. The oracle could not write its state file, so a restart before its next good save would lose what it just recorded, and /health answers 503 until a save works. Do not resend: the attestation is recorded in memory and the next save that works keeps it, and a resend is refused (409 for a settlement already credited, 429 inside the cooldown). The count is sigvara_oracle_attest_total{result="accepted_not_durable"}.
  • Errors:
    • 400 Bad Request: Missing or invalid fields, success that is not a boolean for a token holder, an unreadable body, or an unknown didHash. For a payer-signed outcome also bad_payer_outcome (malformed, or sent with success), payer_outcomes_off, negative_not_accepted (a "not delivered" where only "delivered" is taken), outcome_window (timing) and bad_signature; after five bad signatures for one leg in an hour the answer is 429 with bad_signature_throttled.
    • 401 Unauthorized: Missing or invalid bearer token when required.
    • 402 Payment Required: Invalid payment, self-payment, or other payment-related failure. The code field says which (bad_tx_hash, not_found, reverted, unconfirmed, no_transfer, below_minimum, self_payment), and for a payer-signed outcome payer_mismatch when the signer has no leg of its own in the transaction.
    • 403 Forbidden: The credential is restricted by ORACLE_SCOPES_<NAME> to scopes that do not include attest.
    • 409 Conflict: Settlement already credited (replayed), or the credited payment already has an outcome, which is final (outcome_recorded), except that a payer's signed outcome can replace a token holder's inside PAYER_OUTCOME_WINDOW_DAYS (14 by default). For a payer-signed outcome both carry sameOutcome, and outcome_recorded also carries heldOutcomeHash when the outcome held is one the payer signed (a token holder's held outcome gives only sameOutcome: false), so a carrier sending an outcome again can tell its own from a different one.
    • 413 Payload Too Large: The body is over 1 MB. /flag, /flag/resolve and /link answer the same way. A client still sending has the rest read and dropped, up to 8 MB, so it can read the answer; past that the oracle cuts the connection.
    • 429 Too Many Requests: Rate limited or attestation cooldown active.
    • 502 Bad Gateway: Upstream RPC failure while reading agent info or verifying payment. The body is { error: "could not read the chain", code: "rpc_error" }, and the provider’s own message is logged and never sent.

Authentication Rules

  • If payment verification is enabled and success === true (exactly true, so "yes" is not a positive), no bearer token is required because the verified payment is the credential for the payment. Without a token the payment is recorded and the outcome is not.
  • Negative attestations (success === false), a missing success and any non-boolean value always require a bearer token.
  • A token holder reporting an outcome on a verified payment gets that outcome recorded, and can also give a payment credited without one (by the scan, or by a request with no token) its outcome afterwards, once. An outcome that is already recorded is final, except that the payer's own signed outcome can replace a token holder's within PAYER_OUTCOME_WINDOW_DAYS of the payment when the payer's is the transaction's only paying leg. A token holder's outcome never replaces a payer's.
  • An outcome the payer signed (payerOutcome) needs no token, whoever carries it: the signature is the credential. It goes on the signer's own leg and is final for that leg.
  • When payment verification is disabled, a bearer token is always required and attester must be provided.
  • A credential restricted with ORACLE_SCOPES_<NAME> that does not include attest is refused with 403 even for a positive, payment-backed attestation, because it presented a token.

Input Validation

  • didHash must be present and must be a 32-byte hex string.
  • attester is required when payment verification is disabled.
  • With payment verification enabled, txHash is validated against the chain and linked to the agent’s own address, which is read from the identity registry. The payer is taken from the transfer log, and each sender is held to PAYMENT_MIN_AMOUNT on its own.
  • Self-payments (from the agent’s own operator or agent address) are rejected.
  • Cooldown prevents repeated attestations from the same attester to the same agent within a configured window. It applies to every attestation except a token holder's report on a payment the oracle has just verified; with verification off, token holders are throttled too. With verification on the cooldown keys on the verified payer.

Practical Example

  • Request:
    POST /attest
    Authorization: Bearer <token>
    Content-Type: application/json
    
    {
      "didHash": "0x...64-hex...",
      "success": true,
      "packetId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "payment": {
        "txHash": "0x..."
      }
    }
    
  • Response:
    {
      "didHash": "0x...64-hex...",
      "attester": "0x...",
      "successful": 1,
      "total": 1,
      "payment": {
        "txHash": "0x...",
        "amount": "1000000000000000000"
      },
      "outcomeRecorded": true
    }

Section sources


Endpoint: GET /score/:didHash

Returns a preview of the computed score for an agent without writing to chain. This endpoint is unauthenticated but rate-limited.

Request

  • Method: GET
  • Path: /score/:didHash
  • Query parameters: None defined for this endpoint.
  • Path parameter:
    • didHash (string, required): 32-byte hex identity hash of the agent.

Response

  • Success: 200 OK
    • Fields include:
      • didHash
      • status: the identity registry’s AgentStatus as a number (0 Active, 1 Suspended, 2 Slashed, 3 PendingBond)
      • scores
      • attestations: the raw tally
      • weighted (present only when payment verification is enabled), including weighted attestations, distinct payers, activity (an object with the first and last activity time and a recency weight), trust weight, half-life days, and max per payer. It is what the score was computed from.
      • flags (the raw count) and flagWeight (the age-weighted one the score charges for)
      • erc8004AgentId (when linked)
  • The preview uses the same inputs as the epoch, including counterparty standing and the chain’s clock, so two operators’ answers can be compared by hand.
  • Errors:
    • 404 Not Found: The path is not /score/ followed by a 32-byte hex didHash.
    • 429 Too Many Requests: Rate limited.
    • 502 Bad Gateway: Upstream RPC failure reading agent info, or a node that could not answer for a payer after retries. The body carries code: "rpc_error".
    • 500 Internal Server Error: Unexpected computation failure.

Practical Example

  • Request:
    GET /score/0x...64-hex...
    
  • Response:
    {
      "didHash": "0x...64-hex...",
      "status": 0,
      "scores": { ... },
      "attestations": { "successful": 10, "total": 12 },
      "weighted": {
        "attestations": { "successful": 8, "total": 10 },
        "distinctPayers": 5,
        "activity": { "firstActivitySec": 1789734970, "lastActivitySec": 1789944267, "recency": 0.78 },
        "trustWeight": 1,
        "halfLifeDays": 90,
        "maxPerPayer": 4
      },
      "flags": 1,
      "flagWeight": 0.0012,
      "erc8004AgentId": "123"
    }

Section sources


Endpoint: GET /evidence/:didHash

Retrieves the payments behind an agent’s score, including Merkle proofs. This endpoint is deliberately unauthenticated so anyone can audit the evidence.

Request

  • Method: GET
  • Path: /evidence/:didHash
  • Path parameter:
    • didHash (string, required): 32-byte hex identity hash of the agent.

Response

  • Success: 200 OK
    • Returns JSON containing the agent’s payment events (evidence, with count) and the Merkle root over them (evidenceRoot). Each entry carries its leaf and proof.
  • Errors:
    • 400 Bad Request: Malformed didHash.
    • 429 Too Many Requests: Rate limited.

Merkle Proof Verification

  • The response includes a Merkle root and leaf structure committed to by the oracle.
  • Consumers can independently recompute the root over the payment events and compare it with the value stored on-chain (evidenceRoots(didHash) on SigvaraReputation), or check one leaf with verifyEvidence(didHash, leaf, proof). The served leaf is a convenience: a verifier should derive it from the payment rather than trust it.
  • The response names the committed fields and leaf order so verifiers do not need to infer them from implementation details.
  • success is served as true, false or null and enters the leaf as a uint8: 0 failed, 1 succeeded, 2 not reported. A payment found by the scan, or credited without a token, carries no outcome and so reads null.
  • counterauditPacketId appears on an event when the attestation carried a packetId that was kept (a token holder's, or one beside a payer-signed outcome), and the response lists it under unverifiedFields. It is corroboration, not evidence: the oracle checks only that it is UUID-shaped and never fetches the packet, and it is deliberately not in the leaf. For a payer-signed outcome the packet's public proof names the EIP-712 hash of what the payer signed, so the link can be checked.
  • outcomeSource says whose word each outcome is: payer_signed, credentialed, or null for none. It is this oracle's label and not part of the leaf. Where PAYER_OUTCOMES_PUBLISH=on, a payer-signed entry also carries the signed payerOutcome, and the response gives the signing domain as payerOutcomeDomain, so a verifier can check that the committed outcome is one the payer signed.

Practical Example

  • Request:
    GET /evidence/0x...64-hex...
    
  • Response:
    {
      "didHash": "0x...64-hex...",
      "evidenceRoot": "0x...",
      "count": 1,
      "committedFields": ["txHash","payer","amount","settledAt","success"],
      "evidence": [
        {
          "txHash": "0x...",
          "payer": "0x...",
          "amount": "1000000000000000000",
          "settledAt": 1700000000,
          "success": true,
          "leaf": "0x...",
          "proof": ["0x..."]
        }
      ]
    }

Section sources


Additional Endpoints Referenced

  • /health: Health check with operational signals for alerting. It answers 200 when healthy and 503 when not.
  • /outcomes?since=<ms>: The payer-signed outcomes this oracle has taken, oldest first, 500 to a page, for other operators to import and anyone to check. Unauthenticated; 404 unless PAYER_OUTCOMES and PAYER_OUTCOMES_PUBLISH are on, and 400 for a since that is not a time in milliseconds.
  • /flag and /flag/resolve — Token-gated flag management affecting scores (scope flag). While a save is failing they answer 202 with durable: false, as /attest and /link do; do not resend a flag or a resolve.
  • /link — Link a Sigvara agent to an ERC-8004 identity (token-gated, scope link).
  • /jobs/<address> — Observation-only ERC-8183 jobs data. It answers 501 with code jobs_disabled when no registry is configured.
  • /badge/<address>.svg — Embeddable SVG badge showing finalized score state.
  • /metrics — Prometheus metrics.
  • /epoch — Manual epoch trigger (token-gated, scope epoch).
  • /divergence and /divergence/:didHash — Checker-mode disagreement records. A primary answers 404 with the error not_a_checker.

These endpoints share the same authentication and rate-limiting patterns described below.

Section sources

Dependency Analysis

The following diagram shows how the core endpoints depend on shared modules.

graph LR
A["/attest"] --> H["identifyCaller()"]
A --> R["rateLimited()"]
A --> V["verifyPayment()"]
A --> S["store attestations"]
B["/score/:didHash"] --> H
B --> R
B --> M["measuredFactorsFor()"]
B --> C["computeScore()"]
E["/evidence/:didHash"] --> R
E --> ME["getPaymentEvents()"]
E --> MR["buildEvidenceBody()"]
Loading

Diagram sources

Section sources

Performance Considerations

  • /score performs multiple chain reads (agent info, payer standing, external score when linked). It is rate-limited even though it is public.
  • /evidence may rebuild a Merkle tree over payment events on cache misses; it is rate-limited to bound first-request cost.
  • /attest validates payments and updates persistent state; it is rate-limited and uses a per-attester cooldown.
  • Metrics expose counters for rate limits, RPC errors, attestations accepted/rejected, and more.

Troubleshooting Guide

Authentication Failures

  • 401 Unauthorized on /attest occurs when:

    • A negative attestation is submitted without a bearer token.
    • success is missing or is not exactly true, with no bearer token.
    • Payment verification is disabled and no bearer token is provided.
    • The bearer token does not match any configured credential, and the request needed one.
  • 403 Forbidden on a write route occurs when the credential is restricted by ORACLE_SCOPES_<NAME> to scopes that do not include the one the route needs (attest, flag, link or epoch).

  • Credentials are loaded from:

    • ORACLE_ADMIN_TOKEN (legacy admin token).
    • ORACLE_TOKEN_<NAME> variables (per-service tokens).
  • ORACLE_SCOPES_<NAME> takes a comma-separated list of scopes for the credential ORACLE_TOKEN_<NAME>. A credential with no such variable keeps every scope, and admin is never restricted. An unknown scope, or a restriction naming a credential that does not exist, stops the process at startup.

  • Tokens are matched using constant-time comparison against Authorization: Bearer <token>. A token shorter than 32 characters still works but is named in a startup warning.

Section sources

Rate Limiting

  • Fixed-window rate limiter:

    • Window: 60 seconds.
    • Max: 60 requests per key per window.
    • Key: the caller and the kind of route. Writes, /evidence, /divergence and the other reads each have their own bucket, so a flood against one cannot use up another’s allowance (the watcher polls /divergence). The write routes are metered before they are authorised, so guessing at a token is throttled too. The badge route limits only a cache miss.
    • Caller: the socket address, or the address in X-Forwarded-For when the connection arrives from loopback or from a proxy listed in TRUSTED_PROXIES. Behind a reverse proxy the socket address is the proxy’s, so without TRUSTED_PROXIES every visitor shares one bucket.
  • Responses:

    • 429 Too Many Requests with { error: "Rate limited" }.

Section sources

Attestation-Specific Errors

  • 400 Bad Request:

    • Missing didHash, or a didHash that is not a 32-byte hex string.
    • Missing attester when payment verification is disabled.
    • Unknown didHash.
    • success that is not true, false or omitted, from a token holder.
    • A body that is not valid JSON.
    • A payer-signed outcome that is malformed or sent with success (bad_payer_outcome), a "not delivered" where only "delivered" is taken (negative_not_accepted), timing outside its window (outcome_window), or a signature that is not the payer's (bad_signature).
  • 402 Payment Required:

    • Invalid payment (code is bad_tx_hash, not_found, reverted, unconfirmed, no_transfer or below_minimum).
    • Self-payment detected (code is self_payment).
    • A payer-signed outcome whose signer has no leg of its own in the transaction (code is payer_mismatch).
  • 403 Forbidden:

    • The credential is restricted to scopes that do not include attest.
  • 409 Conflict:

    • Settlement already credited (replayed).
    • The credited payment already has an outcome (outcome_recorded). For a payer-signed outcome the body carries heldOutcomeHash and sameOutcome; sameOutcome: true means the outcome sent is the one already held.
  • 413 Payload Too Large:

    • The body is over 1 MB. The same applies to /flag, /flag/resolve and /link.
  • 429 Too Many Requests:

    • Rate limited.
    • Attestation cooldown active (includes remainingSeconds and cooldownMs). It does not apply to a token holder reporting on a verified payment.
  • 502 Bad Gateway:

    • RPC failure reading agent info or verifying payment. It is counted separately from rejected attestations, because a node having a bad minute must not read as a caller trying it on. Retry.

Section sources

Score Endpoint Errors

  • 502 Bad Gateway:
    • Upstream RPC failure reading agent info, or a node that could not answer for a payer after its retries (code: "rpc_error"). A payer the node could not answer for is never read as having no standing, because that would serve a lower score than the oracle would propose.
  • 500 Internal Server Error:
    • Unexpected computation failure; generic message returned to avoid leaking upstream details.

Section sources

Evidence Endpoint Errors

  • 400 Bad Request:
    • Malformed didHash.
  • 429 Too Many Requests:
    • Rate limited.

Section sources

Conclusion

Sigvara’s oracle API provides secure attestation submission, transparent reputation scoring, and auditable evidence with Merkle proofs. Authentication is flexible, supporting both legacy admin tokens and per-service bearer tokens. Public read endpoints are intentionally open but rate-limited. Consumers should handle 429, 401, 402, 403, 409, 502, and 500 responses appropriately, validate inputs, and use /evidence for independent verification of the oracle’s claims.

Appendices

Authentication Methods

  • Admin token: ORACLE_ADMIN_TOKEN maps to the admin credential name.
  • Named tokens: ORACLE_TOKEN_<NAME> variables define per-service credentials; names are lowercased. The name is logged when a flag is raised or cleared, and the startup line lists the credential names, so deleting one variable revokes exactly that caller.
  • Scopes: ORACLE_SCOPES_<NAME> restricts a credential to some of attest, flag, link and epoch. With no variable a credential keeps every scope, and admin is never restricted.
  • Header format: Authorization: Bearer <token>.
  • Policy enforcement: Non-loopback binds require at least one configured credential; otherwise startup fails.

Section sources

Rate Limiting Policy

  • Fixed window: 60 seconds.
  • Maximum: 60 requests per key per window.
  • Key derivation:
    • Direct socket address for connections that do not come from loopback or a listed proxy.
    • For loopback connections and for proxies listed in TRUSTED_PROXIES (comma-separated addresses or CIDR ranges), X-Forwarded-For is read from the right: hops that are trusted proxies are skipped and the first address that is not one is the client, so a client that sends a forged prefix gains nothing. An entry that is not an IP address is never used as a key.
    • The key also carries the kind of route (writes, /evidence, /divergence, other reads), so each kind has its own 60 a minute.

Section sources

Security Considerations and Best Practices

  • Always set bearer tokens before exposing the oracle publicly.
  • Prefer per-service tokens for better revocation granularity, and restrict each to the scopes it needs.
  • Do not proxy the whole service. The deployment publishes /health, /score/*, /evidence/*, /badge/*, /outcomes and POST /attest, and answers 404 for everything else, so /flag, /flag/resolve, /link, /epoch and /metrics are reachable only from the host.
  • Use /evidence to independently verify Merkle roots and payment events.
  • Treat 502 as upstream failures and retry with backoff.
  • Respect 429 responses and implement exponential backoff.
  • Validate all inputs on the client side to reduce unnecessary retries.
  • Avoid sending sensitive data in logs or error messages.

Overview

Clone this wiki locally