Catenor Protocol
ETHOnline 2026 · Catenor One · the first reference implementation of Catenor Protocol

Identity persists.
Authority stays scoped.

Holding an asset does not make you eligible to be paid today. Possession current eligibility.
Catenordecides identity and authority.
Chainlink CREevaluates private, current state inside a TEE.
Privyconstrains what each key can sign, through Privy policies.
Hedera ATSexecutes the asset lifecycle.
24/24stages verified ok: true
5executions on the deployed CRE Confidential Workflow
6Hedera testnet transactions, all SUCCESS
The thesis
Entitled to 4.
Paid 0.
Investor B was sanctioned after he invested. He still holds 400 units, a dividend entitlement of 4 and a validly signed, ACTIVE credential. The deployed Chainlink Confidential Workflow read today’s evidence inside its TEE and decided he is not eligible today: HOLD 4 — no transaction, no signature request. Investor A was paid 6 HBAR on Hedera testnet.
Wallets are accounts.Credentials are claims.Authority is scoped.Identity persists.
The stack

Four technologies.
Each with one job.

Catenor
Identity and scoped authority
What it is

The identity and authority layer: canonical identity (did:catenor), W3C credentials, scoped capabilities and delegation, policy decisions and a hash-chained audit.

Why we use it
  • Every actor gets its own identity; a wallet is only an account bound to it.
  • Every action needs explicit, scoped authority; delegation can never exceed the delegator.
  • Every decision lands in a hash-chained audit that anyone can re-verify (79 events, valid).
Chainlink CRE
Reads private evidence in a TEE, and decides
What it is

A deployed Confidential Workflow. Its secrets come from the Vault DON, and its Sumsub sandbox calls run inside the TEE.

Why we use it
  • It decides admission, credential issuance, offering eligibility and PAY/HOLD on private, current evidence.
  • Only facts and commitments leave the enclave; Catenor keeps no raw KYC response.
  • 5 executions on the deployed workflow, all SUCCESS.
Privy
Which key can sign what
What it is

Server wallets whose Privy policies constrain which key can sign what. Owner keys stay outside the runtime; the runtime signer cannot change a policy.

Why we use it
  • Trust Anchor & Sponsor signature wallets: message signing only, never money.
  • SPV wallet: the ATS Factory and its own equity only.
  • Distribution Agent wallet: only Lisa or Bart, ≤ 20 HBAR.
  • Investor wallets: receive-only.
Hedera ATS
Runs the asset, verifiable by anyone
What it is

Asset Tokenization Studio on testnet: the tokenized SPV equity lifecycle — deployEquity, issueByPartition, the corporate-action role, dividend entitlements — and the payout.

Why we use it
  • ATS computes entitlements by ownership: Lisa 6, Bart 4.
  • Execution is public: 6 testnet transactions, each verifiable on HashScan and the Mirror Node.
01
Trust Anchor
Root of trust for trust-domain:catenor-one-demo. Admitted on the deployed CRE workflow.
did:catenor:656d…3cab
02
Sponsor
An organization with five scoped capabilities on spv:catenor-demo-001.
did:catenor:5b87…761d
03
SPV 001
“Catenor One Demo SPV 001”, a synthetic real-estate SPV. Its units are equity interests, not a deed.
did:catenor:bd78…5d44
0x182F…9926 · 0.0.10509879
04
Investor A
“Lisa Simpson” · fictional, Sumsub sandbox
Eligible when she invested, and still eligible at distribution time.
600 units · 60%
0x7de5…2473 · 0.0.10509881
05
Investor B
“Bart Simpson” · fictional, Sumsub sandbox
Eligible when he invested. Sanctioned afterwards. Still owns his units.
400 units · 40%
0xCfCa…Da73 · 0.0.10509882
06
Distribution Agent
Its own identity and wallet. Pays only after a delegated EXECUTE_DISTRIBUTION.
did:catenor:9b25…c074
0x9089…13E3 · 0.0.10511413
The demo
One continuous run. Sixteen decisions.
Instance c1-202609121659, 2026-09-12, 16:59 → 22:59 UTC. The maintainer ran every stage by hand, one at a time, from an empty Privy app and an empty database to a paid distribution. Each chapter shows one protocol decision, who made it, and which system executed it.
Act ITrust is admitted, not assumed
01 / 16Stage 10 · 17:08 UTCSponsor liveWho actsCatenorPrivyChainlink CRE

Before anyone is trusted, the rules are pinned.

The Privy app starts with no wallets, no key quorums and no policies. Stage 10 creates the signer infrastructure for the Trust Domain: a Bootstrap Endorsement Key (a Privy signature wallet, message signing only, HKQY5…nVEk) under the Privy policy P_BOOTSTRAP, the Privy policy P_ASSERT for credential-assertion keys, two runtime quorums and two management-owner keys that never leave the maintainer’s machine. The acceptance rules are then hashed into the Bootstrap Configuration, which becomes the input to the first CRE deploy.

  • Trust Domaintrust-domain:catenor-one-demo
  • Admission policypolicy:trust-anchor-admission:v1
  • Privy policiesPrivy policyP_BOOTSTRAP vzww22mkxmtn4f333fdqpse7 · Privy policyP_ASSERT e8d0ndsssfq6rr7p2im0ptt4 — allow signMessage; deny exportPrivateKey and exportSeedPhrase
  • Bootstrap Configuration0x88314d8bae7f81ff33c8192124f7870a4f27586beba74e9772b735a3af007fff
Trust Domainadmission policyhash 0x8831…7fffCRE deploy #1
Stage 10. Signer infrastructure CREATED LIVE; Bootstrap Configuration hash 0x88314d8b…7fff.
02 / 16Stage 11 · 19:18 UTCCRE · deployedWho actsChainlink CRESumsub · SANDBOXCatenorPrivy

The root Trust Anchor is admitted inside a TEE.

The candidate organization gets a random did:catenor and an assertion key — a Privy signature wallet (message signing only) — and proves it holds the key. Then the deployed Confidential Workflow opens the sealed context, fetches its Vault DON secrets and makes two Sumsub sandbox calls from inside the enclave. Six facts come out, all true, plus a commitment. Catenor keeps no raw Sumsub response. The admission policy says ALLOW, the separate Privy signature wallet under the Privy policy P_BOOTSTRAP endorses, and the Trust Anchor becomes ACTIVE: TRUST_ANCHOR_VALID: true.

  • Trust Anchordid:catenor:656d66dff9ce1db692786ee241bc3cab
  • CRE execution0x394f66228ed74f270e5917ba9401a78f36aea40366864cf89306936e74ce4bd8 · TRUST_ANCHOR_ADMISSION · 8 s
  • Facts (in the TEE)ORGANIZATION_KYB_VERIFIED · ORGANIZATION_STATUS_VALID · ORGANIZATION_AML_CLEAR · AUTHORIZED_REPRESENTATIVE_VERIFIED · REPRESENTATIVE_AUTHORITY_CONFIRMED · EVIDENCE_FRESH
  • Evidence commitment0x032c3587582592487fde060e08ab3e3747e80b25cf7bc825ee6bdbc58cf31846 (COMMITMENT_ONLY)
  • Decisiondecision:4721cc3e-702a-42cc-95e1-60f34d66185a · ALLOW
Sumsub sandbox + synthetic company mockhandlerInTee6 facts + commitmentALLOWendorsementACTIVE
Before. identity-confidential-production: Active, private registry, 0 executions.
After. Execution 394f…4bd8: Success, 8 s, $0. trigger, then two http-actions SendRequest from inside the TEE.
Stage 11. mode DEPLOYED, EVIDENCE_RECEIVED, ALLOW, TRUST_ANCHOR_VALID: true.
Act IIScoped authority, then an asset
03 / 16Stages 20–21 · 19:58–20:01 UTCSponsor liveWho actsCatenorPrivy

The Trust Anchor authorizes the Sponsor.

The Sponsor is an organization with a random DID and no PII. At first it holds no capability. The Trust Anchor signs a Relationship Credential (AUTHORIZED_SPONSOR_IN the Trust Domain), which grants nothing by itself, and five Capability grants on spv:catenor-demo-001, all ALLOW. The same capability on another resource is DENY CAPABILITY_MISSING.

Trust AnchorSponsor TOKENIZE_ASSETDEFINE_OFFERING_POLICYCREATE_AGENTCREATE_DISTRIBUTIONDELEGATE_DISTRIBUTION_AUTHORITY
Stage 21. AUTHORIZED_SPONSOR_IN valid + five grants ALLOW; another resource → DENY CAPABILITY_MISSING.
04 / 16Stage 30 · 20:08 UTCSponsor liveWho actsCatenorPrivy

The SPV exists only after TOKENIZE_ASSET says ALLOW.

Catenor evaluated the Sponsor’s TOKENIZE_ASSET on spv:catenor-demo-001. Only then were the SPV subject, its Privy EVM execution wallet and its Privy policy created. The Privy policy is owned by a management-owner key outside the runtime, so the runtime signer cannot change it. It allows eth_signTransaction only on chain 296 and only to the ATS v8 Factory. Everything else is denied.

  • SPVdid:catenor:bd7856964b7ab1cce73a0e9c4c875d44
  • Execution wallet0x182F8c7DDbDa1b5f295893E2c4f6D55abBdB9926
  • Privy policyPrivy policycatenor-one-spv-execution-4c875d44 · cygll4bmgocqr05asp2w6f2s
  • Also createda private SPV_EXECUTION Account Binding, and SPV SPONSORED_BY Sponsor
Catenor capabilitydecides whetherthe SPV may exist·Privy policybounds whatits wallet may sign
Stage 30. SPV did:catenor:bd78…5d44 + wallet 0x182F…9926, created after the ALLOW.
Privy policy of the SPV wallet. eth_signTransaction ALLOW only if chain_id eq 296 AND to eq the ATS v8 Factory.
05 / 16Stage 31 · 20:22 UTCSponsor liveWho actsCatenor

The offering pins who may invest — by hash.

The Sponsor signs offering:ab27ec9a-cd89-4b5b-92ea-bef19fbd9f9f: 1,000 equity-interest units in “Catenor One Demo SPV 001”. Eligibility is policy:offering-eligibility:v1, pinned by hash. It requires four facts, and accepts one credential type. The signature is checked against the Sponsor’s DEFINE_OFFERING_POLICY grant: ALLOW.

  • Policy hash0xa3326b33a0932b68c140a477334160a1585a75d4ccc824e0c89aa68d57dd51a2
  • RequiresINVESTOR_PRESENTATION_VALID · INVESTOR_IDENTITY_VERIFIED · INVESTOR_AML_CLEAR · EVIDENCE_FRESH
  • Accepted credentialCatenorInvestorEligibilityCredential
1,000tokenized equity-interest units on offer
Stage 31. Offering ab27ec9a…, policy pinned by hash 0xa3326b33…51a2, verified ALLOW.
Act IIIInvestors, credentials, confidential eligibility
06 / 16Stages 40–41 · 20:26–20:29 UTCSumsub sandboxWho actsCatenorPrivySumsub · SANDBOX

Two investors onboard. Both green.

“Lisa Simpson” and “Bart Simpson” are fictional Sumsub sandbox applicants, both forced GREEN (Approved). Each gets a random did:catenor with no PII, a holder key — a Privy signature wallet (message signing only) that signs presentations and never money — and a receive-only Privy wallet: an owner key, no signer, no Privy policy. The link from DID to wallet is a private Account Binding, never published in the DID Document. There is no CRE call here, by design: the evidence is read only inside the TEE.

  • Investor A (Lisa)0x7de572532820B22040B561e68543419419b72473
  • Investor B (Bart)0xCfCa833A8e6b7651E44480b92822576BcB78Da73
Sumsub sandbox. Bart and Lisa both Approved (level id-only). Applicant IDs redacted.
07 / 16Stages 42–43 · 20:34–20:41 UTCCRE · deployedWho actsChainlink CRESumsub · SANDBOXCatenor

Credentials are issued only after the TEE has looked.

Two INVESTOR_ELIGIBILITY runs on the deployed workflow read each investor’s current Sumsub sandbox review behind the binding gate. Only after identity, AML and freshness came back true did the Trust Anchor sign a W3C VC 2.0 CatenorInvestorEligibilityCredential (eddsa-jcs-2022), valid from 2026-09-12 to 2026-12-11, with no PII. In stage 43 each investor signed a Verifiable Presentation with the holder key; both pass 7/7 checks, and a replay with another challenge fails HOLDER_PROOF_VALID.

  • Investor A run0xe66a410799c33f710fa19baefa09ea49f0d2f89aaced4266390d14cdee320b8f
  • Investor B run0x61485b72ad43bd2a63d466fdb7778d02d49a5dec4206979171aa68a15df064f8
  • Pacingthe runner waited 44 s between triggers: the deployed rate limit is 1 per 60 s
current Sumsub reviewTEE facts + commitmentVC by the Trust AnchorVP by the holder
CRE. 3 successful, 0 unsuccessful. New: e66a41…0b8f and 61485b…64f8, about 63 s apart.
08 / 16Stage 44 · 20:45 UTCCRE · deployedWho actsChainlink CRESumsub · SANDBOX

Offering eligibility, decided in the enclave.

One sealed OFFERING_ELIGIBILITY run covers both investors. Inside handlerInTee the workflow checked the holder proof, the VC signature, the pinned issuer key, the subject, the validity window and the status, then the current Sumsub evidence and its reconciliation with the credential, then policy:offering-eligibility:v1. Only minimized conclusions left the TEE.

InvestorUnitsDecisionDecision refChecks
A · Lisa600ALLOWdecision:8c0a84a1-d53f-4c2b-94f8-f88787ab53dd7/7 · CONSISTENT
B · Bart400ALLOWdecision:3000f2f5-0376-446f-b9b9-bbffdd8078ef7/7 · CONSISTENT
  • CRE execution0xfecb527d3d0133d4fd23a4c29a36b7998a1dc379c137a65175d72f4272464715 · 8 s
  • Run commitment0x83db064bab91f7dfd86dfd0309cbfe6f5d68f568116554fa2b21715b92599beb
Stage 44. A ALLOW 600 · B ALLOW 400, four requirements TRUE, 7/7 checks. Investor DIDs redacted.
Logs tab. Every DON node emits only safe markers: secrets_fetched, offering_evaluated status=OK, handler_completed status=DELIVERED.
Act IVThe asset goes live on Hedera
09 / 16Stages 50–62 · 20:55–21:57 UTCTestnet liveWho actsHedera ATSPrivyCatenor

The SPV equity is deployed and issued — exactly 600 and 400.

A Privy testnet treasury, funded once from the faucet, activated the wallets. Then the Sponsor’s TOKENIZE_ASSET grant was re-evaluated and the SPV’s Privy wallet sent deployEquity to the ATS v8 Factory. Each issuance was bound to that investor’s TEE-backed ALLOW decision from stage 44 — unused, within the offering total, and consumed on use. No generic mint.

CallTransactionResult
deployEquity → Factory0x2396…c570SUCCESS · 6,898,334 gas · fee 7.864 ℏ
issueByPartition → A, 6000x5935…fa4eSUCCESS · balance 600
issueByPartition → B, 4000x9c6c…0d62SUCCESS · balance 400
  • ATS equity0xf37A91c3aC757ac5f14e4b8BC92D4b1001D97ABC · contract 0.0.10510175 · C1SPV001
  • Read-backtotalSupply() 1000 · decimals() 0 · A 600 · B 400
Investor A · 600 · 60%Investor B · 400 · 40%
HashScan. deployEquity: SUCCESS, 21:14:07 UTC, block 40438071; fee 7.864 ℏ from SPV 0.0.10509879.
10 / 16Stage 63 · 22:05 UTCTestnet liveWho actsHedera ATSPrivy

A dividend is set. ATS computes who is entitled.

The SPV wallet sends grantRole(ROLE_CORPORATE_ACTION, SPV) and then setDividend (amount 1, decimals 2), each allowed only by its equity-pinned Privy policy rule. ATS computes entitlements by ownership only. No funds move. Who is actually paid is decided later, by Catenor, in the TEE.

  • grantRole0x726032ce…9f94 · 179,949 gas
  • setDividend0xaac4a085…f865 · 532,107 gas · record date 22:07:10 UTC · execution 22:10:10 UTC
10 HBARA = 6+B = 4(ownership only)
Stage 63. grantRole + setDividend confirmed; entitlements A 600 → 6, B 400 → 4.
Read-only re-check, 22:08 UTC. 5 transactions SUCCESS; dividend 1: A 6, B 4.
Act VAn Agent earns its authority
11 / 16Stage 70 · 22:10 UTCSponsor liveWho actsCatenorPrivy

The Agent has a wallet. It has no authority: DENY.

The Sponsor’s CREATE_AGENT is ALLOW. The Distribution Agent gets its own identity, its own Privy EVM wallet bound privately (AGENT_EXECUTION), its own signer set, and a Privy policy narrower than the SPV’s: chain 296, only to the two investor receiving wallets, ≤ 20 HBAR. Then it asks to run a distribution before any delegation: DENY CAPABILITY_MISSING.

  • Agentdid:catenor:9b25f52aff6dc4cac4d0a7aa3d8dc074
  • Wallet0x90894535F5f35271f2d4112179BF2b4e887913E3
  • Privy policyPrivy policycatenor-one-distribution-agent-3d8dc074 · p99xe36icsfvewrovm1mczwq
Stage 70. Agent + wallet created; request before delegation → DENY CAPABILITY_MISSING.
12 / 16Stage 71 · 22:19 UTCSponsor liveWho actsCatenor

A relationship alone: still DENY.

The Sponsor signs a Relationship Credential, urn:uuid:87eb74d1-4314-49bc-bb4c-8ad6d3cb6794: Agent AGENT_OF Sponsor. It is valid. The Agent asks again with only the relationship: DENY CAPABILITY_MISSING.

AgentAGENT_OFSponsor=no capability
Stage 71. AGENT_OF valid; request with only the relationship → still DENY.
13 / 16Stage 72 · 22:20 UTCSponsor liveWho actsCatenor

Delegated EXECUTE_DISTRIBUTION: ALLOW.

Before signing, Catenor checked three things: the action is explicitly delegable, and the Sponsor holds both CREATE_DISTRIBUTION and DELEGATE_DISTRIBUTION_AUTHORITY on the same resource from the ACTIVE Trust Anchor. The Sponsor then signed grant capability-grant:7bd93930-e21c-4f78-86fd-8f8c9aa46156 for 20 days, inside its own 30. The chain Trust Anchor → Sponsor → Agent evaluates ALLOW. The Sponsor then tried to delegate TOKENIZE_ASSET: refused ACTION_NOT_DELEGABLE, no signature requested. Investor A presenting the Agent’s grant: DENY SUBJECT_MISMATCH.

Trust AnchorSponsor (until 2026-10-12)Agent (until 2026-10-02)EXECUTE_DISTRIBUTION
Stage 72. Chain ALLOW; TOKENIZE_ASSETrefused ACTION_NOT_DELEGABLE; wrong holder → DENY SUBJECT_MISMATCH.
Act VIThe world changes. The payout follows the present.
14 / 16Stage 80 · 22:29 UTCSumsub sandboxWho actsSumsub · SANDBOXHedera ATS

Bart is sanctioned. Nothing else changes.

Catenor did nothing here: the current external evidence changed. The operator set Investor B’s Sumsub sandbox review to RED, reject label SANCTIONS, FINAL. He still holds 400 units on Hedera ATS and his entitlement of 4. His credential urn:uuid:9c25804e-…4530 is still ACTIVE and validly signed.

400units still owned by Investor B
ACTIVEhis credential, still validly signed
Sumsub sandbox. Bart Rejected (Sanctions), Lisa Approved. Applicant IDs redacted.
Stage 80. Review RED; credential still ACTIVE; holdings still 400. Investor DID redacted.
15 / 16Stages 81–82 · 22:37–22:40 UTCCRE · deployedWho actsChainlink CRESumsub · SANDBOXCatenor

The TEE re-checks today. PAY 6. HOLD 4.

A 10 HBAR revenue event is recorded for the SPV (revenue-event:a2400be4…2355, an audit event; no funds move). Investor A asking to run the distribution is DENY CAPABILITY_MISSING: only the Agent holds the delegated capability. Then one sealed CONFIDENTIAL_DISTRIBUTION run on the deployed workflow, with three Sumsub calls from inside the TEE, verifies each VP, VC and status statement, reads the current evidence, reconciles it, applies policy:distribution-eligibility:v2 and computes 10 HBAR × units / 1000.

PAY 6Investor A · all TRUE · CONSISTENT
HOLD 4Investor B · VP 7/7 valid, but identity FALSE, AML FALSE · MISMATCH · FINAL, SANCTIONS
  • CRE execution0xfd2a96fb91bb4be494fd1f65a3e01ebd0e4da49d07520854cb38e2eb76e82d0b · 8 s
  • Plandistribution-plan:034ffe82-92e3-4e16-a6ff-6ed21562e4a5 · executed: false
Stage 82. A ALLOW → PAY 6; B DENY → HOLD 4; authority chain Trust Anchor → Sponsor → Agent; executed: false.
CRE. Execution fd2a…2d0b: Success, 8 s, $0. trigger, then three SendRequest.
16 / 16Stage 83 · 22:45 UTCTestnet liveWho actsCatenorPrivyHedera ATS

One signature. One payout. Zero for Bart.

The approved plan goes through the Catenor signer boundary, then the Agent’s Privy policy, then a Hedera transfer. The transfer goes to Lisa’s privately bound account, with empty calldata, on chain 296. For Bart, no transaction is built and no Privy signature is ever requested.

+6 HBARInvestor A · tx 0x4781…ff24 · block 40440606
HOLD 4Investor B · transactionConstructed: false · investorBSignatureRequests: 0
  • TransferAgent 0.0.10511413 → Lisa 0.0.10509881: 6.00000000 ℏ (fee 0.024 ℏ)
  • Agent nonce0 → 1: exactly one transaction was sent
Stage 83. A PAY 6.0, received; B HOLD 4.0: privySignatureRequested: false; Agent nonce 0 → 1.
HashScan. SUCCESS, block 40440606: 0.0.105114130.0.10509881, 6.00000000 ℏ.
Chainlink CRE Confidential · deployed
Private in.
Minimum out.
The distribution decision runs inside the deployed Confidential Workflow identity-confidential-production, in handlerInTee. Sensitive evidence stays in the enclave. What leaves is the executable conclusion and the commitments that let Catenor check it.
Confidential input
VPs · VCs · issuer-signed status · current Sumsub sandbox evidence · holdings
The sealed context opens only inside the TEE. The Sumsub credentials come from the Vault DON (SUMSUB_APP_TOKEN, SUMSUB_SECRET_KEY; names only). Holdings were read from Hedera ATS 0xf37A…7ABC with balanceOf.
Computation
Verify → read current evidence → reconcile → policy:distribution-eligibility:v2 → 10 HBAR × units / 1000
The workflow owns the whole distribution decision — pro-rata shares and PAY/HOLD — not an isolated eligibility check. Three http-actions SendRequest calls, 8 seconds, 0 credits.
Minimized output
Decisions · requirement statuses · check results · reconciliation class · coarse reason codes · commitments
A signed callback reaches the Railway relay. Catenor verifies the HMAC, the commitment, the arithmetic and the holder set, then records one protocol Decision per holder.
Never leaves
No Sumsub response, name, document or applicant data.
Honest caveat: the coarse reason codes FINAL and SANCTIONS do reach Catenor, and this page does not claim otherwise.
Investor A · Lisa · 600 units
PAY 6
ALLOW · every requirement TRUE · 7/7 presentation checks · reconciliation CONSISTENT · no reason codes
commitment 0x37df46f9…76db98
Then on Hedera: 6.00000000 ℏ from the Agent, tx 0x4781…ff24, SUCCESS.
Investor B · Bart · 400 units
HOLD 4
DENY · holds the asset, presentation valid 7/7 — but INVESTOR_IDENTITY_VERIFIED=FALSE, INVESTOR_AML_CLEAR=FALSE · MISMATCH · FINAL, SANCTIONS
commitment 0x17b39d3e…4e8cf
Then on Hedera: nothing. No transaction built. Zero signature requests.
Execution 0xfd2a…2d0b CRE UUID a623ebaa-0fb5-48eb-ad70-c65f95f079ee Window 22:40:08–22:40:16 UTC Evidence commitment 0x824b…6405 Plan pay 6.0 · hold 4.0
Judge evidence
Verify every system independently.
Public references, not just screenshots: HashScan links and a live Mirror Node check for every Hedera transaction, CRE execution IDs from both the gateway and the platform, Privy wallet and policy IDs, and the Catenor audit re-verification. Everything here is public-safe: no investor DID, no applicant ID, no VC or VP, no key or secret.

24 of 24 stages. Every one ok: true.

Stage 99 (22:59 UTC, read-only) summarized every stage record and re-ran the Catenor verification. From 01-setup-env at 16:59 UTC to 83-execute-approved-distribution at 22:45 UTC.

24/24stages ran and are ok
5DEPLOYED CRE executions, all SUCCESS
6Hedera testnet transactions, all SUCCESS
79audit events, hash chain valid
3DISTRIBUTION_REQUEST_DENIED events
0signature requests for Investor B

Stage record

Modes: Local 2 · Sponsor live (non-spending) 12 · Confidential (CRE) · deployed 4 · Testnet live (spends HBAR) 6. Stages 11 and 60 show their successful re-run; stage 50 ran twice and shows its second run (the Agent funding).

#StageModeRanOKAt (UTC)
001-setup-envLocaltruetrue16:59:49
110-create-trust-domainSponsor livetruetrue17:08:13
211-admit-root-trust-anchorCRE · deployedtruetrue19:18:13
320-create-sponsorSponsor livetruetrue19:58:44
421-trust-anchor-authorize-sponsorSponsor livetruetrue20:00:46
530-create-spvSponsor livetruetrue20:08:04
631-create-offering-policySponsor livetruetrue20:21:58
740-create-investor-aSponsor livetruetrue20:26:02
841-create-investor-bSponsor livetruetrue20:29:06
942-create-investor-credentialsCRE · deployedtruetrue20:34:29
1043-create-investor-presentationsSponsor livetruetrue20:41:06
1144-check-offering-eligibilityCRE · deployedtruetrue20:44:56
1250-fund-testnet-walletsTestnet livetruetrue22:25:45
1360-tokenize-spvTestnet livetruetrue21:25:41
1461-investor-a-investTestnet livetruetrue21:54:50
1562-investor-b-investTestnet livetruetrue21:56:24
1663-create-dividendTestnet livetruetrue22:05:10
1770-create-distribution-agentSponsor livetruetrue22:10:52
1871-sponsor-establish-agent-relationshipSponsor livetruetrue22:18:58
1972-sponsor-delegate-distribution-capabilitySponsor livetruetrue22:19:50
2080-invalidate-investor-bSponsor livetruetrue22:29:47
2181-trigger-revenueLocaltruetrue22:37:40
2282-run-confidential-distributionCRE · deployedtruetrue22:39:44
2383-execute-approved-distributionTestnet livetruetrue22:45:05

Key public references

Chainlink CRE · DEPLOYED
Confidential distribution · A PAY 6 / B HOLD 4
0xfd2a96fb…2d0b
Chainlink CRE · DEPLOYED
Root Trust Anchor admission · ALLOW
0x394f6622…4bd8
Hedera ATS · TESTNET
Equity deployment · deployEquity
Hedera ATS · TESTNET
Issuance · Investor A 600 / Investor B 400
Hedera ATS · TESTNET
Approved payout · Investor A · 6 HBAR
Privy · REAL dev app
SPV execution wallet · Privy policy cygll4bmgocqr05asp2w6f2s
0x182F8c7D…9926
Privy · REAL dev app
Distribution Agent wallet · Privy policy p99xe36icsfvewrovm1mczwq
0x90894535…13E3
Catenor
Audit hash chain valid · 79 events · Trust Anchor VALID
scripts/demo/93-verify-catenor-audit.sh
Stage 99. All 24 stages ran and are ok: true, each with its mode.
End of demo. identity-confidential-production: 5 executions, all Success, $0.
Stage 91. 6 transactions SUCCESS; A 600 units / 7.0 ℏ, B 400 units / 1.0 ℏ.

The deployed Confidential Workflow.

Label: DEPLOYED. This is the deployed workflow on Chainlink’s private registry, not cre workflow simulate. Five executions, all SUCCESS, about 8 s each, 0 credits used.

Workflow facts

Name
identity-confidential-production (CRE project workflows/, target production-settings)
Handler
cre.handlerInTee(httpTrigger, onHttpTrigger, [{ tee: 'nitro', regions: ['us-west-2'] }]), routing five operations: admission, investor eligibility, credential, offering and distribution
Registry / DON family
private (Chainlink-hosted) / zone-a
Owner
0x7075057f1589BAf347cB6dD6a993B1FC536B8a0a
Deploy #1 · 17:38 UTC
ID 00e12517fc06c8984befaa63accbadb21b8c4abe529595fcf48542ba097ad250
binary 9902db587a3878b07ad03d599cb9c781f7c77b8ea440b168d8c42fd18b3fb9fb
config 7cb69b7b82f6d8a0701ba39b15cc915d6ea3b4ca746c84fd3a805188c38098f8 — pins the Bootstrap Configuration hash; no issuer rules yet
Deploy #2 · 19:36 UTC
ID 0000e58d50da8eaf29fb4212c236f374d11d7eb2452502c3bad0afe74d3f25c8
same binary 9902db58…, new config e7703b193460430b4767b89d44caea2620eda5b3c844deee69f9db9436207d1d adding credentialRules. The CLI updated the workflow in place
Pinned in deploy #2
credential type CatenorInvestorEligibilityCredential; the only accepted issuer did:catenor:656d66dff9ce1db692786ee241bc3cab#assertion-key-1, public key z6MknrBa8Vbrsz5YCoBhgmV54ALJP9p1YSocpeBvgzBgJVXq; status at most 600 s old; policy:offering-eligibility:v1 and policy:distribution-eligibility:v2
Vault DON secrets
CATENOR_INTERNAL_API_TOKEN · SUMSUB_APP_TOKEN · SUMSUB_SECRET_KEY (namespace main; names only, no value was printed)
Trigger
HTTP trigger with one authorized EVM key 0x18487BeFE194528cf429f5889BAE3aCbdA104a6c, called as JSON-RPC workflows.execute with an alg: ETH JWT
Gateway
https://01.gateway.zone-a.cre.chain.link
Result path
callback to the Railway relay https://catenor-one-production.up.railway.app/v1/internal/cre/identity-confidential/results; the local runner pulls each HMAC-authenticated result, re-authenticates it and delivers it
Uploaded configs
public data only: the callback URL, the hashes, the evidence-acceptance rules, the issuer key and the trigger address

The five executions

Execution IDs come from two places: the gateway’s workflow_execution_id and the platform’s cre execution list. From stage 42 on they ran on deploy #2.

StageOperationGateway execution IDCRE UUIDWindow (UTC)Result
11TRUST_ANCHOR_ADMISSION0x394f66228ed74f270e5917ba9401a78f36aea40366864cf89306936e74ce4bd8579f70e6-2fe3-48fc-8314-5facefbe9eb119:18:37–19:18:45ALLOW 6 facts · commitment 0x032c…1846
42INVESTOR_ELIGIBILITY (A)0xe66a410799c33f710fa19baefa09ea49f0d2f89aaced4266390d14cdee320b8f3529711c-121f-4aa6-b708-d531c53f507720:34:36–20:34:45identity ✓ · AML ✓ · fresh ✓
42INVESTOR_ELIGIBILITY (B)0x61485b72ad43bd2a63d466fdb7778d02d49a5dec4206979171aa68a15df064f85a85768b-1a2e-426c-8089-c8bc11e2922a20:35:39–20:35:47identity ✓ · AML ✓ · fresh ✓
44OFFERING_ELIGIBILITY0xfecb527d3d0133d4fd23a4c29a36b7998a1dc379c137a65175d72f4272464715b6c4ff63-cd01-4d32-958d-c5aabb3bfc1120:45:11–20:45:19A ALLOW 600 · B ALLOW 400
82CONFIDENTIAL_DISTRIBUTION0xfd2a96fb91bb4be494fd1f65a3e01ebd0e4da49d07520854cb38e2eb76e82d0ba623ebaa-0fb5-48eb-ad70-c65f95f079ee22:40:08–22:40:16A PAY 6 · B HOLD 4 (FINAL, SANCTIONS)

What leaves the TEE

  • Decisions and requirement statuses.
  • Check results: the seven presentation checks.
  • Reconciliation class: CONSISTENT or MISMATCH.
  • Coarse reason codes: FINAL, SANCTIONS.
  • Commitments over the evidence and each decision.
  • Never: a Sumsub response, a name, a document or applicant data.

What this evidence does not claim

  • No independent attestation check. Catenor did not verify an enclave attestation itself; TEE placement is the CRE platform’s handlerInTee guarantee.
  • reportFromDon is not implemented. Offchain report verification is a P1 item.
  • Sandbox and mock evidence. The company KYB is a SYNTHETIC MOCK; the investors are synthetic Sumsub sandbox applicants. Nothing ran on mainnet.
  • Logs are not claimed to stay in the enclave. They are visible per node; only safe markers are logged.

Findings for Chainlink, reported as observed

  1. The gateway URL. The docs’ private-registry gateway https://01.enterprise-gateway.zone-a.cre.chain.link/ answered -32600 Workflow not found for two ACTIVE private-registry workflows — the confidential one and a minimal non-TEE control workflow, catenor-http-control (0038de78…a66f). https://01.gateway.zone-a.cre.chain.link, the only gateway URL embedded in CRE CLI v1.33.0, accepted the same signed request at once (HTTP 200, ACCEPTED).
  2. User logs are visible. The simulator banner says user logs “will not be visible, and will not leave the TEE”. On the deployed platform the Logs tab shows them from several DON nodes (Node 1, 2, 4, 5, 8, 9…). The workflow logs only non-sensitive markers through safe-log.
  3. Several callbacks per execution. Each node’s copy calls back. The relay keeps the first authenticated result per run and answers 409 LATE_OR_DUPLICATE_RESULT to the rest.
  4. Secrets. cre secrets create parses CRE_ETH_PRIVATE_KEY from the .env it is given, even with --secrets-auth browser; the template’s non-hex placeholder fails with invalid hex character 'Y'.
  5. Enrollment. The first Confidential Workflow deploy on this account needed no extra enrollment step.

Screenshots

C3. 3 secrets created in the Vault DON (namespace=main). Only names are shown.
C4 · deploy #1. Registered in the private registry (zone-a): 00e12517…d250, Active.
E2 · deploy #2. Same binary 9902db58…, new config e7703b19…; new ID 0000e58d…25c8.
Before stage 11. Active, private, owner Catenor One, 0 executions.
Stage 11. 394f…4bd8: trigger + two SendRequest to the Sumsub sandbox.
Stage 44 · Logs. Only non-sensitive markers, from every node.
Stage 82. fd2a…2d0b: trigger + three SendRequest.
End of demo. 5 executions, all Success, $0 spend.

Catenor decides whether. Privy bounds what.

Development app catenor-one-ethonline-2026, created empty for this run. Every execution wallet has its own management-owner key, kept on the maintainer’s machine and never in the runtime, and its own runtime signer: a 1-of-1 key quorum, override-scoped to the wallet’s Privy policy. The runtime can request signatures; it cannot change a Privy policy.

Wallets and Privy policies

Read back live in stage 92 and checked against the dashboard. Everything not listed is denied; exports are denied everywhere.

WalletKindPrivy policyWhat it allows
Bootstrap Endorsement Key
HKQY5…nVEk
Privy signature wallet (message signing only)Privy policyP_BOOTSTRAP
vzww22mkxmtn4f333fdqpse7
signMessage only. Endorses the root Trust Anchor after an ALLOW; its own owner and runtime quorum
Assertion keys
Trust Anchor 9PvXY…PGkT, Sponsor GLHPd…oTdX, investor holder keys, 3 abandoned stage 11 attempts
Privy signature wallets (message signing only)Privy policyP_ASSERT
e8d0ndsssfq6rr7p2im0ptt4
(7 wallets)
signMessage only: credentials, grants, presentations. Never a financial key
SPV
0x182F…9926
EVMPrivy policycatenor-one-spv-execution-4c875d44
cygll4bmgocqr05asp2w6f2s
eth_signTransaction on chain 296 to the ATS v8 Factory; after deployEquity, three owner-added rules pinned to equity 0xf37A…7ABC
Distribution Agent
0x9089…13E3
EVMPrivy policycatenor-one-distribution-agent-3d8dc074
p99xe36icsfvewrovm1mczwq
eth_signTransaction on chain 296 only to Investor A’s or B’s receiving wallet, value ≤ 20 HBAR
Testnet treasury
0x49e9…5210
EVMPrivy policycatenor-one-clean-room-testnet-treasury
zqr5ynnrd3mi6o9snqq8ex8g
chain 296 transfers ≤ 30 HBAR. Bootstrap funding only
Investor A / Investor B
0x7de5…2473 · 0xCfCa…Da73
EVMnoneReceive-only: an owner key, no signer, no Privy policy. Catenor cannot sign for them

The Agent’s Privy policy, rule by rule

Stage 92 read-back: “Agent policy exactly the approved boundary: true.”

PrivyPrivy policycatenor-one-distribution-agent-3d8dc074p99xe36icsfvewrovm1mczwq
Allowallow-distribution-payout-hederamethod eth_signTransaction · rule fxxhjsvbzc7dl9xx9lhybkqz
chain_id eq
296Hedera testnet
to in
{ Investor A, Investor B }0x7de572532820B22040B561e68543419419b72473
0xCfCa833A8e6b7651E44480b92822576BcB78Da73
value lte
20 HBAR0x1158e460913d00000 (18-decimal weibar)
DENY exportPrivateKey · DENY exportSeedPhrase · everything else denied by default: no ATS contract, no SPV wallet, no other chain.

The SPV’s Privy policy after the equity existed

The factory rule came first. After deployEquity, the SPV management-owner key (outside the runtime) added three Privy policy rules, each pinned to the new equity. Six rules in all.

PrivyPrivy policycatenor-one-spv-execution-4c875d44cygll4bmgocqr05asp2w6f2s
RuleActionConditions
allow-ats-factory-hedera-testnetALLOWchain_id = 296 ∧ to = ATS v8 Factory 0xd1F118A40f3b02883D35909eF2517e7EDd78379d
allow-issueByPartition-equityALLOWchain 296 ∧ to = equity ∧ function issueByPartition ∧ partition = 0x…01 (default)
allow-grantRole-corporate-action-to-spvALLOWchain 296 ∧ to = equity ∧ function grantRole_role = 0xa1acfc49…55cd_account = SPV 0x182F…9926
allow-setDividend-equityALLOWchain 296 ∧ to = equity ∧ function setDividend
deny-exportPrivateKeyDENY
deny-exportSeedPhraseDENY

Controls in action

  • Created only after authorization. The SPV wallet and its Privy policy exist only because TOKENIZE_ASSET was ALLOW; the Agent’s, under CREATE_AGENT.
  • Still not authorized. Before delegation, and with the relationship alone, the Agent’s requests were DENY CAPABILITY_MISSING.
  • Every signature fit one Privy policy rule. Each Hedera transaction was dry-signed first (signature discarded), then signed live under exactly one rule: five by the SPV, the payout by the Agent.
  • The HOLD holder got nothing. No transaction built for Investor B, no Privy signature requested: investorBSignatureRequests: 0, Agent nonce 0 → 1.

Separate signer sets

  • Bootstrap: its own owner key and runtime quorum …-bootstrap, signer for one wallet only.
  • Assertion keys: one owner and the …-assertion quorum for the signMessage-only Privy signature wallets.
  • SPV: a new, separate management-owner key and runtime quorum …-spv-runtime.
  • Agent: a new, separate owner key and runtime quorum …-agent-runtime. The treasury has its own pair.
  • Investors: one owner key per receiving wallet, no signer, no Privy policy.

Stage 92 read-back, as stored by Privy

WalletOwnerAdditional signerPrivy policy (owner matches the wallet)Rules
SPV 0x182F…9926set1, override-scopedcatenor-one-spv-execution-4c875d44Factory (chain ∧ to); DENY both exports; issueByPartition (chain ∧ to ∧ function ∧ partition); grantRole (chain ∧ to ∧ function ∧ role ∧ account); setDividend (chain ∧ to ∧ function)
Agent 0x9089…13E3set1, override-scopedcatenor-one-distribution-agent-3d8dc074ALLOW eth_signTransaction [chain ∧ to ∈ ∧ value ≤]; DENY both exports
Treasury 0x49e9…5210set1, override-scopedcatenor-one-clean-room-testnet-treasuryALLOW [chain ∧ value ≤]; DENY both exports
Investor A 0x7de5…2473setnonenonereceive-only
Investor B 0xCfCa…Da73setnonenonereceive-only
Privy policy · agent-policy.jsonartifacts/privy/final-demo/clean-room-c1-202609121659/
{
  "id": "p99xe36icsfvewrovm1mczwq",
  "version": "1.0",
  "name": "catenor-one-distribution-agent-3d8dc074",
  "chain_type": "ethereum",
  "rules": [
    {
      "id": "fxxhjsvbzc7dl9xx9lhybkqz",
      "name": "allow-distribution-payout-hedera",
      "method": "eth_signTransaction",
      "action": "ALLOW",
      "conditions": [
        { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "296" },
        {
          "field_source": "ethereum_transaction",
          "field": "to",
          "operator": "in",
          "value": ["0x7de572532820B22040B561e68543419419b72473", "0xCfCa833A8e6b7651E44480b92822576BcB78Da73"]
        },
        { "field_source": "ethereum_transaction", "field": "value", "operator": "lte", "value": "0x1158e460913d00000" }
      ]
    },
    { "id": "su9uq4lrxg8hgsusvgm3nv3q", "name": "deny-exportPrivateKey", "method": "exportPrivateKey", "action": "DENY", "conditions": [] },
    { "id": "nu05o2k3j0kinl728wimu2c9", "name": "deny-exportSeedPhrase", "method": "exportSeedPhrase", "action": "DENY", "conditions": [] }
  ],
  "_note": "Transcribed from the Privy dashboard (policy JSON tab) on 2026-09-12 after stage 70, final-demo instance c1-202609121659. 0x1158e460913d00000 = 20 HBAR (18-decimal weibar). The two addresses are the investors' receiving wallets. Public data only."
}
Privy policy · spv-policy-after-equity.jsonartifacts/privy/final-demo/clean-room-c1-202609121659/
{
  "id": "cygll4bmgocqr05asp2w6f2s",
  "version": "1.0",
  "name": "catenor-one-spv-execution-4c875d44",
  "chain_type": "ethereum",
  "rules": [
    {
      "id": "t01al94fattl6okgua0jwpd0",
      "name": "allow-ats-factory-hedera-testnet",
      "method": "eth_signTransaction",
      "action": "ALLOW",
      "conditions": [
        { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "296" },
        { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xd1F118A40f3b02883D35909eF2517e7EDd78379d" }
      ]
    },
    { "id": "nfob7nm7jo55mjuj3bbxq6ii", "name": "deny-exportPrivateKey", "method": "exportPrivateKey", "action": "DENY", "conditions": [] },
    { "id": "ub7kylldhvex9z8iq08imioj", "name": "deny-exportSeedPhrase", "method": "exportSeedPhrase", "action": "DENY", "conditions": [] },
    {
      "id": "r6bo3tyjsl1glf3vwimo3m0g",
      "name": "allow-setDividend-equity",
      "method": "eth_signTransaction",
      "action": "ALLOW",
      "conditions": [
        { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "296" },
        { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xf37A91c3aC757ac5f14e4b8BC92D4b1001D97ABC" },
        { "field_source": "ethereum_calldata", "field": "function_name", "operator": "eq", "value": "setDividend", "abi": "[setDividend((uint256 recordDate,uint256 executionDate,uint256 amount,uint8 amountDecimals) newDividend) returns (uint256 dividendId_)]" }
      ]
    },
    {
      "id": "snvx2odgylm68pz4x14hn3uc",
      "name": "allow-issueByPartition-equity",
      "method": "eth_signTransaction",
      "action": "ALLOW",
      "conditions": [
        { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "296" },
        { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xf37A91c3aC757ac5f14e4b8BC92D4b1001D97ABC" },
        { "field_source": "ethereum_calldata", "field": "function_name", "operator": "eq", "value": "issueByPartition", "abi": "[issueByPartition((bytes32 partition,address tokenHolder,uint256 value,bytes data) _issueData)]" },
        { "field_source": "ethereum_calldata", "field": "issueByPartition._issueData.partition", "operator": "eq", "value": "0x0000000000000000000000000000000000000000000000000000000000000001", "abi": "(same issueByPartition ABI)" }
      ]
    },
    {
      "id": "myjpkp43eogob04pvjjmg20x",
      "name": "allow-grantRole-corporate-action-to-spv",
      "method": "eth_signTransaction",
      "action": "ALLOW",
      "conditions": [
        { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "296" },
        { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xf37A91c3aC757ac5f14e4b8BC92D4b1001D97ABC" },
        { "field_source": "ethereum_calldata", "field": "function_name", "operator": "eq", "value": "grantRole", "abi": "[grantRole(bytes32 _role,address _account) returns (bool success_)]" },
        { "field_source": "ethereum_calldata", "field": "grantRole._role", "operator": "eq", "value": "0xa1acfc499025c99f55059195e6276f639d34a18aad7b8121b9192b7f438c55cd", "abi": "(same grantRole ABI)" },
        { "field_source": "ethereum_calldata", "field": "grantRole._account", "operator": "eq", "value": "0x182F8c7DDbDa1b5f295893E2c4f6D55abBdB9926", "abi": "(same grantRole ABI)" }
      ]
    }
  ],
  "_note": "Copied by the maintainer from the Privy dashboard (policy JSON tab) on 2026-09-12 after stage 60, final-demo instance c1-202609121659. The full ABI arrays of the calldata conditions are abbreviated here to their signatures; everything else is verbatim. Public data only."
}
Limitation. The Privy dashboard shows these wallets at $0.00 and lists none of their transactions: Privy’s balance lookup does not cover Hedera (chain 296), and Privy only signed — the runner broadcast the transactions. The money side is on HashScan (see the Hedera tab).

Screenshots

Stage 30. SPV Privy policy: eth_signTransaction ALLOW only if chain 296 AND to = ATS v8 Factory.
Stage 60. SPV Privy policy: factory rule + the three equity-pinned rules; exports denied; default deny.
Stage 70. The Agent’s Privy policy: chain 296 AND to in [A, B] AND value ≤ 20 HBAR.
Stage 92. One Privy policy per execution wallet; override-scoped signers; Agent policy exactly the approved boundary: true.

Six transactions. All SUCCESS. Check them live.

Hedera testnet (chain 296), Asset Tokenization Studio v8 through the official Factory. Every transaction was confirmed by the maintainer after a dry run that simulated the exact call and got a Privy dry signature. The buttons below query the public Mirror Node from your browser.

Accounts

RoleEVM addressHedera IDSigner
SPV execution wallet0x182F8c7DDbDa1b5f295893E2c4f6D55abBdB99260.0.10509879 ↗Privy policy catenor-one-spv-execution-4c875d44
Distribution Agent wallet0x90894535F5f35271f2d4112179BF2b4e887913E30.0.10511413 ↗Privy policy catenor-one-distribution-agent-3d8dc074
Investor A receiving wallet0x7de572532820B22040B561e68543419419b724730.0.10509881 ↗receive-only
Investor B receiving wallet0xCfCa833A8e6b7651E44480b92822576BcB78Da730.0.10509882 ↗receive-only
Testnet treasury (funding only)0x49e969483fEd8b2419D60EcB23D34C89af2E52100.0.10509855 ↗Privy policy: chain 296, ≤ 30 HBAR
ATS equity “Catenor One Demo SPV 001 (SYNTHETIC)”, C1SPV0010xf37A91c3aC757ac5f14e4b8BC92D4b1001D97ABC0.0.10510175 ↗
ATS v8 Factory0xd1F118A40f3b02883D35909eF2517e7EDd78379d0.0.9213391 ↗

The asset lifecycle, each step gated by Catenor

#TransactionCatenor gateSigner (Privy policy rule)HashRecorded resultLive check
1deployEquity (Factory)Sponsor TOKENIZE_ASSET → ALLOWSPV · allow-ats-factory-hedera-testnet0x2396ad88…c570 ↗SUCCESS · 6,898,334 gas · 7.86 ℏ fee
2issueByPartition → A, 600A’s SUBSCRIBE_OFFERING ALLOW from the deployed CRE (stage 44)SPV · allow-issueByPartition-equity0x59359ae9…fa4e ↗SUCCESS · 456,629 gas
3issueByPartition → B, 400B’s ALLOW decision, the same waySPV · same rule0x9c6c4cb2…0d62 ↗SUCCESS · 405,329 gas
4grantRole(ROLE_CORPORATE_ACTION, SPV)SPV · allow-grantRole-corporate-action-to-spv0x726032ce…9f94 ↗SUCCESS · 179,949 gas
5setDividend(amount 1, decimals 2)SPV · allow-setDividend-equity0xaac4a085…f865 ↗SUCCESS · 532,107 gas · entitlements A 6, B 4
6payout → A, 6 HBARAgent’s delegated EXECUTE_DISTRIBUTION and the TEE plan: A PAY 6Agent · allow-distribution-payout-hedera0x4781f6ef…ff24 ↗SUCCESS · 21,000 gas · 0.0.105114130.0.10509881 6.00000000 ℏ
payout → BTEE plan: HOLD 4 (RED / SANCTIONS)none: no transaction built, no signature requested

Final state · stage 91, 22:54 UTC

Investor A · Lisa

600 units

7.0 ℏ = 1 ℏ activation + 6 ℏ payout. Entitled to 6, received 6.

Investor B · Bart

400 units

1.0 ℏ = 1 ℏ activation. Entitled to 4, received 0.

Equity C1SPV001

1000

totalSupply, decimals 0. Dividend 1 entitlements: A 6, B 4.

Check the balances live. This reads the Mirror Node now, so it can differ from the run’s final state if these testnet accounts have moved since.

Bootstrap funding

Testnet gas and account activation, not a distribution. The Privy treasury received 100 ℏ from the portal faucet, then sent:

ToTransactionLive check
SPV250xa2b78912…7f08 ↗
Investor A (activation)10x79829109…0060 ↗
Investor B (activation)10xeb370383…6c4f ↗
Distribution Agent120x229a0682…c5a2 ↗
SPV (unneeded top-up)250xd1180ca8…638e ↗

Prize fit: “Tokenization of Anything”

  • Asset Tokenization Studio v8 contracts through the official Factory. Issuance: issueByPartition ×2. Configuration: the corporate-action role and the dividend. Lifecycle: the dividend entitlement plus the distribution.
  • Deployed and demonstrated on Hedera testnet, verifiable on HashScan.
  • An ineligible holder is visibly blocked: entitled to 4, received 0.

Screenshots

Stage 60. deployEquity: SUCCESS, block 40438071, fee 7.864 ℏ.
Stage 63. Entitlements, dividend 1: A 6, B 4.
Stage 83. Payout: 0.0.105114130.0.10509881, 6.00000000 ℏ.
Stage 91. 6 transactions SUCCESS; A 7.0 ℏ, B 1.0 ℏ; entitlements A 6 / B 4.

Re-verified from signed documents.

Stage 93 (22:57 UTC, local, read-only) re-verifies signatures and chains from the recorded signed documents. The operational database is an index, not proof.

Audit hash chain

valid

79 events in 28 types, append-only.

Trust Anchor

VALID

TRUST_ANCHOR_VALID: true, no failed checks. did:catenor:656d…3cab

Investor credentials

2 × ACTIVE

Both issuer-signed by the Trust Anchor. Bart’s is still valid, as the thesis requires.

Audit events by type

The counts include the three abandoned stage 11 attempts (ADMISSION_REQUESTED 4, CONFIDENTIAL_VERIFICATION_REQUESTED 4, CONFIDENTIAL_EVIDENCE_VERIFIED 1). INVESTMENT_EXECUTED is 2 of 4 INVESTMENT_AUTHORIZED: the dry runs authorize without executing.

The authority chain

Relationships, capabilities and delegation are public-key-verifiable authority with no sensitive input, so none of this runs in the CRE, by design.

Root Trust Anchor
Admitted · ACTIVE
did:catenor:656d66dff9ce1db692786ee241bc3cab
  • Authority from Admission on the deployed CRE workflow
  • Issues the investor credentials
Sponsor
5 capabilities · ALLOW
did:catenor:5b87cad5d25e94dbf62f4443cf85761d
  • AUTHORIZED_SPONSOR_IN · valid
  • TOKENIZE_ASSET · DEFINE_OFFERING_POLICY · CREATE_AGENT · CREATE_DISTRIBUTION · DELEGATE_DISTRIBUTION_AUTHORITY
Distribution Agent
Delegated chain · ALLOW
did:catenor:9b25f52aff6dc4cac4d0a7aa3d8dc074
  • AGENT_OF Sponsor · valid
  • EXECUTE_DISTRIBUTION on spv:catenor-demo-001
GrantIssuer → subjectActionResourceValid until (UTC)
capability-grant:34fd209d-32af-43f5-a3f5-37acbae12c14Trust Anchor → SponsorCREATE_DISTRIBUTIONspv:catenor-demo-0012026-10-12 20:00:46
capability-grant:e250671b-c958-434b-a536-5dc3a01fe996Trust Anchor → SponsorDELEGATE_DISTRIBUTION_AUTHORITYspv:catenor-demo-0012026-10-12 20:00:46
capability-grant:7bd93930-e21c-4f78-86fd-8f8c9aa46156Sponsor → AgentEXECUTE_DISTRIBUTIONspv:catenor-demo-0012026-10-02 22:19:50
capability-grant:1984657d-ecab-403e-850f-67f5f29d3cc6Trust Anchor → SponsorTOKENIZE_ASSETspv:catenor-demo-001— (used for stages 60–62)

The Agent’s grant runs 20 days, inside the Sponsor’s 30: delegated authority stays within the delegator’s authority.

Every refusal in the run

Three of them are DISTRIBUTION_REQUEST_DENIED audit events. The rest are refusals at the authority, presentation or signer boundary.

StageWho asked for whatResultInvariant
21Sponsor, a granted capability on another resourceDENY CAPABILITY_MISSINGAuthority is scoped to the resource
43Replay of a presentation with another challengeHOLDER_PROOF_VALID falsePresentations are bound to a challenge and a domain
70Agent runs a distribution before any delegationDENY CAPABILITY_MISSING · audit eventCreated ≠ authorized
71Agent with only the relationshipDENY CAPABILITY_MISSING · audit eventRelationship ≠ Capability
72Sponsor delegates TOKENIZE_ASSETrefused ACTION_NOT_DELEGABLE, no signature requestedDelegability must be explicit
72Investor A presents the Agent’s grantDENY SUBJECT_MISMATCHA grant belongs to its subject
82Investor A requests the distributionDENY CAPABILITY_MISSING · audit eventOnly the delegated Agent may distribute
82Investor B, eligibility at distribution time (in the TEE)DENYHOLD 4 · FINAL, SANCTIONSSignature validity ≠ current eligibility
83Payout to Investor Bno transaction constructed · 0 signature requestsPolicy Decision ≠ Execution Authorization

Verify it yourself

From the repository root. All read-only.

  1. scripts/demo/90-show-cre-execution.sh — the CRE executions: the local sanitized records against the CRE platform’s own execution list.
  2. scripts/demo/91-verify-hedera.shpublic chain data, no keys: the JSON-RPC relay and the Mirror Node. Anyone can run it.
  3. scripts/demo/92-verify-privy.sh — Privy wallets().get / policies().get read-back. Needs the Privy app credentials.
  4. scripts/demo/93-verify-catenor-audit.sh — re-verifies the audit hash chain, the Trust Anchor, the authority chains and the credentials from the recorded signed documents (the run records stay local and git-ignored).
  5. scripts/demo/99-verify-complete-demo.sh — every stage record plus the Catenor verification, in one summary.

Screenshots

Stage 93. Audit hash chain valid (79 events); Trust Anchor VALID; Sponsor and Agent chains ALLOW; credentials ACTIVE.
Stage 99. All 24 stages ok: true; the Catenor verification passes.

What went wrong, and how it was fixed.

None of these consumed a set-once state or duplicated an on-chain action. The full stage records stay local and git-ignored; the public, reviewed record is artifacts/final-demo/RUN-LOG.md.

The day, in phases (UTC)

TimePhaseOutcome
16:59A · Local preparationInstance c1-202609121659; Railway PostgreSQL with 0 Catenor rows, 5 migrations
17:08Stage 10 · Trust Domain bootstrapSigner infrastructure; Bootstrap Configuration hash
17:24–17:45C · CRE deploy #1Relay check OK; 3 Vault DON secrets; workflow Active; runner in DEPLOYED mode
17:50–19:18Stage 11 · AdmissionAttempts 1–3 refused at the gateway; control workflow at about 18:15; attempt 4 ADMITTED
19:33–19:38E · CRE deploy #2Trust Anchor key pinned; same binary, new config, new ID
19:58–20:22F · Sponsor, SPV, offeringNo spending
20:26–20:45G · InvestorsOnboarding, credentials, presentations, offering eligibility
20:55–22:08H · Hedera ATSFunding, tokenization, issuance, dividend
22:10–22:28I · Distribution AgentDENY → DENY → ALLOW; Agent funded with 12 HBAR
22:29J · Investor B becomes ineligibleSumsub sandbox RED, SANCTIONS, FINAL
22:37–22:45K · Confidential distribution and paymentPAY 6 / HOLD 4; one payout
22:53–22:59L · VerificationStages 90, 91, 92, 93, 99

Incidents

  1. Stage 11, attempts 1–3: the documented gateway does not serve this organization. Attempt 1 got CRE gateway HTTP 400 with no detail; the verifier now surfaces the gateway’s code and message. Attempt 2 got -32600 Workflow not found, although cre workflow list showed the workflow ACTIVE. A minimal non-confidential control workflow (catenor-http-control, 0038de78…a66f) isolated the cause: the same signed request failed on the documented enterprise gateway and was ACCEPTED on https://01.gateway.zone-a.cre.chain.link. The verifier’s default gateway changed (overridable with DEMO_CRE_GATEWAY_URL). Attempt 3 ran while the fix was still being written. Attempt 4 was admitted. No attempt created an execution or consumed the set-once root.
  2. secrets.sh failed on the CRE_ETH_PRIVATE_KEY placeholder. The CLI parses that key from the .env it is given, even in browser mode: invalid hex character 'Y'. Fixed with a fresh random, unfunded key, written without printing it. It is not linked to the account and is not the trigger key.
  3. Stage 60 recorded the asset reference instead of the address, after a successful deployEquity, and failed with invalid equity or SPV address. The runner now records the checksummed address and the reference separately, and never redeploys an existing equity. The 21:25 UTC re-run reported “already deployed … no new deployment” and completed the owner-authorized SPV Privy policy extension. The equity was not deployed twice.
  4. A transient CLI account error after deploy #2 (“unable to retrieve organization info… try again in a few minutes”). A re-run a few minutes later was clean; deployed triggers do not depend on the CLI session.
  5. An unneeded 25 HBAR SPV top-up in the second funding run: the script tops up any wallet below its suggested amount. Accepted to save time; it is testnet HBAR and the SPV had no further transactions.
  6. An earlier instance was stopped. c1-202609121438 was stopped after stage 11 because its admission ran on CRE simulation. The final run restarted on a new Privy app so that the admission also ran on the deployed workflow.

The gateway control experiment

GatewayResult
https://01.enterprise-gateway.zone-a.cre.chain.link/ (documented for the private registry)HTTP 400 -32600 Workflow not found, twice, including about 5 min after the deploy
https://01.gateway.zone-a.cre.chain.link (embedded in CRE CLI v1.33.0)HTTP 200, ACCEPTED · execution 0xf85c571f52d9605e711f3ab37ef64dbd53af6aac67cf14245b1125e880e660b6

Honest labels

SystemLabelWhat that means here
Chainlink CREDeployedThe Confidential Workflow identity-confidential-production on the Chainlink-hosted private registry, admission included. Not cre workflow simulate
PrivyReal development appNew app catenor-one-ethonline-2026: real server wallets, Privy policies and key quorums in a development environment
SumsubSandboxSynthetic applicants, level id-only; reviews set by the operator. “Lisa Simpson” and “Bart Simpson” are fictional
Company KYBSynthetic mockUsed only for the root Trust Anchor admission
HederaTestnetChain 296. HBAR came from the portal faucet. Nothing ran on mainnet
RailwayAPI + relaycatenor-one-production.up.railway.app: API and CRE relay only, inert. Its Postgres holds the demo’s operational state
Still open after the run: reportFromDon / offchain report verification (P1), and deleting the control workflow catenor-http-control.
Study notes
Explain it simply.
Short, honest answers for judges — and for anyone rehearsing the pitch.
Why does Catenor need a canonical identity?

Because wallets, providers and execution accounts change. The subject persists: a random did:catenor with no PII, and wallets attached to it through private Account Bindings. In this run every actor — the Trust Anchor, the Sponsor, the SPV, each investor, the Agent — has its own DID, and none of them is its wallet.

Why separate Relationship from Capability?

A relationship answers who someone acts for. A capability answers what they may do, on which resource, until when. The run shows it twice: the Agent’s AGENT_OF relationship was valid and its request was still DENY CAPABILITY_MISSING; only the delegated EXECUTE_DISTRIBUTION grant made it ALLOW.

What is actually being tokenized?

1,000 equity-interest units in “Catenor One Demo SPV 001”, a synthetic real-estate SPV, deployed as an ATS equity (C1SPV001) on Hedera testnet. The demo does not claim that a land deed is represented by the token.

Why does Bart still hold 400 units?

Because ownership and current payment eligibility are different things. Bart was eligible when he invested — the TEE said ALLOW 400 — and the units were issued. A later sanction does not silently erase ownership or his dividend entitlement of 4. It changes whether he may be paid today, and that is decided confidentially at distribution time: HOLD 4.

Why Privy if Catenor already handles authority?

Catenor gives the semantic decision: this Agent may distribute for this SPV, and this holder is PAY. Privy constrains what a specific key can physically sign, whatever the application asks, through its Privy policy: the Agent’s key can only send on chain 296, to Lisa’s or Bart’s receiving wallet, up to 20 HBAR. Two independent fences. Policy Decision ≠ Execution Authorization.

What exactly happens inside CRE?

In handlerInTee the deployed workflow opens a sealed context, fetches Sumsub credentials from the Vault DON, calls the Sumsub sandbox, verifies each VP, VC and issuer-signed status statement against the pinned Trust Anchor key, reconciles the current evidence with the credential, applies the pinned policy and — for the distribution — computes 10 HBAR × units / 1000 and PAY or HOLD per holder. Five operations, five DEPLOYED executions, about 8 s each.

What exactly leaves the TEE?

Decisions, requirement statuses, the seven check results, the reconciliation class, coarse reason codes (FINAL, SANCTIONS) and commitments. No Sumsub response, name, document or applicant data. Two honest caveats: the reason codes do reach Catenor, and on the deployed platform the workflow’s user logs are visible per node — which is why it logs only non-sensitive markers.

Why is the tokenization not in CRE?

Because it has no private input. Deploying the equity, issuing units and setting a dividend are public actions on Hedera, gated by public-key-verifiable authority (the Sponsor’s TOKENIZE_ASSET grant from the admitted Trust Anchor). The TEE is used where private, current evidence is involved: the admission, the credentials, offering eligibility and the distribution. The issuance amounts are still bound to TEE-backed decisions: 600 and 400 are exactly what the stage 44 run approved.

Why was the workflow deployed twice?

Deploy #1 carried the Bootstrap Configuration hash for the admission. The Trust Anchor’s key did not exist until that admission, so deploy #2 pinned it as the only accepted credential issuer. Same binary (9902db58…), new config, updated in place; the execution history survived.

Why does Bart get no signature request, rather than a refused one?

Because a HOLD holder never reaches the executor. The Catenor signer boundary builds a transfer only for a PAY holder, to that holder’s privately bound account, for exactly the plan amount. The Agent’s Privy policy is a second fence that was never needed for Bart: investorBSignatureRequests: 0, and the Agent’s nonce went from 0 to 1.

Is this production?

No, and the page says so everywhere. The CRE workflow is really deployed on Chainlink’s private registry, and the Privy wallets and Privy policies are real — in a development app. The investors are synthetic Sumsub sandbox applicants with fictional names, the company KYB is a synthetic mock, and the asset lives on Hedera testnet. Catenor did not independently verify an enclave attestation, and reportFromDon verification is not implemented yet.