ADR-002: Custom PDA Token Account for Vault
Context
Section titled “Context”The protocol needs an escrow account to hold tokens while they vest. The two standard Solana approaches are:
- Associated Token Account (ATA): Deterministic address from
(wallet, mint, program). Auto-discovered by explorers and wallets. - Custom PDA Token Account: A token account with a PDA-based address using arbitrary seeds, where the stream PDA is the authority.
The initial architecture documentation proposed using an ATA for the vault. This was reconsidered based on the rent economics of completed streams.
Decision
Section titled “Decision”Use a custom PDA token account ["vault", stream.key()] instead of an ATA.
The vault is a standard SPL Token account created with the Token Program, but its address is a PDA derived from the stream’s address. The stream PDA is set as the token account authority.
Rationale
Section titled “Rationale”When a stream completes or is cancelled, the accounts are closed to return rent-exempt SOL to the creator. A custom PDA token account can be fully closed via CPI to the Token Program’s close_account instruction. An ATA would remain on-chain permanently because ATAs are never closed by convention — they persist even with zero balance.
At ~0.002 SOL per account, a creator with 1,000 completed streams would permanently lock ~2 SOL in unclosable ATA accounts. At scale (10,000+ recipients), this becomes a material cost with no recovery mechanism.
Alternatives Considered
Section titled “Alternatives Considered”| Approach | Pros | Cons |
|---|---|---|
| ATA | Wallet-compatible, auto-discovered by explorers | Cannot be closed; rent locked permanently |
| Custom PDA token account (chosen) | Fully closeable; rent returned to creator | Not auto-discovered by wallets; requires derivation |
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Rent reclaim: Vault accounts are closed on completion and cancellation, returning rent-exempt SOL to the creator. This aligns with the protocol’s fee transparency promise — founders know exactly what they pay and get refunds when streams end.
- Deterministic address: The vault address is
findProgramAddress(["vault", streamPDA]), which is trivially derivable from the stream PDA. - Clean on-chain state: No zombie token accounts accumulating over time.
Negative
Section titled “Negative”- Not wallet-discoverable: Wallets and explorers don’t automatically discover custom PDA token accounts. Users must derive the vault address from the stream PDA. The SDK provides
getVaultPda()for this. - Extra account in instruction lists: Every instruction that interacts with the vault needs the
vaultaccount explicitly passed. With an ATA, some frameworks could derive it automatically.