diff --git a/docs/wallet-integration/hosted/getting-started.mdx b/docs/wallet-integration/hosted/getting-started.mdx
index c26e194f1..8072e6d31 100644
--- a/docs/wallet-integration/hosted/getting-started.mdx
+++ b/docs/wallet-integration/hosted/getting-started.mdx
@@ -3,16 +3,12 @@ title: Turnkey Hosted
description: Integrate with VisualSign via Turnkey's hosted enclave service
---
-{/*
-TODO: Fill in when Turnkey integration details are available.
-*/}
-
Turnkey operates VisualSign parser instances in AWS Nitro Enclaves, providing transaction parsing with attestation verification without requiring you to manage enclave infrastructure.
## Prerequisites
-- Turnkey account with API credentials
-- Understanding of [attestation verification](../self-hosted-tee/attestation)
+- Turnkey account with API credentials.
+- Understanding of [attestation verification](../self-hosted-tee/attestation).
## Integration overview
@@ -31,21 +27,105 @@ sequenceDiagram
W->>W: Display to user
```
-## Getting started
+## Attestation verification
-
-Integration details coming soon. Contact Turnkey for early access.
-
+Every parse response carries an AWS Nitro attestation document. To trust the parsed output you must:
-## Attestation verification
+1. Verify the attestation's certificate chain back to the AWS Nitro root CA.
+2. Check the document's PCR values against an allowlist you control.
+3. Verify the parser's ECDSA signature over the parsed payload using the public key from the attestation.
+
+See [Attestation Verification](../self-hosted-tee/attestation) for implementation details of all three steps. The remainder of this page covers (2) — specifically, how to obtain the PCR values you should put in your allowlist.
+
+## PCR allowlist values
+
+The PCRs are determined by the qos enclave runtime version Turnkey has deployed (currently TVC `v2026.2.6`, qos commit [`d866f2c6cbc58cc08c24eab4828f0824ad16a226`](https://github.com/tkhq/qos/commit/d866f2c6cbc58cc08c24eab4828f0824ad16a226)). You can reproduce them locally from that qos rev — never trust an externally supplied PCR file without verifying it.
+
+### Generating PCRs from source
+
+The `scripts/extract-nitro-pcrs.sh` tool in this repo clones tkhq/qos at the deployment rev declared in `src/Cargo.toml`, builds `qos_enclave`, and extracts `/nitro.pcrs`:
+
+```bash
+./scripts/extract-nitro-pcrs.sh
+```
+
+The deployment rev lives in a marker comment in `src/Cargo.toml`:
+
+```toml
+# qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226
+```
+
+This is distinct from the `rev = "..."` on each `qos_*` crate in the same file, which pins the qos library code that `parser_app` compiles against and does not affect PCRs. To audit a prospective deployment bump before updating the marker, pass `--rev` explicitly:
+
+```bash
+./scripts/extract-nitro-pcrs.sh --rev <40-char-qos-sha>
+```
+
+#### Prerequisites
-Even with a hosted solution, you must verify attestations. The attestation document proves:
+- Docker 26+
+- Git and GNU Make
+- ~8 GB of free RAM and ~15 GB of free disk (StageX base images, a clean cargo compile, and the qos OCI output).
-1. The response came from a genuine AWS Nitro Enclave
-2. The enclave is running the expected VisualSign parser code
-3. The parsed output hasn't been tampered with
+#### Options
+
+| Flag | Default | Purpose |
+| --- | --- | --- |
+| `--cargo-toml PATH` | `src/Cargo.toml` | Source of the deployment-rev marker. |
+| `--qos-dir PATH` | `mktemp -d` | qos checkout location. The default is ephemeral and removed on exit; pass an explicit path to keep cached cargo and docker layers between runs. The script verifies the directory's `origin` URL matches `tkhq/qos` before mutating it. |
+| `--output PATH` | `out/nitro.pcrs` | Where to write the PCRs file. |
+| `--skopeo-image REF` | digest-pinned `quay.io/skopeo/stable` | Container image used to convert the OCI layout to a docker-loadable tar (no host `skopeo` install needed). |
+| `--rev REV` | (from `Cargo.toml` marker) | Override the rev. |
+
+#### How it works
+
+1. **Read the rev** — the script extracts a 40-character hex from the `# qos-deployment-rev = ...` line in `src/Cargo.toml`. If `--rev` is supplied it bypasses this step.
+2. **Check out qos** — clones `tkhq/qos` into the work dir and checks out the rev. If `--qos-dir` points at an existing clone whose `origin` is the upstream `tkhq/qos` URL and whose `HEAD` already matches the rev, the script reuses it; if the origin differs the script refuses to touch the directory.
+3. **Build `qos_enclave`** — runs `make out/qos_enclave/index.json` inside the qos checkout. This is qos's deterministic [StageX](https://codeberg.org/stagex/stagex)-based build: it produces a `nitro.eif` and `nitro.pcrs` via `eif_build` and bakes them into the OCI image. The full pipeline lives in qos's [`qos_enclave` Containerfile](https://github.com/tkhq/qos/blob/main/src/images/qos_enclave/Containerfile).
+4. **Extract `nitro.pcrs`** — runs the pinned skopeo image (as your host user, so the staging files clean up) to convert the OCI directory output to a docker-archive tar, loads it under a unique per-run tag, copies `/nitro.pcrs` out of a freshly created container, and removes both the container and the image on exit.
+
+#### What the PCRs measure
+
+PCR0 and PCR1 measure the Nitro EIF and kernel, and PCR2 measures the boot ramdisk that contains qos's `init` binary. The `parser_app` binary is loaded into qos at runtime over vsock and is **not** part of these PCR measurements — its integrity is established separately via the qos manifest, which is covered at [Level 3 attestation](../self-hosted-tee/attestation#level-3-manifest-verification). PCR3 in a live attestation measures the AWS IAM role and is intentionally absent from `nitro.pcrs`.
+
+### Cross-checking against a live prod attestation
+
+You can confirm the locally built PCRs match what the production enclave actually serves by fetching a fresh attestation through [visualsign-turnkeyclient](https://github.com/anchorageoss/visualsign-turnkeyclient).
+
+```bash
+# 1. Build the client
+git clone https://github.com/anchorageoss/visualsign-turnkeyclient.git
+cd visualsign-turnkeyclient
+go build -o ./bin/visualsign-turnkeyclient .
+
+# 2. Fetch a real prod attestation and print PCRs (any well-formed unsigned
+# tx works — the parser returns the same enclave attestation regardless).
+./bin/visualsign-turnkeyclient verify \
+ --host https://api.turnkey.com \
+ --organization-id \
+ --key-name \
+ --chain CHAIN_ETHEREUM \
+ --unsigned-payload \
+ --debug 2>&1 | grep -E '^[[:space:]]+PCR\[[012]\]'
+
+# 3. Compare against the script output.
+./scripts/extract-nitro-pcrs.sh --output /tmp/local.pcrs
+cat /tmp/local.pcrs
+```
+
+If both sets of PCR0/PCR1/PCR2 hex values match, the local build reproduces the deployed runtime byte for byte.
+
+## Reproducibility
+
+The qos build is deterministic. Running the script twice should produce byte-identical `nitro.pcrs` files. Pass `--qos-dir` to the second run so the cargo and docker layer caches are reused — the second build should be a no-op:
+
+```bash
+./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.a --qos-dir /tmp/qos
+./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.b --qos-dir /tmp/qos
+diff /tmp/pcrs.a /tmp/pcrs.b
+```
-See [Attestation Verification](../self-hosted-tee/attestation) for implementation details.
+If the diff is empty, the build reproduced.
## API reference
diff --git a/docs/wallet-integration/self-hosted-tee/attestation.mdx b/docs/wallet-integration/self-hosted-tee/attestation.mdx
index 1c29dc29b..abbb9e593 100644
--- a/docs/wallet-integration/self-hosted-tee/attestation.mdx
+++ b/docs/wallet-integration/self-hosted-tee/attestation.mdx
@@ -241,13 +241,13 @@ func verifyPCRs(pcrs map[int][]byte) error {
### Updating your allowlist
-When the parser is updated, PCR values change. Follow this process:
+When the deployed qos enclave runtime is updated, PCR values change. Follow this process:
-1. Subscribe to parser release announcements
-2. Verify new PCR values against published hashes
-3. Add new PCRs to your allowlist
-4. Deploy to production
-5. Remove old PCRs after migration completes
+1. Watch for deployment announcements that change the qos runtime rev (the rev whose EIF boots in the enclave, not the qos library rev parser_app links against — see [Generating PCRs from source](../hosted/getting-started#generating-pcrs-from-source) for the distinction).
+2. Reproduce the new PCR values: run `scripts/extract-nitro-pcrs.sh` with the updated deployment rev and confirm the script's output matches the values published with the release.
+3. Add new PCRs to your allowlist.
+4. Deploy to production.
+5. Remove old PCRs after migration completes.
### Supporting multiple versions
diff --git a/scripts/extract-nitro-pcrs.sh b/scripts/extract-nitro-pcrs.sh
new file mode 100755
index 000000000..1884b0074
--- /dev/null
+++ b/scripts/extract-nitro-pcrs.sh
@@ -0,0 +1,156 @@
+#!/usr/bin/env bash
+# Build the tkhq/qos enclave image at the deployment rev declared in
+# src/Cargo.toml (the `# qos-deployment-rev = …` marker, not the library
+# `rev = "..."` on each qos crate) and extract /nitro.pcrs from the
+# resulting OCI image.
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
+
+CARGO_TOML="$REPO_ROOT/src/Cargo.toml"
+OUTPUT="$REPO_ROOT/out/nitro.pcrs"
+SKOPEO_IMAGE="quay.io/skopeo/stable@sha256:4d60d6c00b62b463d0a99a7aeedc49358a32c8222540c72d561606e188afb168"
+QOS_REMOTE="https://github.com/tkhq/qos.git"
+QOS_DIR=""
+REV=""
+
+QOS_DIR_AUTO=0
+STAGE_DIR=""
+CID=""
+DOCKER_TAG=""
+
+usage() {
+ cat <&2
+ exit 1
+}
+
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --cargo-toml) CARGO_TOML="$2"; shift 2 ;;
+ --qos-dir) QOS_DIR="$2"; shift 2 ;;
+ --output) OUTPUT="$2"; shift 2 ;;
+ --skopeo-image) SKOPEO_IMAGE="$2"; shift 2 ;;
+ --rev) REV="$2"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) die "Unknown argument: $1" ;;
+ esac
+ done
+}
+
+read_rev() {
+ [[ -f "$CARGO_TOML" ]] || die "Cargo.toml not found: $CARGO_TOML"
+ local marker
+ marker=$(grep -oE '^#[[:space:]]*qos-deployment-rev[[:space:]]*=[[:space:]]*[0-9a-f]{40}' "$CARGO_TOML" || true)
+ [[ -n "$marker" ]] || die "No '# qos-deployment-rev = ...' marker in $CARGO_TOML"
+ REV=$(echo "$marker" | grep -oE '[0-9a-f]{40}')
+}
+
+ensure_qos_checkout() {
+ if [[ -z "$QOS_DIR" ]]; then
+ QOS_DIR="$(mktemp -d -t visualsign-qos.XXXXXX)"
+ QOS_DIR_AUTO=1
+ fi
+
+ if [[ -d "$QOS_DIR/.git" ]]; then
+ local origin
+ origin="$(git -C "$QOS_DIR" remote get-url origin 2>/dev/null || true)"
+ [[ "$origin" == "$QOS_REMOTE" ]] \
+ || die "qos checkout $QOS_DIR has origin '$origin'; expected '$QOS_REMOTE'. Refusing to mutate."
+ local head
+ head="$(git -C "$QOS_DIR" rev-parse HEAD)"
+ if [[ "$head" == "$REV" ]]; then
+ echo "Reusing qos checkout at $QOS_DIR (HEAD=$head)" >&2
+ return
+ fi
+ [[ -z "$(git -C "$QOS_DIR" status --porcelain)" ]] \
+ || die "qos checkout $QOS_DIR has uncommitted changes"
+ echo "Updating qos checkout in $QOS_DIR to $REV" >&2
+ git -C "$QOS_DIR" fetch --quiet origin "$REV"
+ git -C "$QOS_DIR" checkout --quiet "$REV"
+ return
+ fi
+
+ echo "Cloning tkhq/qos into $QOS_DIR" >&2
+ git clone --quiet "$QOS_REMOTE" "$QOS_DIR"
+ git -C "$QOS_DIR" checkout --quiet "$REV"
+}
+
+build_qos_enclave() {
+ echo "Building qos_enclave at rev $REV (may take several minutes)..." >&2
+ make -C "$QOS_DIR" out/qos_enclave/index.json
+}
+
+extract_pcrs() {
+ local oci_dir="$QOS_DIR/out/qos_enclave"
+ [[ -f "$oci_dir/index.json" ]] || die "qos build did not produce $oci_dir/index.json"
+
+ STAGE_DIR="$(mktemp -d -t visualsign-pcrs.XXXXXX)"
+ DOCKER_TAG="qos-enclave:extract-$$-${RANDOM}"
+
+ docker run --rm \
+ --user "$(id -u):$(id -g)" \
+ -v "$oci_dir:/src:ro" \
+ -v "$STAGE_DIR:/dst" \
+ "$SKOPEO_IMAGE" \
+ copy "oci:/src:latest" "docker-archive:/dst/qos_enclave.tar:$DOCKER_TAG"
+
+ docker load -i "$STAGE_DIR/qos_enclave.tar"
+ CID="$(docker create "$DOCKER_TAG")"
+
+ mkdir -p "$(dirname "$OUTPUT")"
+ docker cp "$CID:/nitro.pcrs" "$OUTPUT"
+ [[ -s "$OUTPUT" ]] || die "nitro.pcrs not found in qos-enclave image"
+}
+
+cleanup() {
+ if [[ -n "$CID" ]]; then
+ docker rm "$CID" >/dev/null 2>&1 || true
+ fi
+ if [[ -n "$DOCKER_TAG" ]]; then
+ docker rmi "$DOCKER_TAG" >/dev/null 2>&1 || true
+ fi
+ if [[ -n "$STAGE_DIR" ]]; then
+ rm -rf "$STAGE_DIR"
+ fi
+ if [[ "$QOS_DIR_AUTO" -eq 1 && -n "$QOS_DIR" ]]; then
+ rm -rf "$QOS_DIR"
+ fi
+}
+
+main() {
+ parse_args "$@"
+ if [[ -z "$REV" ]]; then
+ read_rev
+ fi
+ trap cleanup EXIT
+ ensure_qos_checkout
+ build_qos_enclave
+ extract_pcrs
+ echo "Wrote $OUTPUT:" >&2
+ cat "$OUTPUT"
+ echo
+}
+
+main "$@"
diff --git a/src/Cargo.toml b/src/Cargo.toml
index d0d6ff8c9..d2c433263 100644
--- a/src/Cargo.toml
+++ b/src/Cargo.toml
@@ -40,6 +40,18 @@ repository = "https://github.com/anchorageoss/visualsign-parser"
[workspace.dependencies]
# See https://doc.rust-lang.org/nightly/cargo/reference/workspaces.html#the-dependencies-table
+
+# qos dependencies. Two distinct revs matter:
+# * Library rev (the `rev = "..."` on each crate below) — compiled into parser_app
+# so it links against qos_core etc.
+# * Deployment rev — the qos version whose EIF actually boots in the Nitro enclave
+# and is measured by PCRs in attestation documents. They are not required to
+# match. The deployment rev currently tracks TVC v2026.2.6:
+# https://github.com/tkhq/qos/commit/d866f2c6cbc58cc08c24eab4828f0824ad16a226
+# PCRs from upstream CI:
+# https://github.com/tkhq/qos/actions/runs/22202233633/job/64217796901
+# scripts/extract-nitro-pcrs.sh reads the marker line below to reproduce them.
+# qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226
qos_client = { git = "https://github.com/tkhq/qos.git", rev = "365ba7ed529bc5af617bcfb27502c3efce8b37ae", default-features = false }
qos_core = { git = "https://github.com/tkhq/qos.git", rev = "365ba7ed529bc5af617bcfb27502c3efce8b37ae", default-features = false }
qos_crypto = { git = "https://github.com/tkhq/qos.git", rev = "365ba7ed529bc5af617bcfb27502c3efce8b37ae", default-features = false }