# NANDA Privacy-Preserving Data Exchange ## What This Service Does Shares data between agents with per-recipient disclosure based on trust scores. You upload data once (`/data/prepare`), then share it with any agents (`/data/share`). For each target agent the service decides one of three access levels and returns it in the same response: - `full` — the agent gets the complete data. - `partial` — the agent gets only the fields the owner marked revealable, each with a cryptographic proof (salt + Merkle path) that it belongs to the original document. Hidden fields are never transmitted. - `denied` — the agent gets no data, only a signed denial receipt. Every grant and denial carries an HMAC signature you can check at `/receipts/verify`. ## Base URL ``` https://nanda-privacy-exchange-production.up.railway.app ``` (All paths below are relative to the base URL.) ## Authentication None. All endpoints are open for this hackathon prototype. ## The 3-Step Workflow ``` 1. POST /data/prepare -> returns data_ref (a sha256 content address) 2. POST /data/share -> returns one grant per target agent (full/partial/denied) 3. POST /receipts/verify (optional) -> confirm any receipt is genuine ``` ## Trust Scores (how access is decided) Scores are floats in [0.0, 1.0]. The thresholds depend on the `sensitivity_level` chosen at prepare time: | sensitivity_level | `full` requires | `partial` requires | below that | |---|---|---|---| | `low` | score >= 0.6 | score >= 0.3 | `denied` | | `medium` (default) | score >= 0.8 | score >= 0.5 | `denied` | | `high` | score >= 0.9 | score >= 0.7 | `denied` | Pre-seeded demo agents: `agent-alpha` = 0.95, `agent-beta` = 0.62, `agent-gamma` = 0.10. Any unknown agent defaults to 0.5. You can set any agent's score with `POST /trust/report` (see endpoint 5). ## Endpoints ### 1. Health Check **GET** `/health` — returns 200 when the service is up. ```bash curl https://nanda-privacy-exchange-production.up.railway.app/health ``` Response: ```json {"status": "ok", "service": "nanda-privacy-exchange", "version": "1.0.0"} ``` ### 2. Prepare Data **POST** `/data/prepare` Request body (JSON). Unknown fields are rejected with 422. | field | type | required | meaning | |---|---|---|---| | `data` | string | yes | The payload. If it is a JSON **object with only string values** (e.g. `"{\"k\": \"v\"}"` — note: a JSON string containing JSON), partial disclosure works per-field. Any other string is stored as opaque. | | `sensitivity_level` | `"low"` \| `"medium"` \| `"high"` | no (default `"medium"`) | Selects the trust thresholds (table above). | | `reveal_fields` | array of strings | no (default `[]`) | Field names that `partial`-level agents are allowed to see. Only valid for structured `data`; every name must exist in it. | ```bash curl -X POST https://nanda-privacy-exchange-production.up.railway.app/data/prepare \ -H "Content-Type: application/json" \ -d '{ "data": "{\"dept\": \"finance\", \"region\": \"EU\", \"salary\": \"250000\"}", "sensitivity_level": "medium", "reveal_fields": ["dept", "region"] }' ``` Response (exact shape): ```json { "data_ref": "sha256:cb6cac62a229993ab45247fd01c3e3d4c0fc1f6304281ef0b06583227a9732d6", "sensitivity_level": "medium", "structured": true, "commitment_root": "65f7e2b6835190186eeda6b22c19169ed0c71967f99b5ea7bb59954abf67f3d1", "size_bytes": 55 } ``` Save `data_ref` — you need it to share. The same `data` always produces the same `data_ref` (content addressing). `commitment_root` is `null` for opaque payloads. ### 3. Share Data **POST** `/data/share` Request body (JSON): | field | type | required | meaning | |---|---|---|---| | `data_ref` | string | yes | A `data_ref` returned by `/data/prepare`. Unknown refs return 404. | | `target_agents` | array of strings (1–64 items) | yes | Agent ids to share with. Duplicates are ignored. | ```bash curl -X POST https://nanda-privacy-exchange-production.up.railway.app/data/share \ -H "Content-Type: application/json" \ -d '{ "data_ref": "sha256:cb6cac62a229993ab45247fd01c3e3d4c0fc1f6304281ef0b06583227a9732d6", "target_agents": ["agent-beta"] }' ``` Response: `{"data_ref": ..., "sensitivity_level": ..., "grants": [...]}` with **one grant per agent**. Grant fields: | field | present when | meaning | |---|---|---| | `agent_id` | always | The target agent. | | `access_level` | always | `"full"`, `"partial"`, or `"denied"`. | | `score` | always | The trust score used for the decision. | | `data` | `full` only | The complete original payload string. `null` otherwise. | | `revealed` | `partial` only | Map of field name → `{value, salt, path}`. Empty `{}` for opaque payloads. `null` otherwise. | | `commitment_root` | `partial` only | Merkle root to verify `revealed` against. `null` otherwise. | | `receipt` | always | Signed statement of this decision (see endpoint 6). | | `signature` | always | Hex HMAC over the receipt. | Example grant for `agent-beta` (score 0.62 → `partial` at `medium`), values verbatim from a live run: ```json { "agent_id": "agent-beta", "access_level": "partial", "score": 0.62, "data": null, "revealed": { "dept": { "value": "finance", "salt": "424155d5bd1cbaed21e8a9df55de0aaa", "path": [ ["acbb0736ae1ed94fcf74eee380834b472cc4125484004861ebb28af71a8f5687", true], ["28033876874fe78972f17ecaffee20e8a313bf020a614705718fc47218480b8f", true] ] }, "region": { "value": "EU", "salt": "09d0a06c31ef38ec918d2b8e41cadba0", "path": [ ["9c7bca53d0c95bf78e8deeaeb63da5681483c0baf30a6f1c06aa7b2e9c9316d8", false], ["28033876874fe78972f17ecaffee20e8a313bf020a614705718fc47218480b8f", true] ] } }, "commitment_root": "65f7e2b6835190186eeda6b22c19169ed0c71967f99b5ea7bb59954abf67f3d1", "receipt": { "service": "nanda-privacy-exchange", "data_ref": "sha256:cb6cac62a229993ab45247fd01c3e3d4c0fc1f6304281ef0b06583227a9732d6", "agent_id": "agent-beta", "access_level": "partial", "score": 0.62, "thresholds": {"full": 0.8, "partial": 0.5}, "issued_at": 1783566398.486 }, "signature": "a5dffc945a8880a1089542bbc39a8f867beb9414f5bb474f6b1b1d479509a302" } ``` Note the hidden field `salary` appears nowhere in the grant. ### 4. Check an Agent's Access Level (before sharing) **GET** `/trust/{agent_id}?sensitivity_level=medium` `sensitivity_level` is an optional query parameter (default `medium`). ```bash curl "https://nanda-privacy-exchange-production.up.railway.app/trust/agent-beta?sensitivity_level=high" ``` Response: ```json {"agent_id": "agent-beta", "access_level": "denied", "score": 0.62} ``` (Same agent is `partial` at `medium` — the sensitivity changes the verdict, not the score.) ### 5. Set a Trust Score (mock backend) **POST** `/trust/report` | field | type | required | constraint | |---|---|---|---| | `agent_id` | string | yes | non-empty | | `score` | number | yes | 0.0 to 1.0 inclusive | ```bash curl -X POST https://nanda-privacy-exchange-production.up.railway.app/trust/report \ -H "Content-Type: application/json" \ -d '{"agent_id": "agent-delta", "score": 0.85}' ``` Response: ```json {"agent_id": "agent-delta", "access_level": "full", "score": 0.85} ``` ### 6. Verify a Receipt **POST** `/receipts/verify` Send back a `receipt` object and its `signature` exactly as received from `/data/share`: ```bash curl -X POST https://nanda-privacy-exchange-production.up.railway.app/receipts/verify \ -H "Content-Type: application/json" \ -d '{"receipt": {"service": "nanda-privacy-exchange", "...": "..."}, "signature": "a5dffc..."}' ``` Response: `{"valid": true}`. If any receipt field was modified — even one character — the response is `{"valid": false}`. ## How to Verify a Partial Disclosure (optional but recommended) Each revealed field proves itself against `commitment_root`. Algorithm: 1. `acc = SHA256(b"npe-leaf\x00" + len4(name) + name + len4(value) + value + salt_bytes)` where `len4(x)` is the 4-byte big-endian length of the UTF-8 bytes of `x`, and `salt_bytes = bytes.fromhex(salt)`. 2. For each `[sibling_hex, sibling_is_right]` in `path`, in order: - if `sibling_is_right` is `true`: `acc = SHA256(b"npe-node\x00" + acc + sibling)` - if `sibling_is_right` is `false`: `acc = SHA256(b"npe-node\x00" + sibling + acc)` 3. The field is genuine iff `acc.hex() == commitment_root`. Copy-paste Python: ```python import hashlib def verify_field(name: str, value: str, salt_hex: str, path, root_hex: str) -> bool: def len4(s: str) -> bytes: return len(s.encode()).to_bytes(4, "big") acc = hashlib.sha256( b"npe-leaf\x00" + len4(name) + name.encode() + len4(value) + value.encode() + bytes.fromhex(salt_hex) ).digest() for sib_hex, sib_is_right in path: sib = bytes.fromhex(sib_hex) pair = acc + sib if sib_is_right else sib + acc acc = hashlib.sha256(b"npe-node\x00" + pair).digest() return acc.hex() == root_hex ``` ## Errors | status | when | body shape | |---|---|---| | 404 | `data_ref` not found on `/data/share` | `{"detail": "unknown data_ref '...'"}` | | 422 | Unknown/missing/invalid field in any request body; `reveal_fields` on opaque data; `reveal_fields` naming a field not in `data`; invalid `sensitivity_level` | `{"detail": [{"type": "...", "loc": [...], "msg": "...", "input": ...}]}` (FastAPI validation format) or `{"detail": "message"}` | There are no other error codes. Retry logic is unnecessary; all endpoints are synchronous and answer immediately. ## Limits (read before relying on this) - **Storage and trust scores are in-memory.** A service restart clears all `data_ref`s and any scores you set. Re-prepare after a restart. - **The trust store is a mock** for the hackathon: seeded demo agents, 0.5 default, writable by anyone via `/trust/report`. In production it would be replaced by a real reputation backend (e.g. NANDA Town's trust layer). - **Receipts are HMAC-signed by the service** — they prove the service issued the receipt unmodified; they are not third-party-verifiable signatures. - **Partial disclosure needs structured data**: a JSON object with string values only. Anything else is opaque — partial-level agents then receive an empty `revealed` map and the `data_ref` acts as the content commitment. - Trust decisions are made **at share time**; re-share to apply changed scores.