Hide My Email, for your money.
- Live demo: usefacet.xyz
- Demo video: Watch the Facet walkthrough
The recording shows the reviewed wallet-mediated Mainnet path. Receipt-backed transactions, technical evidence, and the product boundary are documented below.
Facet is a private application launcher for Starknet. Its target model is one shielded balance funding a persistent, context-specific account for each compatible application or strategy. Mainnet evidence includes receipt-backed Facet/Ekubo, Facet/Endur, and xSTRK exit actions and deployed Facet helper contracts; the direct identity sequence is proven on Sepolia.
One balance. A different face in every app.
Facet's privacy goal is to remove the direct public funding link between a shielded balance and app-specific identities. It is not a guarantee against correlation. It does not hide an app account's downstream protocol activity: its address, calls, balances, timing, and user-chosen recipients are public or inferable once it reaches Starknet.
The launcher now reads the unified private portfolio from Ready X and can reconcile deterministic per-app accounts from the Mainnet anonymizer when the connected wallet exposes the optional shadow-account commitment request. The current Mainnet routes still fall back honestly to the wallet-mediated helper path when that capability is absent; the direct write path remains gated by the pool's external screening attestation.
Read docs/PRODUCT.md for the product model, privacy boundary,
account lifecycle, and architecture. This README keeps the implementation evidence and
the runnable path close at hand.
Every Starknet address is a permanent public record of everything its owner has ever done. That is not an abstract problem:
- Copy-trading and front-running. A trader who is good at this gets watched. Their entries get front-run, and there is nothing they can do about it while their positions are public and attributable.
- Liquidation hunting. A leveraged position has a public liquidation price, and searchers hunt it deliberately.
- Portfolio doxxing. Anyone who learns your address knows your net worth — permanently, and in some jurisdictions that is a physical safety problem rather than a privacy preference.
Facet is designed to give you one persistent identity per application or strategy instead of one identity forever. The Mainnet launcher can now reconcile those deterministic accounts when the wallet exposes the required read capability; funding and direct account execution still depend on the wallet and pool transport. Where the direct identity path is used, its private funding does not name you, but public behaviour, repeated amounts, timing, recipients, or transfers between facets can still create a link.
Built on STRK20 shadow accounts — a primitive that lets a proved private transaction fund a deterministic account, have it call a normal Starknet application, and settle the result back into shielded notes. Facet turns that primitive into an account and portfolio model rather than asking users to think in raw commitments, notes, and contract phases.
Status: in development. Nothing here is audited. Do not route funds you cannot afford to lose. Claims in this README are traceable to a source reference or a transaction hash; anything not yet done is marked as such.
Start with docs/PRODUCT.md for the product, then use
docs/SHADOW_ACCOUNTS.md for the underlying primitive:
derivation, the action model, the funding pattern, what leaks, and every revert with its
cause. The implementation is intentionally documented at both levels: what a user gets,
and how the protocol makes it possible.
The fork-backed contract checks run locally from the repository root with the pinned toolchain and the configured read-only RPC endpoints:
cd packages/contracts
snforge testThe pinned environment is Node 22.23.0 (.nvmrc), Scarb 2.17.0, Starknet Foundry 0.59.0, and
Universal Sierra Compiler 2.10.0; it currently runs 20 tests, 0 failures against Mainnet and
Sepolia forks. If
snforge reports that universal-sierra-compiler is missing, install the toolchain named in
.tool-versions before treating a failed command as a contract failure. The suite does not
prove the full transaction path — that path is proved on chain instead, in
docs/FINDINGS.md §§6.17–6.18 and the Mainnet receipts below.
The STRK20 privacy pool can run compatible dapp calls on a user's behalf through a per-user shadow account, without publishing the private funding link back to the user. The pool derives an identity key inside a proved execution:
identity_key = poseidon(IDENTITY_KEY_TAG:V1, user_addr, user_private_key, anonymizer_addr)
and hands it to the anonymizer, which never learns who the user is. The anonymizer then scopes it further:
partial_commitment = poseidon(identity_key, dapp_name)
identity_commitment = poseidon(partial_commitment, nonce)
The result is one identity per user, per anonymizer, per dapp, per nonce — each backed by its own contract account. The derivation separates contexts; it cannot prevent a user from linking them later through public behaviour.
That is a facet: one visible face of a single private portfolio, different from every angle, still one portfolio.
| Layer | What it does |
|---|---|
| STRK20 privacy pool | Holds shielded STRK notes and applies proved UseNote, Withdraw, and ComputeAndInvoke actions. |
| Viewing-key flow | Uses the wallet-bound private identity inside the proved execution; the key is never written to the repository. |
| Immutable anonymizer | Derives a fresh shadow-account identity from the user, anonymizer, dapp name, and nonce. |
| Shadow account | Becomes the public caller seen by the dapp and is funded at its deterministic address from the private note. |
| Protocol-bound helper | A Facet-owned helper is bound to the pool, STRK, and one protocol vault, then calls only that vault. |
| Ekubo router | Receives the shadow account's STRK call and returns the swap balances for private-note settlement. |
| Transaction prover | Re-executes the signed Invoke V3 and supplies the proof facts required by the pool. |
| Relayer / paymaster | Submits proof-bearing transactions and can sponsor execution where the network path supports it. |
wallet signature + private identity
│
▼
STRK20 shielded note
│ UseNote → Withdraw
▼
deterministic shadow account
│ call the selected protocol as the caller
▼
STRK / ETH private notes
The underlying contracts exist, but turning them into a reliable product requires more than deriving a new address. The system has to preserve privacy across funding, application execution, settlement, proving, fee payment, recovery, and portfolio display.
- A shadow account anonymizer has been live on mainnet since 23 July 2026
(
0x4f33230dc57855c6e7eabe66dfa0fde82c5458fd0e54827cdb7cb4c474888a7, deployed at block 12,199,879). - It had been invoked 39 times by the historical measurement cutoff, and all 39 were
decoded. Every one in that dataset issues one call against the same contract — the STRK
token. Thirty-two are
balance_ofreads; seven usetransfer_fromto pull pre-approved funds into the shadow account. None of those 39 historical invocations interacted with a DeFi protocol. - The current product state is different: Facet has since verified wallet-mediated Mainnet actions through its Ekubo and Endur helpers, including the xSTRK exit. Those receipts are listed below; the historical 39-invocation finding is intentionally preserved rather than rewritten.
- The official docs contain no coverage of it whatsoever. Across the full 121 KB
documentation dump:
shadow0 occurrences,stealth0,identity_key0,identity commitment0,invoke_with_computation0.
There is a concrete reason, documented in docs/FINDINGS.md:
privacy_invoke_with_computation never receives funds. It resolves or deploys the
shadow account, snapshots balances, runs the calls, and collects — but nothing puts
tokens in. Anything requiring capital has to be funded first.
The seven transfer_from invocations are a workaround for exactly this: an external
account pre-approves the shadow account, and the shadow account's first call pulls the
funds in. It works — and it leaks. Granting that allowance takes a public approve
from a funded, non-private address naming the shadow account as spender, which ties a
real identity to the facet and defeats the purpose.
Facet funds the shadow account from a shielded note instead, at its deterministic address, inside the same transaction:
UseNote spend a shielded note
Withdraw send tokens to the shadow account's predicted address
ComputeAndInvoke deploy it there, run the dapp calls, settle to an open note
No public approval, no funded address linked to the facet.
Everything asserted above is recorded with a file:line reference or a block height in
docs/FINDINGS.md, including:
- pool identity, deployment block, and a full 112,464-event activity breakdown
- the two-tier
ClientAction/ServerActionmodel, which is not described in the documentation and is the most common way to lose days on this stack - a field-by-field decode of a real mainnet shadow-account invocation
- the one-invoke-per-transaction constraint and the required action ordering
The funding pattern above is also exercised as a test suite against the live deployed
anonymizer, forked at mainnet block 13,329,863 — a predicted address is funded before
any code exists at it, and the shadow account deploys exactly there and collects the
balance. The pinned environment has run 20 Cairo tests successfully against recorded
mainnet and Sepolia state, including the decoded invocation replayed against real bytecode
with a control that breaks the decode as it must. A fresh environment must install the
Universal Sierra Compiler named in .tool-versions; with that dependency present, the same
fork suite is reproducible. What those tests cannot cover is the proved half — UseNote,
Withdraw, and the ClientAction → ServerAction translation are unreachable from a fork
test. That half is proved on chain instead: FINDINGS.md §6.17.
apply_actions needs a proof, and we could not find a hosted proving endpoint published
anywhere — every reference in the SDK, the docs dump and the demo configuration is a
placeholder. The transaction prover is a public container, so the answer is to run it. The
catch is that the published linux/amd64 image aborts with SIGILL on older AMD hosts,
before it reads any configuration, in a way that looks like a broken pull.
It is a build flag: the published image is compiled for Zen 5. Rebuilding the same upstream revision for your own target fixes it.
docs/PROVER.md is the whole thing written down — the diagnosis, the fix,
the measured memory floor, the request format, the historical-replay trap that costs a day,
and which RPC providers actually serve the storage proofs the prover needs. Alongside it:
infra/prover/build.sh |
Builds for any CPU target and verifies the binary starts |
infra/prover/docker-compose.yml |
Loopback binding, memory limit, working health check |
ghcr.io/jennycruzy/facet-prover:znver2 |
Prebuilt for Zen 2 and newer AMD, if you would rather not build |
Two proofs generated on a 2 vCPU host, 355s and 485s, peaking at 6.6 GiB. Neither is a benchmark — the same request on the same machine varied by 27%, which is itself worth knowing before you size anything.
None of this is specific to Facet. If you are building on STRK20 and hit the same wall, take it.
Facet is not a new privacy protocol. It uses StarkWare's, unmodified.
It is not a claim of exclusivity: of 326 compute-path calls on mainnet, 287 went to six other custom anonymizers built by other teams. Others are competent in this territory — they rolled their own rather than using the shadow account contract.
Nor is it a claim that nobody has funded a shadow account — seven transfer_from
transactions have. The difference is that their method needs a public approve from a
funded address, naming the shadow account as spender, which links a real identity to
the facet.
The claim made here is narrow and checkable: Facet provides a private account and portfolio layer by funding context-specific shadow accounts from shielded notes, then settling application results back into shielded notes. The implementation evidence, known limits, and unfinished product layers are all called out below. The intended product journey is asynchronous because proving takes minutes: queue an allowlisted action, return a job id, keep a compatible worker warm, and resume by polling. A queue improves the user's wait experience; it does not make the proof computation faster.
Claims in this README are traceable to a source reference, a block height or a test run. What is not built is listed as plainly as what is.
| Research | docs/FINDINGS.md — pool, anonymizer, identity derivation, all 39 invocations decoded |
| Contracts | packages/contracts — 20 fork tests passing with the pinned Scarb, Foundry, and Universal Sierra Compiler toolchain |
| Prover tooling | docs/PROVER.md, infra/prover/ — diagnosed, fixed, documented, reusable by anyone |
| SDK | packages/sdk — adapter, lifecycle, recovery-classification, private-transaction primitives, and a tested compatible-app example plus the operational Sepolia runner; build clean, 43 tests passing |
| Private transaction | executed on Sepolia, 25 August 2026 — see below |
| Product layer | working wallet-mediated demo with chain-backed portfolio reads — Facet's reviewed Ekubo/Endur routes, tested xSTRK exit composition, shared SDK executor, private-balance reader, optional deterministic account reconciliation, recoverability view, local activity cache, and generated browser SDK bundle are shipped; direct shadow-account writes, automatic protocol exits, and the async service remain gated or future work |
| Mainnet contracts | deployed — immutable anonymizer, FacetAccount, Ekubo helper, and Endur helper are deployed; the current protocol routes have receipt-backed evidence |
| Mainnet interaction | verified across Ekubo and Endur — the 7 STRK eligibility shield plus reviewed Facet/Ekubo, Facet/Endur, and xSTRK exit actions are confirmed on Mainnet |
The UseNote → Withdraw → ComputeAndInvoke sequence was first executed by this project on
25 August 2026. It ran on Starknet Sepolia twice and succeeded — proved by a self-hosted
transaction prover and submitted through a self-hosted paymaster:
-
0x05faace1d275d2a301b10dd1fb3f809cc65d3ba8799fbc68f0828eca4a1dedef, block 14,018,840 — the shadow account deploys at its predicted address, 0.5 STRK is withdrawn to it, and the full amount is collected back. -
0x0111b815a660ee41c17bf285bde7c6b43cbef5bc5d6fbf43d25e94e7f17f3693, block 14,020,928 — the same withdrawal, then a dapp call executed as the shadow account, then the remainder collected back into the shield. The account's balance afterwards is 0. -
0x4cee84654535d0f98f7a8e0402fce4c47aab1ff62b6b132d725184e5eb30a07and0x68510769914a25f6dc9d90fa7f5672bd83908c4ddafc77b1fd6ff3782286b3a— a second facet namedfacet-second; its one-wei call went to an unrelated0x…deadrecipient rather than the owner. The predicted shadow account is0x560b1983…e2b8.
The event-level decode is docs/FINDINGS.md §6.17. The reviewed Wallet API Ekubo action is now
verified in Mainnet transaction
0x2d3c449ebb9cef73f953df5c233a6d932c6f0a4dd5f1f54fc5605e3eab236ab, block 14,004,049:
the receipt succeeded and emitted STRK20 pool and Ekubo core events, while the transaction data
contains the deployed helper and router. The reviewed Wallet API Endur action also succeeded in
0x240d2b8285a19485536f686ef9915eb1c6ae5214091ebd10b9770ecab2163f5, block 14,052,044, with
STRK20 pool, Endur helper, and Endur xSTRK events.
The reviewed xSTRK exit then succeeded in
0xf5ac560c25e7935cb47691d2f025735395e45d04de723a818d5b5a2df090b0, block 14,134,005:
the configured Facet helper sent the xSTRK position through Ekubo's initialised pool and the
STRK result returned to the private balance. The receipt contains STRK20 pool and protocol/token
events, and its transfer path identifies the configured helper and Ekubo router.
There is also a direct Facet Mainnet transaction:
0x54ae85094a3baaba9e27c39b52687f3149c6c2a9c532f84452f3d75e4e60b1e.
It was accepted and finalized on L1, but reverted with EMPTY_PROOF_FACTS; its approval and
registration state changes were rolled back. It proves that Facet's direct transaction reached
Mainnet, not that the action succeeded. The later proof-compatible route progressed further and
was rejected before broadcast because it lacked the live pool's authorized screening attestation.
That distinction, including the RPC receipt and AVNU response, is recorded in docs/FINDINGS.md
§§6.22 and 6.27. A retired Vesu experiment is preserved there as historical failure analysis,
not presented as a supported route.
| Document | Contents |
|---|---|
docs/PRODUCT.md |
Product model: private portfolio, context accounts, user flow, privacy boundary, and roadmap |
docs/SHADOW_ACCOUNTS.md |
The guide to the primitive that does not otherwise exist — derivation, the two-tier action model, the funding pattern, what leaks, and every revert with its cause |
docs/FINDINGS.md |
Everything verified from source and chain data |
docs/PROVER.md |
Self-hosting the transaction prover, start to finish |
docs/PRIVATE_DEFI.md |
The complete shielded STRK20-to-DeFi path, prover, proof facts, paymaster, timing, and safe runbook |
docs/ASYNC_PROVING.md |
The warm-worker job contract, resumable polling flow, quote expiry rules, and service security boundary |
docs/PROGRESS.md |
Dated record of what was established, and when |
| Concern | Implemented work and current boundary |
|---|---|
| Protocol adapters | Implemented and exercised. One public ProtocolAdapter interface powers the Ekubo and Endur builders. Each produces reviewed protocol calls, helper bindings, amount bounds, settlement hints, and recipient declarations; the browser and SDK executors are pinned by parity tests. Facet supports compatible, reviewed applications rather than arbitrary calldata. |
| Dapp SDK flow | Implemented reference flow. An application supplies intent; Facet selects the adapter, builds and validates the plan, then hands it to FacetExecutor. executeAppIntent, the shared executor, and packages/sdk/examples/compatible-app.ts cover the complete intent → plan → execution composition, with golden vectors matching the verified Ekubo and Endur actions. A different submission transport can implement the same executor interface. |
| Funding and spend policy | Separated and tested. Facet defines fixed public pool-funding denominations while allowing a user-selected app spend within each route's bounds. assertFundingDenomination and the route policies are covered by tests; the current Mainnet shielding surface remains a wallet integration boundary rather than a Facet UI feature. |
| Recipient and route safeguards | Implemented for declared route inputs. Every plan declares its public recipients, and the SDK/browser executors reject linked recipients, unsupported assets, invalid amount bounds, undeclared asset kinds, unsafe collection policies, and settlement/open-note mismatches. This is a strong adapter policy boundary, not an arbitrary-calldata decoder or a promise that downstream activity cannot correlate. |
| Lifecycle and recovery | Implemented for local state and confirmed route exits. The launcher and SDK use launch → use → hold → recover → retire, expose guarded recovery/retirement controls, classify fungible deltas separately from persistent positions, and include the Endur xSTRK → STRK exit through Ekubo. Fungible balances can be recovered where the route declares them safe; xSTRK, LP positions, debt, NFTs, and receipts still require explicit protocol exits. The configured xSTRK exit has a receipt-backed Mainnet execution. |
| Persistent facets | Deterministic discovery and portfolio reconciliation shipped where supported. The launcher reads Ready X's private balances and, when the wallet exposes wallet_strk20ShadowAccountCommitment, resolves each app context through the Mainnet anonymizer and reads public token balances. The local map caches only confirmed activity and the latest public observation; it never stores the commitment or claims authority over chain state. Direct Mainnet shadow-account execution still needs a wallet transport plus the pool's screening attestation. |
| Mainnet execution | Facet-owned routes are live. Facet owns the intent, adapter plan, helper contracts, route allowlist, calldata, amount/recipient policy, settlement rules, and lifecycle record behind the verified Ekubo, Endur, and xSTRK exit actions. The current Wallet API is the transport adapter for wallet-side signing, proof handling, and submission; it does not define Facet's product or route policy. |
| Direct Facet Mainnet path | Direct transport evidence exists. The direct Facet transaction reached and finalized on Mainnet before reverting with EMPTY_PROOF_FACTS; a later compatible proof reached AVNU and stopped before broadcast with SCREENING_REQUIRED. The complete direct identity sequence is proven on Sepolia. A production screening-attestation source is the remaining Mainnet infrastructure dependency, documented with StarkWare's shadow-account derivation, pool-policy client, and deposit-address screening work. |
| Mainnet evidence | Receipt-backed and deployed. Facet/Ekubo succeeded in 0x2d3c…36ab, Facet/Endur succeeded in 0x240d…163f5, and the xSTRK exit succeeded in 0xf5ac…90b0, with pool/protocol events and configured helper calls. The 7 STRK eligibility shield is separate evidence; the reverted direct attempt is recorded but not counted as a successful action. |
MIT. See LICENSE.