Clinic
Contract role ClinicCreates the record and requests the referral. In the guided demo this is Riverbend Family Clinic. The deploy kit function-call key may call create_record and request_referral.
How TendAI Custody records patient-approved clinic-to-hospital referrals on NEAR testnet, what the contract stores, and what this hackathon demo does not do.
01 / Start here
The landing page calls the guided path a 3-minute demo. You play each person. The chart is fictional. Three places are enough to check the entry.
Privacy is the primary track. The summary runs in NEAR AI Cloud, and the chain stores hashes and the custody log. Security is the secondary track. Staff sign with function-call keys that cannot consent for the patient.
02 / Overview
TendAI Custody is a patient-approved referral from a clinic to a hospital. The patient holds the approval. The clinic encrypts the chart before upload. NEAR testnet keeps a chain-of-custody log: who shared which hash, with whom, on what scope, until when, and whether it was opened or revoked.
Before approving, the patient can read a short summary from NEAR AI Cloud. The server keeps that summary only when the receipt says provider_tee. The enclave writes the summary. It does not re-encrypt the chart for the hospital. The patient's side wraps a data key to the hospital's public key from the contract.
Revoking stops the next open through this system. It cannot erase a copy that was already downloaded. Removing identifiers for an app copy is best effort, with a preview. This demo is designed around HIPAA principles. All data shown is fictional.
03 / How it works
Four parties show up in a referral. The web app is the fifth piece: it is the Next.js client and server, not a contract role. A second opinion, Northgate Heart Institute in the story, uses the same key shape as the hospital.
Creates the record and requests the referral. In the guided demo this is Riverbend Family Clinic. The deploy kit function-call key may call create_record and request_referral.
Receives the referral. The contract has no separate Hospital role, so the hospital account is registered as Clinic. Its function-call key may call request_open and submit_reply. In the story this is St. Aster Hospital.
Approves, declines, creates a further share, and revokes. Those calls attach exactly 1 yoctoNEAR, so the demo patient key is a full-access key. In the story this is Maya Okafor. The chart is fictional.
Can receive a DeidApp share. Its function-call key may call request_open. In the story this is PulseFit. It gets a stripped copy only if the patient sends one.
The clinic encrypts the selected sections on its own side and calls create_record. The contract stores section hashes, not the chart.
request_referral names the hospital, the sections, and a purpose hash. The share stays Requested.
The patient reads the NEAR AI Cloud summary, then approve_share with 1 yoctoNEAR. The grant can be narrower than the request, and it has an expiry.
request_open writes a 120 second ticket for the hospital or the patient. The web app releases the ciphertext only when that ticket checks out.
revoke_share sets that share to Revoked. It does not delete a file someone already saved.
A later request_open panics ERR_REVOKED. The attack demo recorded that refusal on testnet.
Section keys are history, meds, labs, imaging, and notes. The patient can grant a subset. A second-opinion share and a transfer are patient create_share calls. A PulseFit copy is a DeidApp share and needs a receipt hash. A hospital consult comes back as submit_reply, which stores a reply hash, not the note.
04 / Architecture
The Rust contract crate is referral-relay, version 2.0.0, on near-sdk 5.30.1. Public methods use deny_unknown_arguments. There is no upgrade method and no migration from schema 1. The schema 2 account for this demo is rr191f8afd-contract.testnet. The owner is rr191f8afd-owner.testnet. The NEP-297 standard name is referral_relay and the event version is 2.0.0. The deploy kit records wasm referral_relay.wasm at 278564 bytes, SHA-256 67b2ec90989a2f6b6afbebb1b77061b479c65f787943f82ac0cd61ab478e5d3b.
The contract publishes staff_method_names as create_record, request_referral, request_open, request_owner_open, and submit_reply. The keys actually added by the deploy kit are narrower. The clinic key cannot call request_open. The attack demo saw AccessKeyMethodNameMismatchActionError when that key tried.
| Signer | Methods on the function-call key | Consent |
|---|---|---|
| Clinic, and Lakeside uses the same list | create_record, request_referral | Cannot approve or revoke. |
| Hospital | request_open, submit_reply | Cannot approve or revoke. |
| Second opinion | request_open, submit_reply | Cannot approve or revoke. |
| App (PulseFit) | request_open | Cannot approve or revoke. |
| Patient in the guided demo | Full-access key, not the function-call list | approve_share, decline_share, create_share, revoke_share, and revoke_all_for_record need 1 yoctoNEAR. |
Encryption is envelope version 1, algorithm x25519-hkdf-sha256+xchacha20poly1305. A random 32 byte data key encrypts the plaintext with XChaCha20-Poly1305. Each recipient gets that key wrapped to their X25519 public key. The associated data binds the record and the section. The SHA-256 of the canonical envelope is what the contract can store as a hash. The encryption context is SHA-256 of the domain referral_relay/v2/context, the contract id, the caller, the method tag, and the 32 byte request id. The same helper lives in contract/src/context.rs and web/lib/near/context.ts.
The summary and the de-identification model are z-ai/glm-5.3-flash. assertTeeModel refuses any other id, including Incognito model ids, at startup. Completions are non-streaming because the gateway signs streamed bodies by default, and only provider_tee is the signature this app treats as the sealed model. De-identification runs a local scrub, then the TEE pass, then a privacy-filter scan. A leftover identifier or a span scored at least 0.5 blocks the share. The patient sees a preview. The gate does not claim it catches every shape.
The web app is Next.js 16. It reads the contract with near-api-js and can connect a testnet wallet with @hot-labs/near-connect. In the guided demo, clinic and hospital actions are signed on the server with function-call keys. The patient path on that demo uses the server full-access key. A connected wallet can call self_register_patient on the live contract. If that call fails as method-not-found, the wallet screen says this contract account did not accept self_register_patient.
05 / Real and simulated
Live means a testnet account, a testnet transaction, or a call this demo makes to NEAR AI Cloud. Fictional means the people and the chart. Simulated, in this table, means the product shows a check it did not run, or the chain stores a hash it does not verify.
| Piece | Kind | Where this is grounded |
|---|---|---|
| rr191f8afd-contract.testnet, schema 2, owner rr191f8afd-owner.testnet | Live on testnet | docs/ATTACK-DEMO-RESULT.md names the owner. deploy-kit/provision.sh names the contract PREFIX-contract.testnet. get_owner on this account returns that owner. |
| create_record, request_referral, approve_share, request_open, submit_reply, revoke_share, and the refused open | Live on testnet | docs/ATTACK-DEMO-RESULT.md, 2026-11-14, 11 of 11. Fictional payloads. No deploy in that run. |
| NEP-297 events, standard referral_relay, version 2.0.0 | Live on testnet | contract/src/events.rs. Unit test register_create_and_referral_events. |
| Section encryption and re-wrap to the hospital public key | Live in the demo browser, keys seeded for the demo | web/lib/crypto/envelope.ts. Demo actor keys are derived from DEMO_ENC_SEED. The seed is not in the repo. |
| NEAR AI Cloud summary on z-ai/glm-5.3-flash when the demo runs that call | Live call, fictional chart | web/lib/tee/completion.ts. Kept only for signatureKind provider_tee. |
| Maya Okafor, Riverbend Family Clinic, St. Aster Hospital, Northgate Heart Institute, Lakeside Family Medicine, Dr. Reyes, PulseFit, and every chart | Fictional | web/lib/fixtures.ts. The attack demo registered those organization names as actors. The people are not chart data on chain. |
| Guided demo signing | Fictional demo keys on the server | README Security. The server picks the signer from the role. The request cannot pick an account id. |
| On-chain tee_receipt_hash | Hash only. The chain does not verify the TEE | contract/DESIGN-V2.md. The app checks provider_tee before it will approve from this server. |
| Verification page rows for RTMR3, nonce binding, compose measurement, model measurements, browser encryption, and the on-chain receipt | Not checked on that page | web/lib/near/proof.ts leaves those rows not-checked. Signature kind can pass. Gateway TCB can pass when the summary included a server check through the NEAR AI SDK. This browser does not verify the quote. |
| Attack list on the proof page | A recording, not a live run | web/components/wireframe/activity/verify-panel.tsx. The source table is docs/ATTACK-DEMO-RESULT.md. |
| Nonce replay, key substitution, and a flipped ciphertext byte | Local checks in the attack script | docs/ATTACK-DEMO-RESULT.md. Those three rows have no transaction. |
| Landing proof | Reference-run transactions | web/components/landing/proof.tsx links the guided-demo transactions, the testnet contract, and the GitHub repository. It does not label them example data. |
| README event version 1.0.0 | Older text in the repo | README.md still names an older contract account and event version 1.0.0. web/lib/fixtures.ts names the schema 2 account. The contract source on this commit is schema 2. |
| Shade Agents, Chain Signatures, and NEAR Intents | Not used | README.md. |
06 / Security and privacy
The threat model in docs/threat-model.md pairs each risk with a control. Unauthorized opens require the caller to be the recipient or the patient. Revoked and expired shares fail in request_open and check_ticket. Tickets are bound to a nonce hash and last 120 seconds, and the server rejects a replayed nonce. Staff keys are limited by method name, and consent needs 1 yoctoNEAR. Recipient encryption keys are read from the contract. Artifact hashes are checked before decrypt. The summary path requires provider_tee. phi-scan looks for fixture identifiers in contract state and in logs.
docs/ATTACK-DEMO-RESULT.md is the latest full run: 2026-11-14, 17:05 to 17:06 Bangkok, after rebasing onto 546f5ce, attack share 14, 11 of 11 passed. create_record returned record 8 (EPR5jHDRpxJSLdqCjg9a8EP6d93QKiyeeMgG2doZa2Eg). request_referral returned share 14 (2uJt3QZrsrabGMqkxsptbGaENZSX6dY8cPg7ybegui8t). An unauthorized open returned ERR_UNAUTHORIZED (C5oaSkJTX61yDwc4y6H3wyKdXL1AnPcFrQ8Mbjn5Yi6k). approve_share was accepted (B2XvGsCzWiZRkvbg4auKCUFQEnxB96JQUjXUvcai4mUn). The hospital ticket open was accepted (DxkQ7GPqtr8sX384sKM5vMmuoRFTMk1ARhNkCvYoSUqn). submit_reply returned reply 16 (CrMs24p6wUp3fbLv3PcSkMnkYMEonW4pPMq5o7oTc67Y). revoke_share was accepted (6BUCRw2VQ6xndpbgyLAczDPzrLSZ2EmRcFggMSoyT9HA). The later open panicked ERR_REVOKED (CGaCe4yTiSc5uc9Rtwuc82BFy72bZ5sbdwq8r4QjGf8L). Nonce replay, key substitution, and the flipped ciphertext byte passed with no transaction.
That run is not a claim that every attack was tried. An earlier pass the same day scored 10 of 11 because the clinic function-call key cannot call request_open. phi-scan on the same contract reported actors=6, records=7, shares=13, events=0, status=complete, schema=2, terms=50, hits=0. Schema 2 has no get_timeline, so events=0 with status=complete is the expected coverage.
| On the contract | Off the contract |
|---|---|
| Account ids, roles, organization display names, encryption public keys | Chart text, summary text, and the data encryption key |
| Section hashes, purpose hashes, artifact hashes, receipt hashes | Ciphertext blobs in process memory, a local file, and the browser session |
| Scope, expiry, status, grant epoch, open counters, 120 second ticket nonces | The server's used-nonce set, visitor cookie, and NEAR AI spend counter |
| NEP-297 custody events | Support tickets in Redis, when Redis is configured. Those are not the chart. |
Known gaps are written down in docs/KNOWN-LIMITS.md. The contract accepts any 64-hex receipt from a transaction signed outside this app. A saved visitor cookie from before a sign can still pass on another instance. The daily NEAR AI budget is not one shared counter across every Vercel instance unless Upstash is configured. Actors are marked active at registration. There is no call that deactivates an actor. create_share checks that a parent share belongs to the same record. It does not check the parent's status. An empty section grant is accepted for DeidApp, and approve_share accepts an empty granted list.
07 / Claims
This is the map for the claims above. Paths are in this repository. Transaction hashes point at the testnet explorer.
| Claim | Code | Test or record |
|---|---|---|
| Fresh state is schema 2, with no migration from schema 1. | contract/src/lib.rs (SCHEMA_VERSION) and contract/DESIGN-V2.md | contract/src/unit_tests.rs fresh_state_is_schema_v2_without_a_migration_path |
| Events use NEP-297 standard referral_relay at version 2.0.0. | contract/src/events.rs | contract/src/unit_tests.rs register_create_and_referral_events |
| An unregistered account or a patient cannot create a record. | contract/src/lib.rs create_record | contract/src/unit_tests.rs unregistered_account_cannot_create_record and patient_cannot_create_record |
| approve_share, decline_share, create_share, revoke_share, and revoke_all_for_record require exactly 1 yoctoNEAR. | contract/src/lib.rs assert_one_yocto on those methods | contract/src/unit_tests.rs approve_without_one_yocto_fails; contract/tests/sandbox.rs rejects a function-call key on approve_share |
| The patient can narrow the granted sections and cannot add one. Expiry past 365 days is rejected. | contract/src/lib.rs approve_share and require_future_expiry | contract/src/unit_tests.rs approve_narrows_scope_and_sets_active, approve_cannot_add_a_section, approve_rejects_expiry_past_365_days |
| A caller who is not the recipient or the patient cannot open. | contract/src/lib.rs request_open | contract/src/unit_tests.rs unauthorized_open_is_rejected. Attack demo transaction C5oaSkJTX61yDwc4y6H3wyKdXL1AnPcFrQ8Mbjn5Yi6k in docs/ATTACK-DEMO-RESULT.md |
| After revoke, the next open fails with ERR_REVOKED. | contract/src/lib.rs revoke_share and request_open | contract/src/unit_tests.rs revoke_then_open_fails and contract/tests/sandbox.rs. Attack demo transaction CGaCe4yTiSc5uc9Rtwuc82BFy72bZ5sbdwq8r4QjGf8L |
| An expired share fails open with ERR_EXPIRED. The open ticket lasts 120 seconds. | contract/src/lib.rs TICKET_TTL_NS and effective status | contract/src/unit_tests.rs lazy_expiry_blocks_open and open_by_recipient_and_check_ticket_window. Sandbox fast-forward covers expiry. |
| Record-wide revoke bumps the grant epoch and does not walk child shares. | contract/DESIGN-V2.md and revoke_all_for_record | contract/src/unit_tests.rs revoke_all_epoch_covers_requested_children_and_replies |
| A DeidApp share requires an App recipient and a receipt hash. | contract/src/lib.rs create_share | contract/src/unit_tests.rs deid_app_requires_a_receipt and deid_app_rejects_a_clinic_recipient |
| Chart sections are encrypted with X25519, HKDF-SHA256, and XChaCha20-Poly1305. A swapped AAD or a wrong key does not decrypt. | web/lib/crypto/envelope.ts | web/lib/crypto/envelope.test.ts (AAD swap fails, wrong key fails) |
| Wrapping uses the encryption public key stored on the contract when a server-offered key differs. | web/lib/relay/keys.ts | web/lib/relay/keys.test.ts. The attack demo key-substitution check is local and has no transaction. |
| The summary model must be z-ai/glm-5.3-flash. Other model ids are refused at startup. | web/lib/relay/policy.ts and web/lib/tee/completion.ts | web/lib/relay/policy.test.ts |
| A summary is kept only when the receipt says signatureKind provider_tee. | web/lib/tee/live.ts and the summary route | web/lib/tee/summary-retry.test.ts and web/lib/tee/completion-gate.test.ts |
| De-identification blocks a leftover fixture identifier and a classifier span at or above 0.5. | web/lib/deid/pipeline.ts | web/lib/deid/pipeline.test.ts |
| An open without a fresh ticket is HTTP 403. Reusing a nonce is HTTP 403. | web/lib/open/handle.ts | web/lib/open/handle.test.ts. Attack demo nonce replay is local: first HTTP 200, second HTTP 403. |
| The 2026-11-14 attack demo passed 11 of 11 on the schema 2 contract, and phi-scan reported zero fixture identifiers. | scripts/attack-demo.ts and scripts/phi-scan.ts | docs/ATTACK-DEMO-RESULT.md |
08 / Contract
Account rr191f8afd-contract.testnet on NEAR testnet. Arguments below are the JSON arguments from contract/src/lib.rs. Hashes and encryption public keys are 64 hex characters (32 bytes). near-sdk U64 values are JSON strings. A JSON object with an unknown field fails to deserialize. Views do not check the caller.
| Method | Kind | Arguments | Rule |
|---|---|---|---|
new | init | owner: AccountId | Predecessor must be the contract account. Owner must be a different account. No migrate method. |
staff_method_names | view | none | Returns create_record, request_referral, request_open, request_owner_open, submit_reply. |
get_owner | view | none | Returns the owner account. |
get_owner_proposal | view | none | Returns proposed, nonce, and expires_at_ns, or null. |
propose_owner | call | new_owner: AccountId, expires_at_ns: U64 | Owner only. Exactly 1 yoctoNEAR. Expiry is in the future and at most seven days out. Returns the proposal nonce. |
accept_owner | call | proposal_nonce: U64 | Proposed owner only. Exactly 1 yoctoNEAR. Must be strictly before expiry. |
cancel_owner | call | proposal_nonce: U64 | Owner only. Exactly 1 yoctoNEAR. |
register_actor | call | account_id: AccountId, role: Clinic | Patient | App, display_name: string, enc_pubkey: 64 hex | Owner only. Insert only. Deposit must cover the new row. Excess is refunded. Display name must look like an organization, at most 64 characters. |
self_register_patient | call | enc_pubkey: 64 hex | Caller registers only itself as Patient and pays for that row. |
rotate_enc_key | call | enc_pubkey: 64 hex | Self only. The same key is a no-op. Future shares use the new key. Old shares are not re-wrapped. |
encryption_context | view | caller: AccountId, method: CreateRecord | RequestReferral | CreateShare | SubmitReply, request_id: 64 hex | Returns the hex SHA-256 context. The numeric id is not an input. |
get_operation | view | account_id: AccountId, method: CreateMethod, request_id: 64 hex | Returns the first result for that caller, method, and request id, if any. |
create_record | call | request_id: 64 hex, patient: AccountId, sections: { key, cipher_hash }[] | Caller must be Clinic. Patient must be Patient. Section keys are history, meds, labs, imaging, notes. A repeated request id with the same arguments returns the original id and emits nothing. |
request_referral | call | request_id: 64 hex, record_id: U64, expected_epoch: U64, to: AccountId, requested_sections: string[], purpose_hash: 64 hex | Caller must be the clinic that created the record. Recipient must be a different Clinic-role actor. Returns a share id. Status is Requested. |
approve_share | call | share_id: U64, granted_sections: string[], expires_at_ns: U64, read_only: bool, artifact_hash: 64 hex, tee_receipt_hash: 64 hex | Patient only. Exactly 1 yoctoNEAR. Grant must be a subset of the request. Expiry must be in the future and at most 365 days out. The contract checks that the receipt hash is 64 hex. It does not verify a TEE attestation. |
decline_share | call | share_id: U64 | Patient only. Exactly 1 yoctoNEAR. Share must be Requested. |
create_share | call | request_id, record_id, expected_epoch, to, kind: SecondOpinion | Transfer | DeidApp, sections, expires_at_ns or null, read_only, purpose_hash, artifact_hash, tee_receipt_hash or null, parent or null | Patient only. Exactly 1 yoctoNEAR. DeidApp requires an App recipient and a receipt hash. SecondOpinion and Transfer require a Clinic recipient. An empty section list is allowed only for DeidApp. |
request_open | call | share_id: U64, nonce_hash: 64 hex | Caller must be the recipient or the patient. Share must be active, in the current grant epoch, and not expired. Replaces the ticket slot. Ticket lifetime is 120 seconds. |
request_owner_open | call | record_id: U64, nonce_hash: 64 hex | Caller must be the patient or the clinic that created the record. |
submit_reply | call | request_id: 64 hex, share_id: U64, expected_epoch: U64, reply_hash: 64 hex | Recipient of a Referral or SecondOpinion. The share must still be openable. Creates a Reply share back to the patient. The reply text is not stored. |
revoke_share | call | share_id: U64 | Patient only. Exactly 1 yoctoNEAR. Active or Requested only. Does not revoke child shares. |
revoke_all_for_record | call | record_id: U64, expected_epoch: U64 | Patient only. Exactly 1 yoctoNEAR. Passing the current epoch bumps it and emits one record_grants_revoked event. The call does not walk the shares. Passing the epoch this call already produced returns that epoch and emits nothing. |
get_actor | view | account_id: AccountId | Account, role, display name, encryption public key, active flag, key version. |
list_actors | view | from_index: u32, limit: u32 | Limit is capped at 50. A limit of 0 returns an empty list. |
get_record | view | record_id: U64 | Sections are key plus cipher hash. Also returns grant epoch, share count, and open counters. |
get_share | view | share_id: U64 | Includes status and effective_status. Expired is computed from the block timestamp. |
list_shares_for_record | view | record_id: U64, from_index: u32, limit: u32 | Pages that record's share index. Limit capped at 50. |
list_shares_for_recipient | view | account_id: AccountId, from_index: u32, limit: u32 | Pages shares addressed to that account. Limit capped at 50. |
list_pending_for_patient | view | patient: AccountId, from_index: u32, limit: u32 | Pending shares for that patient. Limit capped at 50. |
check_ticket | view | share_id: U64, account_id: AccountId, nonce_hash: 64 hex | True only when the share is effectively active and the nonce matches a ticket younger than 120 seconds. |
check_record_ticket | view | record_id: U64, account_id: AccountId, nonce_hash: 64 hex | Same 120 second check for a record open by the patient or the creating clinic. |
09 / Limits
10 / Whitepaper
The card below reads WHITEPAPER_URL and WHITEPAPER_PDF_URL in web/content/docs.ts. Both are null, so this page does not link a paper and does not offer a PDF.
Coming
There is no page link and no PDF download while both config values are null.
11 / Companion
The same submitter also entered TrueNav in NEAR HACKATHON. The two projects are independent, with separate code, contracts and deployments, and neither depends on the other.
12 / FAQ
revoke_share is a patient call. It attaches exactly 1 yoctoNEAR, which a function-call key cannot do. The share status becomes Revoked. The next request_open panics ERR_REVOKED.
The 2026-11-14 attack demo recorded that refusal in transaction CGaCe4yTiSc5uc9Rtwuc82BFy72bZ5sbdwq8r4QjGf8L. Revoke does not erase a copy that was already downloaded. revoke_share does not revoke child shares. revoke_all_for_record bumps the record grant epoch so older shares, including children and replies, are effectively revoked. That call does not walk them.
request_open succeeds only for the share recipient or the patient, and only while the share is active, in the current grant epoch, and not past its expiry. The clinic that created the record can call request_owner_open. Anyone can read the public log of account ids, hashes, and timestamps.
Chart text is not stored on the contract. In the guided demo, Riverbend Family Clinic is where the fictional chart starts, so that clinic already holds it. St. Aster Hospital opens the share after Maya approves. PulseFit and a second opinion see a share only if the patient sends one. Every chart in this demo is fictional.
POST /api/tee/summary receives the chart text and sends it to NEAR AI Cloud, model z-ai/glm-5.3-flash. The handler does not write that text to Redis, the blob store, or the log. The log line is the status and the elapsed time. The server keeps the summary only when verifyResponse reports signatureKind provider_tee.
NEAR AI Cloud does not re-encrypt for the hospital and does not hold the data key. The patient's side wraps a new data key to the hospital public key stored on the contract. In this demo the app server sees the plaintext on the way into the summary call. The browser cannot perform the gateway TLS binding check. The contract stores the receipt hash as 64 hex characters and does not check an attestation.
The contract is a neutral record that no clinic can edit. Two hospitals do not have to trust each other's database. Events follow NEP-297, standard referral_relay, version 2.0.0, so a judge can read them on the testnet explorer without trusting this website.
Patient approval, decline, and revoke attach exactly 1 yoctoNEAR. A function-call key cannot attach that deposit, so a clinic staff key cannot consent for the patient. Shade Agents, Chain Signatures, and NEAR Intents are not used. This build is testnet only.
The contract stores actor account ids, roles, organization display names, and encryption public keys. It stores section hashes, share scope, purpose hashes, expiry, status, grant epoch, open counters, and ticket nonce hashes. Custody events are the NEP-297 log.
It does not store chart text. Schema 2 has no get_timeline. A view of contract state cannot reconstruct every open by itself. History is the receipt log plus the counters on the record and the share. U64 arguments are JSON strings. Page views cap limit at 50.
No. It is a hackathon prototype on NEAR testnet. It is not a live clinic system and not mainnet. It is designed around HIPAA principles: patient authorization, minimum necessary, and access logs.
Demo signing keys and the encryption seed live on the server for the guided demo. Revoking cannot erase a downloaded copy. Removing identifiers is best effort, with a preview. Rate limits are best effort across instances. There is no contract method that upgrades the code. A full-access key on the contract account could still deploy new code. This repository does not delete that key.
The schema 2 contract account rr191f8afd-contract.testnet is on testnet. Its owner is rr191f8afd-owner.testnet. The transactions named in docs/ATTACK-DEMO-RESULT.md are real testnet calls. The payloads in that run were fictional.
People, clinics, and chart text in the app are fictional. Do not load a real patient record. web/lib/fixtures.ts names the schema 2 account. The root README still names an older account and event version 1.0.0. The running server reads RELAY_CONTRACT_ID when that variable is set.
Start at Guided demo. The path is: set up Maya, Riverbend Family Clinic sends the referral, Maya approves, St. Aster Hospital opens, Maya revokes, the hospital open is refused, then the proof log.
Open a transaction hash from that log, or from docs/ATTACK-DEMO-RESULT.md, on https://testnet.nearblocks.io/txns/. The app uses that explorer. It does not link explorer.testnet.near.org, because that host opens an explorer picker. You can also view rr191f8afd-contract.testnet and call get_share or get_record. The proof page copies the last attack-demo table. It does not run the attacks again.
Consent calls attach exactly 1 yoctoNEAR. The contract keeps that deposit. It is the proof of a full-access key, not a fee, and it is not refunded. Function-call keys in deploy-kit/provision.sh are added with an allowance of 1 NEAR. Testnet NEAR is not a payment.
NEAR AI calls are refused when the next estimate would pass $4. The spend code also names a $5 cap. Public list prices on 2026-11-09 put z-ai/glm-5.3-flash at input 80 and output 500, and openai/privacy-filter at input 10 and output 0, in the public price units (scale 9). This page does not quote a gas price for one referral. Gas varies, and this is testnet.
The hospital function-call key in the deploy kit may call only request_open and submit_reply on this contract, with a 1 NEAR allowance. It cannot attach NEAR, so it cannot approve, decline, or revoke, and it cannot transfer the account balance. request_open still checks that the caller is the share recipient or the patient.
rotate_enc_key changes that account's encryption public key for future shares only. Shares already wrapped to the old key are not re-wrapped. A lost device key means the clinic re-issues the record. The demo patient key is a full-access key held by the server, which is a wider key than the staff keys. Private keys are not in this repository.
The contract cannot. It stores hashes. In this demo the server does see plaintext in two places: the TEE summary request, and the open route, which can decrypt a demo envelope with DEMO_ENC_SEED and return that text in the response. It does not write that text to Redis or the blob store. A local device key in IndexedDB is a different path. The guided demo does not keep the clinic key only on the device.
PulseFit is the App actor. It can open a DeidApp share the patient created. That copy is de-identified on a best-effort basis, and the patient sees a preview first. The app function-call key may call request_open only.
The patient calls approve_share with exactly 1 yoctoNEAR. The arguments are the granted sections, an expiry, a read-only flag, an artifact hash, and a tee receipt hash. The share becomes Active. The contract emits share_consented and share_transformed.
decline_share records a refusal the same way, with 1 yoctoNEAR, and emits share_declined. The guided demo asks the patient to read the summary before approving. The server will not treat a forged 64-hex receipt as the summary it issued: that path returns HTTP 403. The contract itself still accepts any 64-hex receipt when a transaction is signed outside this app.
decline_share is only valid while the share is Requested. The hospital cannot open a declined share. Expiry is lazy. When the block timestamp is past expires_at_ns, request_open fails with ERR_EXPIRED. The maximum expiry is 365 days from the call.
An open ticket is separate from the share expiry. check_ticket stays true for 120 seconds after request_open, and only while the share is still effectively active. The server also remembers a used nonce so the same nonce cannot open twice inside that window. That memory is per instance.
Chart text, the summary text, and ciphertext blobs stay off the contract. The blob helper keeps bytes in process memory and in a file under BLOB_DIR, or .data/blobs. It does not set an expiry. A body over one million bytes is rejected. A later open on another hosted function does not see that file. The browser keeps the ciphertext and sends it again.
Upstash, when configured, stores a spend counter and support tickets. Support tickets expire 30 days after the last write. They are not chart text. The visitor cookie holds ids and receipt hashes, not the chart. A summary receipt in that cookie expires after 10 minutes.