Social-account escrows
A creator may want to launch a coin for someone who has never heard of zk-pad: an artist, an open-source maintainer, a public figure. A social-account escrow commits the fees to a platform account so that only the real owner of that account can ever claim, and the account is not published on-chain.
On the BSC mainnet deployment no attestors are registered and attestorThreshold is 0. The
FeeVault's proposeBind then always reverts with InsufficientAttestations, so no
social-account escrow can be bound or claimed until the owner registers attestors and sets a
threshold. The attestor service is not deployed yet, and without its attestorSalt no valid
handle commitment can be made, so do not create social-account escrows outside the app until it
is. Funds credited to an existing escrow id stay in the FeeVault, but only until its fallback
clock runs out: if the escrow has a fallback recipient and no bind is finalized before its
fallback delay ends (at least 180 days after registration or first credit), anyone can call
sweepToFallback and move the whole balance to the fallback recipient, after which the account
owner can no longer claim it. See trust assumptions.
If the coin should openly and verifiably say which account it supports, use a public social recipient. The account is public, and its wallet stays hidden.
Building blocks
| Piece | Role |
|---|---|
| Handle commitment | keccak256(abi.encode(platformId, userIdHash, attestorSalt, nonce)). A salted hash of the platform's immutable numeric user id, not the handle string. |
| Encrypted hint | The account details (platform, user id, nonce, attestorSalt), encrypted to the hint key of every attestor of the committee and emitted in TokenLaunched. Lets each attestor recognise and verify the escrow when the owner logs in. Only the ciphertext is on-chain. |
| Attestors | Independent services that verify an account login (OAuth) and sign an EIP-712 BindOwner message. The vault requires k of n distinct attestor signatures. |
| Timelock | A bind only takes effect after bindDelay (1 to 30 days). A rebind of an account that already has an owner waits twice as long. |
| Veto | During the timelock, the guardian or any single attestor can veto. The current owner can cancel a rebind. |
Why the user id and not the handle
Handles change hands. X resells inactive handles, Telegram usernames are tradable collectibles, and GitHub frees renamed usernames. If the escrow committed to the handle string, whoever holds the handle later could claim. zk-pad commits to the account's permanent numeric id, resolved at launch time.
What hides the account
Without secret randomness, anyone could hash every famous account's user id and find which one an
escrow is for. Every commitment includes a fresh random 32-byte nonce, known only to the
creator and the hint's readers, so dictionary attacks do not work.
The attestorSalt (an HMAC of the platform and user id under the issuing attestor's pepper) is
not what hides the account. The attestor returns it with the commitment and the creator puts
it in the hint, because attestors have independent peppers: without it the other committee
members could not recompute the commitment and a multi-attestor bind could never be collected.
Lifecycle
- Launch. The creator's browser asks an attestor for a commitment and encrypts the hint to every attestor of the committee. The coin launches with the escrow id as beneficiary. Fees start accruing immediately.
- Login. The account owner opens the bind portal. The browser generates a fresh stealth key.
The owner logs in separately with each of the k attestors. The intent (stealth key and
escrow ids) is sent in a
POSTbody and bound server-side to a random OAuthstate, so it never appears in a URL, and the login cannot be replayed for someone else's key. - Propose. The browser picks one signature deadline and asks every attestor to sign
BindOwnerwith it, becauseproposeBindchecks all k signatures against a single deadline. With k signatures, a relayer callsproposeBind. The vault checks that every signer is a current attestor, that the signers are distinct and sorted, and that the bind nonce matches. - Timelock. The bind waits
bindDelay. Anyone watching can see that a bind is pending. Attestors run a veto watcher that (withAUTO_VETO) vetoes proposals for their escrows they did not attest. If the portal finds a bind to another key pending, it explains that this may be the owner's own bind from an earlier visit and contests it only after the owner confirms, one escrow at a time (listing escrows never vetoes anything, so a login cannot publicly group an owner's escrows). - Finalize. After the timelock, anyone can call
finalizeBind. The escrow becomes a normal account controlled by the stealth key, and the owner claims like any stealth beneficiary.
Rebinding
If the owner loses the stealth key, or the platform account is recovered after a hijack, the account can be bound again through the same flow. Because the account already has an owner:
- the timelock is 2 × bindDelay;
- the current owner can cancel the rebind with a signed
CancelBind; - the guardian and every attestor can still veto.
A veto or cancel never moves funds. It only clears the pending bind, and a veto keeps the
fallback sweep closed for at least 2 × bindDelay so
the owner can be re-attested in time. A finalized rebind discards the previous owner's
shield templates.
Supported platforms
| Platform | platformId | Verification (phase 0) |
|---|---|---|
| X | 1 | OAuth 2.0 with PKCE, minimal read scopes, token revoked immediately after reading the user id |
| Telegram | 2 | Telegram Login Widget hash verification (a Mini App flow with Telegram's Ed25519 signature is planned) |
| GitHub | 3 | GitHub OAuth app |
| Farcaster | 4 | Sign In With Farcaster (FIP-11): a SIWE message with an attestor-issued nonce, signed by the fid's custody address or an auth address, checked against the IdRegistry / KeyRegistry on Optimism. The user id is the fid. |
| Discord | 5 | OAuth 2.0 authorization code with PKCE, scope identify; the user id is the snowflake; token revoked immediately |
Endpoint details: attestor API. The attestor service exists in
services/attestor but is not deployed for mainnet yet (no attestor keys or OAuth apps).
Limits you should know about
- Any single attestor learns the account. Hints are encrypted with threshold 1: each attestor of the committee opens them alone with its own key (there is no threshold decryption), so every one of them, or anyone who steals one hint key, knows which accounts have escrows. The chain does not learn this.
- k colluding attestors could bind an escrow to their own key. The timelock, single-attestor
veto, guardian veto and contest flow are there to catch it, but they rely on someone noticing.
Later phases replace OAuth with zkTLS or on-chain ZK proofs (see the roadmap in
docs/research/social-beneficiaries.md). - The platform is trusted to report who owns an account. A hijacked account could pass OAuth; the timelock gives the real owner time to contest. For Farcaster the "platform" is the fid's on-chain control: whoever holds the custody key (or an auth key it added), including after a transfer or a recovery by the recovery address, can bind.
- A paid handle lookup is public. When the creator lets the attestor resolve a handle, the x402 payment ($0.03 USDT, same price on X, GitHub and Farcaster) is an on-chain transfer from the creator's wallet. It links that wallet to "a handle-based escrow" (not the platform or the handle), around the time of the launch. Creators who care pay from another wallet or enter the numeric user ID, which is free and makes no payment (Telegram and Discord escrows always use the numeric ID).
- The coin itself often names the beneficiary. If a coin is called "Donate to @alice", the escrow hides Alice's wallet, not the fact that the coin is about her.
- An escrow may never be claimed. Set a fallback recipient so donations are not stranded.