> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keystoneos.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Custody Protocol Consumer

> The pure Python consumer of custody protocol v1 that this API carries: exact commitments, closed wire schemas and admission rules, executed against the accepted vectors. No route, table, provider or custody state exists behind it.

<Warning>
  **Protocol support, not a custody feature.** Nothing on this page is reachable through the API. No custody endpoint is mounted, no custody table exists, no provider is called and no hold, registration or decision is ever made by this code. `GET /v1/platforms/me/capabilities` still advertises `escrow` alone, and every settlement read still reports `execution.mechanism: "escrow"`. What exists is the hashing, validation and rule logic a later custody mechanism will be built on, proven against the protocol's own vectors.
</Warning>

## What the consumer is

Custody protocol v1 is owned by the parent `custody-protocol` package in the `KeyStone-OS/keystone` repository: its normative specification, EIP-712 type strings, closed JSON schemas, constants and golden vectors. This API consumes one accepted revision of it and recomputes what that revision pins. It never defines a format of its own, and a vector is never edited to make this consumer pass; a disagreement is a defect to return to the parent.

| Pin | Value |
| - | - |
| Repository | `KeyStone-OS/keystone` |
| Accepted commit | `75490a075d9b8d5cf023052874359f016c115e3b` |
| Vectors manifest sha256 | `3ee0d432fc1e7f02493476179a89bed6b25714f31e7c2655d36b49395e1565e8` |
| Normative document sha256 | `b861b9629af38e4726561d13816a64b2a10aee4aaabece25de9e6612e5675541` |
| Fixture copy | `tests/fixtures/custody/upstream/` (87 files: 86 manifest entries plus the manifest), pinned by `tests/fixtures/custody/provenance.json` |

The runtime pin is `ACCEPTED_PROTOCOL` in `keystone/settlement/custody/constants.py`; the artifact tests fail if the fixture copy, the provenance file or this page cite a different commit or digest, and if any artifact byte differs from the manifest.

## Where it lives

| Module | Owns | Parent counterpart |
| - | - | - |
| `keystone/schemas/custody.py` and `custody_*.py` | The 75 published wire schemas as strict Pydantic models: closed, no coercion, numbers refused, exact numeric bounds, the executable refinements JSON Schema cannot express, and the `SCHEMA_REGISTRY` keyed by schema id | `schemas/*.schema.json`, `src/wire/*.ts` |
| `keystone/settlement/custody/constants.py` | Ordinals, matrices, bounds, the EIP-712 type table and typehashes, route tables, transport constants, the accepted pin | `constants.json`, `typehashes.json` |
| `keystone/settlement/custody/errors.py` | Protocol refusal codes, the API error table and mapping, the sanitized `parse_wire` boundary | `src/errors.ts`, `src/api-errors.ts` |
| `keystone/settlement/custody/encoding.py` | Closed RFC 8785 canonicalization, `hash_struct`, `domain_separator`, `signing_digest`, canonical signature form, `recover_typed_signer`, every derived identity | `src/jcs.ts`, `src/hashing.ts`, `src/signing.ts`, `src/typed-data.ts` |
| `keystone/settlement/custody/manifest.py` | `assemble_bundle`, `validate_bundle` in the frozen order, `derive_identities`, `verify_own_terms_inclusion` | `src/bundle.ts` |
| `keystone/settlement/custody/packages.py` | Package payload and binding, `verify_recovery_package`, `verify_package_admission`, package idempotency | `src/package.ts` |
| `keystone/settlement/custody/evidence.py` | `expected_facts`, `apply_fact`, `eligibility_report`, `admit_registration`, signed-fact verification | `src/evidence.ts` |
| `keystone/settlement/custody/completion.py` | The explicit completion predicate | `src/completion.ts` |
| `keystone/settlement/custody/protocol.py` | Action and setup matrices, grant evaluation and `select_view`, `api_request_digest`, `admit_command`, `admit_intake`, `verify_custody_intake`, route templates | `src/actions.ts`, `src/read.ts`, `src/api.ts`, `src/http.ts` |

Function names are Python snake\_case of the parent's names (`validateBundle` is `validate_bundle`, `applyFact` is `apply_fact`). Inputs are custody models or closed raw wire values; outputs are lowercase hex strings, frozen result records or a `CustodyProtocolError` carrying the pinned code. No function reads a clock, a setting, a database or a key, and none performs I/O.

Wire models keep the parent's camelCase keys as attribute names, so a model dump is the exact object the protocol digests. The one keyword, the EVM artifact's `from`, carries an alias; every dump goes through `.wire()`, which applies aliases and omits absent optional fields without inserting nulls.

## What it enforces

* **Commitments.** `manifestHash = hashStruct(CustodyHeader) = bundleId` under the `KeystoneCustodyRegistry` domain pinned by the cash group's chain and Registry. Hashing goes through eth-account and eth-abi; the typehashes, struct hashes, domain separators and signing digests of both profiles' vectors recompute, and the fixture's public test keys reproduce every committed signature byte for byte through the same library.
* **Signatures.** Exactly one encoding: 65 lowercase hex bytes, `r` and `s` in `[1, n-1]`, low `s`, `v` in {27, 28}. The high-s twin and any out-of-range scalar are refused before the recovery library runs; an unrecoverable point is refused as `SIGNATURE_INVALID` with the safe identity and no library text. Hash-only types never get a signing digest.
* **Closed JSON.** RFC 8785 over strings, booleans, null, lists and string-keyed objects. Numbers, `Decimal`, non-string keys, arbitrary objects and lone surrogates are refused; keys sort by UTF-16 code units, not code points. The API request digest `custody-api-request-jcs-keccak256/v1` and the deployment, package, artifact and readiness digests all use it. The escrow `sha256-canonical-json/v1` request digest is a different function and is not an RFC 8785 implementation.
* **Strict schemas.** A JSON number, a coercible boolean or a `Decimal` never becomes a decimal-string field, and numeric zero never becomes the false-only trust label; width overflow, zero commitments, zero trade amounts, checksum casing, whitespace and Unicode normalization are refused, while a zero balance stays representable. A malformed child is a sanitized `SCHEMA_INVALID` naming paths only; the dependent refinements run only on validated children.
* **Admission order.** `validate_bundle` refuses in the frozen order (profile, deployment digest, admitted keys, group domains, timing, topology, commitments, addressed identity); `verify_recovery_package` adds recipient, own-term inclusion and the source signature; `verify_package_admission` binds mandate and acknowledgement to the exact digest sent and the admitted payer key.
* **Evidence.** Exactly five expected facts per header; monotonic versions per fact key; identical replay is a no-op, an equal version with a changed payload conflicts, a lower version is stale, nothing changes after SETTLE or ABORT; expiry equality is invalid; registration is exactly the three non-readiness facts, each current, strictly before cutoff.
* **Projections.** A custodian view carries only its own private terms, the source view both and no account-wide books; the groups are the fixed layout with recomputed ids; `completion.satisfied` must equal the canonical predicate; events pages, exports and provenance are refused when their correlations do not hold rather than repaired.

## Conformance executed here

Every family of the parent's section 16 matrix for the Python API runs in `tests/settlement/custody/` and `tests/schemas/test_custody.py`, each case through the entrypoint its `check` names; an unmapped check fails the run.

| Family | Cases | Entry points |
| - | - | - |
| `identities.json` (hybrid and diagnostic) | fixture rebuild, 5 struct hashes, 12 operation ids, 9 setup operation ids, 3 attempts, 12 signatures per profile | `assemble_bundle`, `hash_struct`, `signing_digest`, `recover_typed_signer`, derived identity functions |
| `packages.json` | 2 custodians | `verify_recovery_package`, `verify_package_admission`, `package_digest_of` |
| `evidence.json` | 40 steps (15 eligibility, 18 apply, 6 register, 1 state change) plus the unknown-bundle view | `apply_fact`, `admit_registration`, `eligibility_report` |
| `negatives.json` | 139: `validateBundle` 44, `schema` 38, `ownTermsInclusion` 6, `signature` 15, `verifyPackage` 14, `verifyAdmission` 5, `identity` 5, `action` 10, `attempt` 2 | the named functions; identity cases also refuse through `require_identity` |
| `api.json` | positives (intake, 11 commands, 5 command reads, 3 reads, 3 completed reads, events, export, admission, 5 errors, 2 grants, 3 view selections, the 31-code error mapping) and 136 cases: `apiSchema` 107, `commandAdmission` 8, `intakeAdmission` 4, `intakeVerify` 7, `viewSelection` 7, `requestDigest` 3 | `parse_wire`, `admit_command`, `admit_intake`, `verify_custody_intake`, `select_view`, `api_request_digest` |
| `jcs.json` | 5 canonicalizations, 3 numeric refusals, plus lone-surrogate refusals in values, keys and nested values per RFC 8785 section 3.2.2.2 | `canonical_json`, `jcs_digest` |

The parent's draft 2020-12 JSON Schemas are also loaded with an independent validator: every positive sample passes both validators, every negative is refused by the models, and the exact set of negatives only the code refuses (numeric upper bounds and the executable refinements) is pinned, so a refinement silently moving into or out of JSON Schema is noticed.

## What is explicitly not claimed

* No route, query or body is served; the route tables in `constants.py` describe the contract a later ticket mounts and are not registered with FastAPI. The custody API error codes are not emitted by this API and do not appear in the [error codes](/guides/error-codes) guide.
* No authorization storage: `select_view` and `closeout_authorized` evaluate grants handed to them; no grant is persisted or looked up.
* No durable command, replay cache or revision counter: `admit_command` and `admit_intake` decide over a supplied stored record and resource state. The ticket that mounts the routes owns authentication, the storage lock, replay before revision under it, transactional acceptance and post-commit dispatch.
* No Registry, Canton or provider effect: eligibility and registration are computations over supplied facts and an explicit time, never an observation that a fact or a native effect occurred.
* No signer and no spending key: the fixture keys are public test material under `tests/fixtures/custody/` and are never copied into runtime modules or configuration.
* No Solidity parity, no live Canton or provider qualification and no native transaction atomicity: those belong to their own tickets.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.