A time-locked vesting contract with a clean web UI. Owner deposits ADA for a beneficiary; beneficiary can claim after the deadline; owner can reclaim anytime. Demonstrates Aiken validator design, inline datums, validity intervals, collateral handling, and reference scripts as a complete end-to-end pattern.
Why this DApp matters
Most “smart contract tutorials” stop after aiken build. This DApp continues from there: how does the validator integrate with off-chain code, how does the frontend present the vest, how do you deploy a reference script to save fees, how do you handle the unlock UX when the user doesn’t know exactly when the deadline lands?
It’s the simplest non-trivial contract that touches every layer:
- A non-trivial validator (two paths: owner-cancel, beneficiary-claim-after-deadline)
- Inline datums carrying typed state
- Validity intervals as a time-checking mechanism
- Collateral handling
- Reference scripts as a fee optimisation
- A frontend with two distinct flows (owner setup vs. beneficiary claim)
What you’ll see when you run it
As an owner:
- Connect wallet → click “Create vest”
- Form: amount in ADA, beneficiary address, unlock date/time
- Sign → ADA locks at the script address
- Dashboard shows your active vests with a “Cancel” option (works anytime)
As a beneficiary:
- Connect wallet → app queries for vests where you’re the beneficiary
- Each vest shows: amount, deadline, status (locked / claimable / claimed)
- Once deadline passes → “Claim” button activates
- Sign → ADA arrives in your wallet
Architecture
On-chain (Aiken): validators/vesting.ak — about 30 lines. The validator:
pub type VestingDatum {
lock_until: Int,
owner: VerificationKeyHash,
beneficiary: VerificationKeyHash,
}
validator vesting {
spend(datum_opt, _redeemer, _, tx) {
expect Some(datum) = datum_opt
or {
key_signed(tx.extra_signatories, datum.owner),
and {
key_signed(tx.extra_signatories, datum.beneficiary),
valid_after(tx.validity_range, datum.lock_until),
},
}
}
}Off-chain (Mesh): Three scripts:
lock.ts— builds the locking transaction (sends ADA to script address with inline datum)unlock.ts— builds the unlocking transaction (with validity interval and collateral)deploy-reference.ts— one-time deployment of the reference script
Reference script registry: script-registry.json tracks where the reference script UTxO lives. Used by unlock.ts to avoid inlining the script bytes (saves ~70% on unlock fees).
Frontend: Next.js 14 with two main routes:
/owner— vest creation + cancellation dashboard/beneficiary— claim dashboard (queries Blockfrost for UTxOs at the script address)
File layout
dapps/vesting-vault/
├── on-chain/
│ ├── validators/vesting.ak
│ ├── aiken.toml
│ └── plutus.json (generated by aiken build)
├── off-chain/
│ ├── lock.ts
│ ├── unlock.ts
│ ├── deploy-reference.ts
│ └── script-registry.json
├── frontend/
│ ├── app/
│ │ ├── owner/page.tsx
│ │ ├── beneficiary/page.tsx
│ │ └── api/vests/route.ts # Lists vests at script addr by beneficiary
│ ├── components/
│ │ ├── VestForm.tsx
│ │ ├── VestCard.tsx
│ │ └── ClaimButton.tsx
│ └── lib/
│ ├── lock.ts (uses off-chain/lock.ts)
│ └── unlock.ts (uses off-chain/unlock.ts)
└── README.mdRunning it locally
git clone https://github.com/pbwebdev/LearnCardano.git
cd LearnCardano/dapps/vesting-vault
# Build the validator
cd on-chain
aiken build
cd ..
# Deploy the reference script (one-time)
cd off-chain
npm install
node deploy-reference.ts
# Note the txHash + index that gets logged — saved automatically to script-registry.json
# Run the frontend
cd ../frontend
npm install
cp .env.example .env.local # add Blockfrost key
npm run devWallet must be on preview testnet with some preview ADA.
What this teaches (recipe by recipe)
vesting-validator-aiken-mesh— writing the validator and basic lock/unlock flowaiken-unit-tests— testing the validator withaiken check(the repo includes the test suite from this recipe)reference-scripts-aiken-mesh— deploying and using the reference script
Pitfalls baked into the design
The DApp is structured to demonstrate three common pitfalls and their fixes:
validity-interval-not-set— the unlock flow always sets a validity interval; without it, the time check failscollateral-not-set— the unlock flow always includes collateral; the wallet is checked at connect timemin-ada-with-metadata— the lock flow trusts Mesh’s calculation rather than hard-coding lovelace amounts
Reading the unlock script will show you what “production-grade” looks like for these three issues.
Test transaction examples
Real preview-testnet transactions from this DApp are listed in the post sidebar. Good ones to inspect on Cardanoscan:
- The “Lock 5 ADA for 48h vest” transaction shows the inline datum structure
- The “Beneficiary claim after deadline” transaction shows the redeemer + validity interval + collateral
- The “Reference script deployment” transaction shows where the script bytes live
Production hardening (left as an exercise)
The repo version is a teaching artifact. To productionise:
- Multi-asset support (currently only ADA; native tokens locked alongside ADA are a small extension)
- Cliff vesting (one big release at deadline) → linear vesting (continuous release over a window)
- Multiple beneficiaries per vest (requires datum redesign)
- Owner withdrawal of “yield” — letting the locked ADA earn delegation rewards while locked (this is a real benefit of Cardano’s stake-while-locked model and worth a follow-up DApp)
- Audit before mainnet
Demo
No public demo URL yet — run locally for now. Production deployment planned alongside the Cardano Content Creator Consortium video series.