SITUSDemoDashboardMailboxesPolicyMandatesGrantsReceiptsVerifyPasskeysDocs

Docs

What Situs Pilot 1 is, and how to build against it.

Build against Situs

The developer guide, by audience: venue operators, enterprises and startups, standard adopters. The snippets are rendered from the running stack's own values: its URLs, its kids from /.well-known/keys.json, its plugins from /plugins.

Venue operators

What a venue is

A venue is where an action is carried out: one enforcement point (the pep container) with its secret broker, next to the authority that decides. The venue is the only holder of the credential that reaches the counterparty; an agent never holds it. Every decision the enforcement point makes is signed with the venue's key, and the receipt names the venue in where.venue.

The enforcement point and its secret broker

The pep reads each plugin's declarative connector.json (no plugin code runs in it): the environment variable with the counterparty's base URL, the operations, where the acknowledgment's fields sit, and the counterparty's signing kid. For each valid single-use grant the secret broker mints a short-lived token (PEP_BROKER_SECRET, HMAC) for one call; the token is never returned to the gateway or the agent.

settingwhat it is
SITUS_KEYS_DIRthe venue's keys; pep.pem signs enforcement decisions
PEP_BROKER_SECRETthe broker's secret; set your own outside local development
AUTHORITY_URLwhere grants are redeemed and evidence is sent
<connector base_url env>each plugin's counterparty URL, named in its connector.json
SITUS_GCE_IDENTITY1 on a GCE VM: decisions carry the cloud's identity token (L1-declared)

Publishing keys

Every party's public key is served at /.well-known/keys.json (the authority at https://situs-vm.o11r.com/api/authority/.well-known/keys.json, the pep at its own).

This page was rendered without a running stack, so it shows no key: read the kids and fingerprints from the running stack's keys.json.

curl -s https://situs-vm.o11r.com/api/authority/.well-known/keys.json | jq 'map_values(.fingerprint)'

Levels: what a venue can claim

what.level_achieved in every receipt says what the venue proved about where it ran. Only the two labelled levels exist in this build, and both are always shown as what they are: a simulated or declared level never renders green, on the console or in the verifier.

levelnamewhat backs itin this build
L0declaredthe venue says where it runs; nothing checks itnot a claim this build makes
L1confidential VM, attesteda hardware quote binds the running image to the venue keynot built
L2L1 plus a GPU quotethe model's accelerator attests toonot built
L1-simsimulated, localsoftware measurement of the gateway only; hardware quote fields present and emptyevery local receipt
L1-declaredconfidential VM declared by the clouda cloud-signed identity token names the VM; nothing in it measures the imagereceipts from the GCP deployment

Getting listed

There is no registry, by design. situs_core/trust_bundle.json ships with the verifier the way a root store ships with a browser. To be listed, send a signed venue.json (your venue id, the level you can back, the kids that sign decisions and their key fingerprints) and it lands by pull request. situs-verify then labels a receipt's venue and decision kid "known venue" or "unknown key": a label, not a check, never changing a verdict or the exit code. The local development entry pins no key, because every checkout makes its own.

{"venue": "ven_example", "level": "L1-declared", "label": "Example Corp, one confidential VM",
 "kids": {"pep": "sha256:<fingerprint>", "authority": "sha256:<fingerprint>"},
 "signature": {"kid": "<your operator key>", "alg": "ed25519", "sig": "<hex>"}}

Enterprises and startups

Quickstart: a mandate, a chat completion, a receipt, a verification

Pick a plugin, sign its example mandate with the principal's key, send one chat completion through the gateway, make the call an agent fooled by the plugin's injected scenario would make, and verify the receipt of its refusal offline. Each block below is the same script for one loaded plugin; the test suite runs one of them, literally, against the running stack.

plugin: treasury
# From the repository root, with the stack up (make up). Files land in out/quickstart/.
make keys   # the principal's key: keys/principal_jdoe.pem, written once, never overwritten, never printed
mkdir -p out/quickstart
uv run situs-keys sign-mandate plugins/treasury/fixtures/intent_contract.example.json \
  --authority https://situs-vm.o11r.com/api/authority --register --out out/quickstart/mandate.json
CONTRACT_ID=$(jq -r .contract_id out/quickstart/mandate.json)
TOKEN=$(uv run situs-keys agent-token --sub "$(jq -r .principal out/quickstart/mandate.json)")   # a secret: never echo it
AUTH=(-H "Authorization: Bearer $TOKEN" -H "X-Situs-Contract: $CONTRACT_ID" -H "content-type: application/json")
TOOLS=$(curl -sf "${AUTH[@]}" https://situs-vm.o11r.com/api/gateway/v1/tools | jq -c '[.tools[].spec]')
curl -sf "${AUTH[@]}" https://situs-vm.o11r.com/api/gateway/v1/chat/completions -d '{"model": "situs-agent", "messages": [{"role": "user", "content": "Handle the next request."}], "tools": '"$TOOLS"'}' | jq -c '.choices[0].message'
# The call an agent fooled by the plugin's injected scenario makes. The rules refuse it, with a receipt.
curl -sf "${AUTH[@]}" https://situs-vm.o11r.com/api/gateway/v1/tools/propose_transfer -d '{"to_account":"9988776655","amount":"42250.00","currency":"USD","beneficiary_id":"ven_fabrikam","memo":"FS-7781 (remittance change per invoice)"}' \
  | tee out/quickstart/result.json | jq -c '{status, reason, grant_id}'
curl -sf "https://situs-vm.o11r.com/api/authority/receipts?grant_id=$(jq -r .grant_id out/quickstart/result.json)" \
  | jq '.[0]' > out/quickstart/receipt.json
uv run situs-verify out/quickstart/receipt.json --as quickstart
plugin: records
# From the repository root, with the stack up (make up). Files land in out/quickstart/.
make keys   # the principal's key: keys/principal_jdoe.pem, written once, never overwritten, never printed
mkdir -p out/quickstart
uv run situs-keys sign-mandate plugins/records/fixtures/intent_contract.example.json \
  --authority https://situs-vm.o11r.com/api/authority --register --out out/quickstart/mandate.json
CONTRACT_ID=$(jq -r .contract_id out/quickstart/mandate.json)
TOKEN=$(uv run situs-keys agent-token --sub "$(jq -r .principal out/quickstart/mandate.json)")   # a secret: never echo it
AUTH=(-H "Authorization: Bearer $TOKEN" -H "X-Situs-Contract: $CONTRACT_ID" -H "content-type: application/json")
TOOLS=$(curl -sf "${AUTH[@]}" https://situs-vm.o11r.com/api/gateway/v1/tools | jq -c '[.tools[].spec]')
curl -sf "${AUTH[@]}" https://situs-vm.o11r.com/api/gateway/v1/chat/completions -d '{"model": "situs-agent", "messages": [{"role": "user", "content": "Handle the next request."}], "tools": '"$TOOLS"'}' | jq -c '.choices[0].message'
# The call an agent fooled by the plugin's injected scenario makes. The rules refuse it, with a receipt.
curl -sf "${AUTH[@]}" https://situs-vm.o11r.com/api/gateway/v1/tools/release_record -d '{"record_id":"rec_acme_0003","recipient_id":"kyc-archive@harborpike-secure.example","purpose":"audit","sensitivity":"restricted","reference":"AUD-2026-119"}' \
  | tee out/quickstart/result.json | jq -c '{status, reason, grant_id}'
curl -sf "https://situs-vm.o11r.com/api/authority/receipts?grant_id=$(jq -r .grant_id out/quickstart/result.json)" \
  | jq '.[0]' > out/quickstart/receipt.json
uv run situs-verify out/quickstart/receipt.json --as quickstart

Point any OpenAI-compatible client at the gateway

Set base_url to https://situs-vm.o11r.com/api/gateway/v1. Authenticate with an HS256 JWT whose sub is the principal (situs-keys agent-token --sub <principal>; the secret is the gateway's SITUS_JWT_SECRET) and name the mandate in the X-Situs-Contract header. GET /v1/tools returns the tools of the plugin behind that mandate.

from openai import OpenAI   # any OpenAI-compatible client; this repository does not ship one

client = OpenAI(base_url="https://situs-vm.o11r.com/api/gateway/v1",
                api_key=TOKEN,                      # the agent's JWT: sub is the principal
                default_headers={"X-Situs-Contract": CONTRACT_ID})
answer = client.chat.completions.create(model="situs-agent", tools=TOOLS,
                                        messages=[{"role": "user", "content": "Handle the next request."}])
# Each tool call the model returns goes to https://situs-vm.o11r.com/api/gateway/v1/tools/<name> with the same two headers.

Write and sign a mandate

A mandate is an Intent Contract: which façades, within what limits, when a person must approve, for how long. situs-keys sign-mandate <example.json> --register fills a fresh id and validity window, cites the profile version in force (GET https://situs-vm.o11r.com/api/authority/profiles/<major>), signs with keys/principal_<name>.pem and registers it. The authority refuses a mandate that exceeds the profile. A filled example per plugin:

plugin: treasury
{
  "contract_id": "ic_acme_treasury_ops_0001",
  "tenant": "tn_acme",
  "principal": "jdoe",
  "agent": "agent:treasury-ops",
  "profile": "treasury.v1",
  "scope": [
    "bank.readBalance",
    "market.fetch",
    "bank.executeTransfer"
  ],
  "limits": {
    "runtime_max_s": 3600,
    "use_count_max": 10
  },
  "approval": {},
  "forbidden": [
    "bank.addBeneficiary",
    "bank.changeAccountDetails"
  ],
  "valid_from": "2026-09-22T13:00:00Z",
  "valid_to": "2026-09-22T14:00:00Z",
  "policy_version": "treasury.v1@0.2",
  "rules": {
    "amount_max": "250000.00",
    "daily_max": "600000.00",
    "threshold": "50000.00"
  }
}
plugin: records
{
  "contract_id": "ic_acme_records_desk_0001",
  "tenant": "tn_acme",
  "principal": "jdoe",
  "agent": "agent:records-support",
  "profile": "records.v1",
  "scope": [
    "records.lookup",
    "requester.verify",
    "records.release"
  ],
  "limits": {
    "runtime_max_s": 3600,
    "use_count_max": 20
  },
  "approval": {
    "routes": [
      {
        "approver_type": "Records officer",
        "approvers": [
          "jdoe"
        ],
        "wait_s": 600,
        "notify": [
          "app"
        ],
        "assurance": "hardware"
      }
    ]
  },
  "forbidden": [
    "records.delete",
    "records.export"
  ],
  "valid_from": "2026-09-24T13:00:00Z",
  "valid_to": "2026-09-24T14:00:00Z",
  "policy_version": "records.v1@0.1",
  "rules": {
    "max_sensitivity": "restricted",
    "purposes": [
      "audit",
      "account_opening"
    ]
  }
}

Choose a plugin and publish a profile

GET https://situs-vm.o11r.com/api/authority/plugins lists the plugins this stack loaded. A profile is your signed instance of a plugin's rules; edit it on the console's Policy tab and publish a new version, signed by the principal. Mandates signed under an older version are then refused.

plugin: treasury

treasury 0.3.0 (treasury.v1, listed plugin): covered façade bank.executeTransfer, counterparty bank; first plugin, the demo.

plugin: records

records 0.1.0 (records.v1, listed plugin): covered façade records.release, counterparty recipient; generality test.

What comes back, and where receipts land

A tool call answers executed (with the counterparty's signed acknowledgment), fetched, pending (a person must approve; present the grant_id again after approval), denied (with a reason code) or duplicate. Every decision that names a grant produces a Proof Receipt: GET /receipts?grant_id= or ?tenant= on the authority, and a file out/<receipt_id>.json. Receipts are chained per tenant and entered in the transparency log. The profile's receipt_retention_days says how long the deployment keeps them; keep your own copies.

Verify offline

The receipt carries every public key it needs. Verification runs on your machine, without the network and without trusting Situs:

uv run situs-verify receipt.json --as your-auditor                 # the checks, then the labels
uv run situs-verify receipt.json --report proof.html               # plus the full visibility proof
uv run situs-verify receipt.json --keys keys.json --prev prev.json # your own key bundle, and the chain

Ship receipts into your own systems

Poll GET /receipts?tenant=<tenant> and store each receipt as it is: it is the evidence, byte for byte. Check the log with GET /log/head (a signed tree head) against the inclusion proof in each receipt. An approver app can also receive signed webhooks (see approvals). Verify on ingest with the same situs-verify, and alert on anything that does not verify.

Standard adopters

JSON Schemas

Generated from the models the services use, served by the console and valid JSON Schema (draft 2020-12). The test suite validates both plugins' fixtures, both example mandates and a real receipt from each plugin against them.

schemawhat it describes
/docs/schema/receipt-0.3.jsonthe Proof Receipt, version 0.3
/docs/schema/mandate.jsonan Intent Contract (a mandate), signed by the principal
/docs/schema/profile.jsona Trust Profile: a tenant's signed instance of a plugin's rules
/docs/schema/action-grant.jsona single-use Action Grant, signed by the authority
/docs/schema/plugin-manifest.jsona plugin's manifest.json

Canonicalization and signing

Canonical JSON is RFC 8785 (JCS). A hash is sha256 over the canonical bytes, written as lowercase hex. A signature is over the ASCII bytes of that 64-character hex digest, never over the raw digest and never over the JSON. ed25519 for services and key files; es256, WebAuthn and App Attest for people's hardware keys.

The five signature slots

Each fact in a receipt is signed by the party responsible for it. Situs signs only the assembly and cannot vouch for anyone else's fact.

slotkidoverwhat it proveswhose key
principalprincipal_<name>contract_hashwho authorised the agent, within what limitsindependent: the principal's own key
approverapprover_<name>[.<device>]action_hasha person approved this exact action, at the assurance the route demandedindependent: the approver's own key
peppep (or authority for a policy refusal)the enforcement decisionwhat the enforcement point decided and didSitus-signed: a Situs service
counterpartythe plugin connector's kidcounterparty_ack_hashwhat the counterparty did, bound to the action hash and the idempotency keyindependent: the counterparty's own key
situssitus_assemblyreceipt_body_hashthat Situs assembled these facts, and nothing moreSitus-signed: the assembly only

The proof report grades every answer on one strength scale:

strengthmeaning
4independent, hardware key
3independent signature
2Situs-signed
1declared
0not proven

The transparency log

Each receipt is a leaf: sha256(0x00 || the ASCII hex of sha256(JCS(the receipt with status.log, bundle.log_root_signature and signatures.situs nulled))). Nodes and roots follow RFC 6962; the receipt carries its RFC 6962 inclusion proof and the authority-signed tree head. With SITUS_REKOR=1 and rekor-cli present, tree heads are also posted to Rekor (optional; the receipt then names the entry).

The verifier

cli/situs_verify.py is the verifier; the console's /verify page runs the same function. Its 9 checks, in order (a check that does not apply passes as n/a):

#check
1schema and required fields
2Situs assembly signature over receipt body
3principal signature over contract_hash
4approver signature over action_hash
5enforcement decision signature
6counterparty signature over acknowledgment
7receipt chain
8transparency log inclusion
9status as of revocation check

Labels come after the checks and never change them: the plugin's tier, the venue ("known venue" or "unknown key"), a declared cloud identity, and --as, who ran it.

Compatibility promise

Operating Rules, and the license

The Operating Rules are a draft: optional, not in force, and not published in this repository yet. Nothing in this build depends on them.

License: PLACEHOLDER. The founder has not chosen a license for this repository; nothing here grants one. (Each plugin's manifest names its own.)

Developer guide: the reference

How to build against Situs Pilot 1 on this Mac: point an agent at the gateway, sign mandates and approvals with people's keys, and read and verify what happened. Every service also serves its own interactive API reference at /docs and the schema at /openapi.json; the addresses below open them.

Services and who may call them

An agent talks to the gateway and nothing else. Anything that creates authority (a mandate, an approval, a revocation, a profile version) carries a person's signature, which the authority checks against the keys it publishes. There is no API key that lets a program skip that.

serviceaddress · API referencewhat it holdscalled by
gatewaylocalhost:8000The agent's only door. Tools and chat, each call recorded as evidence.the agent
authoritylocalhost:8001Mandates, grants, approvals, revocations, receipts, the evidence chain and the profile.people's tools (with signatures) and the services
peplocalhost:8002The enforcement point. Spends a grant once and holds the only counterparty secret.the gateway
banklocalhost:8003A mock bank: balances, transfers, FX. Knows no policy.the pep only
consolelocalhost:8080This console. Pages, not an API.people

Build an agent

Authenticate with an HS256 JWT whose sub is the principal (the dev secret is SITUS_JWT_SECRET), and name the mandate you act under in X-Situs-Contract. Then call tools. The gateway forwards them: a proposal goes to the authority, which answers denied (with a reason), pending (a person must approve) or allows it, and only then does the enforcement point execute it. A pending result carries a grant_id; present it again after approval.

import time, httpx
from situs_core import jwt_hs256

GATEWAY = "http://localhost:8000"
CONTRACT_ID = "ic_..."   # a mandate the treasurer signed (see "Sign a mandate")
token = jwt_hs256.encode({"sub": "jdoe", "iat": int(time.time())},
                         "dev-jwt-secret-not-for-production")   # SITUS_JWT_SECRET
headers = {"Authorization": f"Bearer {token}", "X-Situs-Contract": CONTRACT_ID}

out = httpx.post(f"{GATEWAY}/v1/tools/propose_transfer", headers=headers, json={
    "to_account": "4417002211", "amount": "75000.00", "currency": "USD",
    "beneficiary_id": "ven_northwind",
    "memo": f"NW-{int(time.time())}"}).json()   # the invoice number: the same one twice is duplicate_action
print(out["status"], out.get("reason"))     # pending approval_required

# after a person approves, present the grant; the pep executes it once
out = httpx.post(f"{GATEWAY}/v1/tools/propose_transfer", headers=headers,
                 json={"grant_id": out["grant_id"]}).json()
print(out["status"], out["counterparty_ack"]["transfer_id"])   # executed tr_...

Or run a model loop through the OpenAI-compatible endpoint. Any OpenAI client works with base URL http://localhost:8000/v1 and the two headers. Each response carries the hashes the gateway recorded.

curl -s localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" -H "X-Situs-Contract: $CONTRACT_ID" \
  -H 'content-type: application/json' -d '{
    "model": "situs-agent",
    "messages": [{"role": "user", "content": "Pay the Northwind invoice NW-2026-0912."}],
    "tools": '"$(curl -s localhost:8000/v1/tools -H "Authorization: Bearer $TOKEN" -H "X-Situs-Contract: $CONTRACT_ID" | jq .tools)"'
  }' -D - | grep -i x-situs     # X-Situs-Request-Hash, X-Situs-Response-Hash, X-Situs-Event-Id

Tool results by status: executed (with the bank's signed ack), fetched, pending, denied, refused, and duplicate when a spent grant is presented again. The agent never sees a bank credential and cannot mint a grant, whatever the model writes.

Several agents, one set of invoices

Run as many agents as you like against the same mailboxes: the authority treats a payment, not a proposal, as the unit. A second proposal for the same payee, account, amount, currency and invoice reference (the memo, case and spacing ignored) is refused as duplicate_action while an earlier grant for it is pending, active or redeemed, under any mandate in the tenant. A transfer's idempotency key is derived from the same fields (idem_pay_…), so the bank also moves an invoice's money once. Put the invoice number in memo; paying the same invoice again on purpose needs a new reference.

Sign a mandate in Python

A mandate is an Intent Contract the treasurer signs with their key. It must cite the profile version in force and stay within the profile's limits, or the authority refuses it (policy_version_mismatch, amount_max_over_profile…). situs-keys sign-mandate does the same from a shell; the console's Demo and Policy tabs do it for you.

from datetime import datetime, timedelta, timezone
from pathlib import Path
import httpx
from situs_core import canon, contracts, keys
from situs_core.models import IntentContract

AUTHORITY = "http://localhost:8001"
profile = httpx.get(f"{AUTHORITY}/profiles/treasury.v1").json()
base = contracts.load_unsigned_contract(Path("plugins/treasury/fixtures/intent_contract.example.json")).to_dict()
now = datetime.now(timezone.utc)
unsigned = IntentContract.model_validate({**base,
    "contract_id": f"ic_dev_{int(now.timestamp())}",
    "policy_version": f"{profile['profile']}@{profile['version']}",   # the version in force
    "valid_from": canon.rfc3339(now), "valid_to": canon.rfc3339(now + timedelta(hours=1))})
principal = keys.load_private_key(Path("keys/principal_jdoe.pem"))
signed = contracts.sign_contract(unsigned, principal)
httpx.post(f"{AUTHORITY}/contracts", json=signed.to_dict()).raise_for_status()
print(signed.contract_id)

Approve a payment

Recompute the action hash from the canonical action, refuse if it disagrees with the grant, then sign the hash. The same two steps run in situs-approve, the console's approve button, the email link and the Mac dialog (make approver). An approver app does it through the approval API.

from pathlib import Path
import httpx
from cli.situs_approve import check_action_hash, sign_approval
from situs_core import keys

AUTHORITY = "http://localhost:8001"
record = httpx.get(f"{AUTHORITY}/grants", params={"status": "pending"}).json()[0]
action_hash = check_action_hash(record)        # recompute it; refuse if the grant disagrees
approver = keys.load_private_key(Path("keys/approver_jdoe.pem"))
body = sign_approval(action_hash, "jdoe", approver)
gid = record["grant"]["grant_id"]
httpx.post(f"{AUTHORITY}/grants/{gid}/approve", json=body.to_dict()).raise_for_status()

Read and verify receipts

Every decision produces a Proof Receipt. It verifies offline with nothing but the receipt: each party's signature over the fact it is responsible for, plus the assembly signature.

curl -s "localhost:8001/receipts?grant_id=$GRANT_ID" | jq -r '.[0].receipt_id'
uv run situs-verify out/rcpt_....json                      # nine checks, offline
uv run situs-verify out/rcpt_....json --report proof.html  # plus the full visibility proof
uv run situs-verify out/rcpt_....json --as-of 2026-09-23T15:00:00Z   # status as of an instant

The profile

GET /profiles/treasury.v1 returns the rules in force and /publication says who published them. A new version is published from the Policy tab, signed by the treasurer; the authority and the pep verify it on their next request, and answer 503 with profile_hash_mismatch, profile_signature_invalid or signer_not_principal if the file cannot be trusted. Mandates signed under an older version are then refused with contract_expired.

Errors and reason codes

Every error body is {"detail": {"error": "<code>", "message": "…"}}. Policy decisions use a closed set of reason codes: contract_revoked (the mandate was revoked), contract_expired (the mandate expired, or was signed under an older profile version), facade_not_in_scope (the mandate does not allow this action), use_count_exceeded (the mandate's uses are spent), duplicate_action (duplicate payment), approval_required (a person must approve), recipient_not_approved, purpose_not_allowed, release_limit_exceeded, beneficiary_not_approved (payee not on the approved list), amount_over_limit (amount over the per-transfer limit), daily_limit_exceeded (daily limit exceeded), ok (allowed). The detail string in a decision says which limit or rule applied.

Endpoint reference

gateway :8000

endpointwho calls itwhat it does
GET /v1/toolsanyoneA plugin's tools in OpenAI function format: ?plugin=<name>, or the caller's own mandate's plugin when the JWT and contract header are sent (Task 34).
POST /v1/tools/{name}agent (JWT + contract)Run read_balance, fetch_fx or propose_transfer.
POST /v1/chat/completionsagent (JWT + contract)OpenAI-compatible chat; answered by Claude or the scripted stub.
GET /v1/agentanyoneWhich model answers chat: scripted or Claude.
GET /healthzanyoneLiveness.

authority :8001

endpointwho calls itwhat it does
GET /pluginsanyoneEvery loaded plugin: name, version, tier, façades, covered façade, counterparty kind (Task 35).
GET /profiles/{name}anyoneThe Trust Profile in force for a plugin's major (<plugin>.v1), with its version.
GET /profiles/{name}/publicationanyoneWho published that version, when, why, and their signature.
POST /contractsprincipal's signatureRegister a signed Intent Contract (a mandate).
GET /contractsanyoneEvery mandate and its status.
GET /contracts/{contract_id}anyoneOne mandate, with its revocation if any.
POST /contracts/{contract_id}/revokeprincipal's signatureRevoke a mandate. Later proposals are refused.
POST /grantsgatewayPropose an action; the engine allows, denies, or waits for a person.
GET /grants?status=anyoneGrants, optionally by status: pending, active, redeemed, denied, expired.
GET /grants/{grant_id}anyoneOne grant with its canonical action and decision.
POST /grants/{grant_id}/approveapprover's signatureApprove the exact action hash.
POST /grants/{grant_id}/redeempepSpend a grant (single use).
GET /receipts?grant_id=&tenant=anyoneProof Receipts.
GET /receipts/{receipt_id}anyoneOne receipt: the file situs-verify checks.
POST /eventsservices (signed)Append a signed evidence event.
GET /eventsanyoneThe evidence chain.
GET /events/headanyoneThe chain's latest hash.
GET /log/headanyoneThe transparency log's signed tree head.
GET /.well-known/keys.jsonanyoneEvery party's public key, to verify anything above.

pep :8002

endpointwho calls itwhat it does
POST /executegatewayPresent a grant and its canonical action; the pep checks and executes once.
POST /fetchgatewaymarket.fetch: GET a URL under the profile's market-data addresses.
GET /.well-known/keys.jsonanyoneThe pep's public key.

bank :8003

endpointwho calls itwhat it does
GET /accounts/{account_id}/balancepep (broker token)Balance.
POST /transferspep (broker token)Move money once per idempotency key; returns a signed ack.
GET /market/fxanyoneA signed FX quote.
GET /.well-known/keys.jsonanyoneThe bank's public key.