Writing Cardano Smart Contracts with Aiken
- The execution model Aiken targets
- Everything compiles to UPLC
- The toolchain
- Anatomy of a validator
- The standard library
- Testing without an external framework
- From source to a deployable script
- Security considerations specific to eUTXO
- Conclusion
- Annex — Key Terms
- Frequently Asked Questions
- References
Aiken is a programming language and toolchain for writing smart contracts on Cardano. It targets the same on-chain bytecode as every other Cardano contract language, but it was designed from scratch for that single job, and it ships the compiler, formatter, test runner, and language server in one binary. This article explains how Aiken fits into Cardano’s execution model, how a validator is structured, how the toolchain takes source code to a deployable artifact, and where the common mistakes are. Code paths were checked against Aiken v1.1.23 and the hello_world and gift_card examples in the aiken-lang/aiken repository.
This article has been made with the help of Claude Code and several custom skills
[TOC]
The execution model Aiken targets
Before any syntax, one fact governs everything: Cardano is not account-based. It uses the Extended UTXO (eUTXO) model, and a smart contract there is a validator, not an autonomous actor.
A validator cannot send funds, call another contract imperatively, read external state, generate randomness, or loop without bound. It is a pure function that receives a proposed transaction and answers one question: may this transaction proceed? Its signature is conceptually
validator(datum, redeemer, context) -> Bool
- Datum: state data attached to the UTXO being unlocked.
- Redeemer: an argument the spender supplies to select an action or provide a witness.
- Context: the entire transaction under evaluation, including its inputs, outputs, minted value, signatories, and validity interval.
Because the validator sees the whole transaction and produces no side effects, its outcome is deterministic. The wallet or backend can evaluate a script off-chain, learn the exact execution cost, and know whether the node will accept the transaction before it is submitted. The developer-portal guide phrases the split as the off-chain code being the lawyer who drafts an agreement and the on-chain validator being the judge who only checks compliance. The creative work of assembling a valid transaction happens off-chain; the validator rejects anything that violates its rules.
The diagram below traces a spend from transaction construction to the node’s phase-2 script evaluation.

This model is the reason Aiken exists in the form it does. The language does not need imperative I/O, concurrency primitives, or unbounded recursion, and leaving them out keeps compiled scripts small and their costs predictable.
Everything compiles to UPLC
Every Cardano smart-contract language compiles to a single target: UPLC (Untyped Plutus Lambda Calculus), the bytecode the node’s Plutus evaluator executes. The on-chain semantics are fixed by UPLC and the ledger rules, so the choice of source language is a question of ergonomics, not capability.
| Language | Suited to | Notes |
|---|---|---|
| Aiken | Most new projects | Purpose-built, small and fast output, built-in tests, emits a CIP-57 blueprint on build. |
| Plinth / Plutus Tx (Haskell) | Haskell teams | The original framework; full Haskell type system and on/off-chain code sharing, at the cost of a steep learning curve and larger scripts. |
| Plutarch | Maximum performance | Fine-grained control close to hand-written UPLC; hardest to use. |
| OpShin (Python) | Python teams | A typed subset of Python; pairs with PyCardano off-chain. |
| Scalus (Scala 3) | JVM teams | On-chain and off-chain in Scala. |
| Marlowe | Financial contracts | A domain-specific language, deliberately not Turing-complete, so termination is guaranteed. |
Aiken took syntax cues from Rust, Elm, and Gleam. It is on-chain only by design: off-chain code stays in whatever language the application already uses, which keeps the two halves of a contract cleanly separated. For a team with no prior commitment to the Haskell ecosystem, it is the shortest path to a correct, small validator.

The toolchain
Aiken is distributed as a single executable that bundles the compiler, formatter, test runner, documentation generator, package manager, and language server. The recommended way to install it is aikup, the official version manager:
npm install -g @aiken-lang/aikup # or: brew install aiken-lang/tap/aikup
aikup # installs the latest release
aiken --version # verify
Alternatives are cargo install aiken, the Nix flake in the repository, or the browser playground at play.aiken-lang.org for experiments that need no local setup.
The command surface, taken from the CLI source (crates/aiken/src/cmd/mod.rs):
| Command | Purpose |
|---|---|
aiken new <owner/repo> |
Scaffold a project. The name must use the owner/repo form. |
aiken build (alias b) |
Type-check and compile every validator to plutus.json. |
aiken check (alias c) |
Type-check and run tests. -m <pattern> filters by test name. |
aiken fmt |
Format source files. |
aiken add <owner/repo> |
Add a dependency. |
aiken docs |
Generate HTML API documentation from doc comments. |
aiken bench |
Run benchmarks. |
aiken blueprint <sub> |
Operate on plutus.json: apply, address, policy, hash, convert. |
aiken packages <sub> |
add, upgrade, clear-cache. |
aiken tx simulate |
Evaluate a transaction’s scripts and report execution units. |
aiken uplc <sub> |
Low-level UPLC tooling: eval, encode, decode, fmt, shrink. |
aiken new produces a fixed layout:
my-project/
├── aiken.toml # manifest: name, version, plutusVersion, dependencies
├── env/ # per-network environment modules (selected with -e)
├── lib/ # reusable non-validator modules
└── validators/ # validator modules, one entry each in plutus.json
└── placeholder.ak
The manifest declares the Plutus version and dependencies. New projects should target Plutus V3:
name = "aiken-lang/hello_world"
version = "1.0.0"
plutusVersion = "v3"
description = "Aiken contracts for project 'aiken-lang/hello_world'"
[[dependencies]]
name = "aiken-lang/stdlib"
version = "v2"
source = "github"
Anatomy of a validator
A file in validators/ defines one or more handlers, one per script purpose. Under Plutus V3 the purposes are spend, mint, withdraw, publish, vote, and propose, plus an else fallback that catches any purpose the validator does not handle explicitly. This set is fixed by the compiler (crates/aiken-lang/src/ast.rs).
The hello_world example is a complete spend validator:
use aiken/collection/list
use aiken/crypto.{VerificationKeyHash}
use cardano/transaction.{OutputReference, Transaction}
pub type Datum {
owner: VerificationKeyHash,
}
pub type Redeemer {
msg: ByteArray,
}
validator hello_world {
spend(
datum: Option<Datum>,
redeemer: Redeemer,
_own_ref: OutputReference,
self: Transaction,
) {
let must_say_hello = redeemer.msg == "Hello, World!"
expect Some(Datum { owner }) = datum
let must_be_signed = list.has(self.extra_signatories, owner)
must_say_hello && must_be_signed
}
else(_) {
fail
}
}
Three properties of this handler generalise to every validator:
- It returns a
Bool.Trueapproves the transaction;False, anexpectthat does not match, orfailrejects it. - It never acts; it constrains. Every rule is a predicate over
self, the transaction. Here it readsself.extra_signatories. Other validators inspectself.mint, the inputs and outputs, orself.validity_range. - The datum is optional. In Plutus V3 an output at a script address may carry no datum, so
datumhas typeOption<...>and theNonecase must be handled. Theexpect Some(...) = datumline rejects the spend if the datum is absent or malformed.
Parameterized validators and minting policies
A validator can take compile-time parameters, written after its name. Applying different parameters yields a different compiled script, and therefore a different script address or minting policy ID. This is how one source file produces many independent on-chain instances.
The gift_card example uses parameters to build a one-shot minting policy: a token that can be minted exactly once. It is parameterized on a token name and a specific UTXO reference, and the mint handler only approves minting when that exact UTXO is spent in the same transaction. Since a UTXO can be spent only once, the mint can happen only once.
validator gift_card(token_name: ByteArray, utxo_ref: OutputReference) {
mint(rdmr: Action, policy_id: PolicyId, self: Transaction) {
let Transaction { inputs, mint, .. } = self
expect [Pair(asset_name, amount)] =
mint |> assets.tokens(policy_id) |> dict.to_pairs()
when rdmr is {
Mint -> {
expect Some(_) =
list.find(inputs, fn(i) { i.output_reference == utxo_ref })
amount == 1 && asset_name == token_name
}
Burn -> amount == -1 && asset_name == token_name
}
}
else(_) { fail }
}
Two checks in the Mint branch matter for correctness and are easy to get wrong: the handler pins both the exact asset_name and the exact amount. Verifying quantity without pinning the asset name is a documented eUTXO pitfall (the other-token-name class), since a policy that checks only quantity can be satisfied by minting a different name under the same policy.
Parameters are supplied at deployment time with aiken blueprint apply, which rewrites the generic blueprint into a concrete, applied script.
The standard library
aiken-lang/stdlib provides the types and functions a validator reaches for. It is added with aiken add aiken-lang/stdlib and the current major line is v2. The modules seen most often:
aiken/collection/list,aiken/collection/dict,aiken/collection/pairsfor collections.aiken/cryptoforVerificationKeyHash, hashes, andblake2b_256.aiken/primitive/bytearray,aiken/primitive/int,aiken/primitive/string.cardano/transactionforTransaction,OutputReference,Input, andOutput.cardano/assetsforValue,PolicyId, and helpers such astokensandquantity_of.cardano/addressforAddressandCredential.cardano/certificateandcardano/governancefor thepublish,vote, andproposehandlers.
Testing without an external framework
Aiken has a test runner built into the compiler, so a project needs no separate test dependency. A test is a function marked with the test keyword that returns a Bool:
test or_else_2() {
or_else(Some(42), 14) == 42
}
aiken check runs every test and reports the CPU and memory execution units each one consumed, so tests double as cost measurements against the same budget the ledger enforces. aiken check -m or_else runs only tests whose name matches the pattern.
The runner supports three further modes:
- Property-based tests. A
testthat takes an argument becomes a property test. Aiken generates sampled inputs through aFuzzer, and on failure it shrinks the input toward a minimal counterexample. - Failure expectations.
test foo() fail { ... }asserts that the body errors, which is how you check that a validator rejects an invalid transaction. - Benchmarks. Functions measured by
aiken benchtrack cost across sampled inputs.
To exercise a full validator, a test constructs a mock Transaction, calls the handler directly, and asserts on the returned Bool. Because there is no network and no node in the loop, these tests are fast and deterministic.
From source to a deployable script
aiken build produces plutus.json, a CIP-57 Plutus blueprint. The blueprint is the contract’s interface description, analogous to an ABI: it lists every validator, the schemas of its datum, redeemer, and parameters, and the compiled UPLC. Off-chain code consumes the blueprint rather than the Aiken source, and tooling generates TypeScript, Python, or Rust types from it.
The deployment flow reads and rewrites that blueprint:
aiken build # -> plutus.json
aiken blueprint apply -v <name> <PARAM> # bake in a compile-time parameter
aiken blueprint policy -v <name> # minting policy id (for a mint validator)
aiken blueprint address -v <name> # bech32 script address (for a spend validator)
aiken blueprint hash -v <name> # script hash

From there the off-chain application takes over: it reads plutus.json, derives the addresses and policy IDs, and builds transactions the validator will approve. The hello_world example does this in plain TypeScript (lock.ts and unlock.ts); the gift_card example wires the same blueprint into a full SvelteKit application. Common off-chain libraries are Lucid Evolution, Mesh, cardano-serialization-lib, Blaze, and PyCardano, with any CIP-30 browser wallet on the front end.
Security considerations specific to eUTXO
Aiken removes whole classes of bugs by construction (no reentrancy, no unbounded loops, deterministic costs), but eUTXO has its own catalogue of mistakes that the compiler cannot catch. A validator only enforces what it explicitly checks, and forgetting a check is the usual failure mode. The recurring classes:
- Double satisfaction: one output is counted as satisfying two independent validators in the same transaction.
- Arbitrary or unbounded datum: trusting an attacker-supplied datum, or one large enough to make the output expensive to spend.
- Missing UTXO authentication: trusting that a UTXO holds particular state without an identifying token to prove it.
- Other token name: a minting policy that checks the minted quantity but not the exact asset name.
- Insufficient staking control: validating the payment part of an address while ignoring the staking part.
These are cross-language properties of the model, not of Aiken specifically. A validator should be reviewed against them regardless of the source language, and the review belongs before any mainnet deployment.
Conclusion
Aiken is a focused tool: an on-chain language plus a single-binary toolchain for Cardano validators. It compiles to the same UPLC as every other Cardano language, so it changes the developer experience rather than the on-chain semantics, and its output is compact with predictable costs. A validator is a pure predicate over a transaction, structured as one handler per Plutus V3 purpose, and compile-time parameters turn one source file into many independent on-chain instances such as one-shot minting policies. The build produces a CIP-57 blueprint that off-chain code consumes directly.
Two limits are worth restating. The compiler removes reentrancy, unbounded loops, and non-determinism by construction, but it cannot supply a check the author forgot, so an eUTXO security review remains a separate step. And the language is on-chain only by design; transaction construction stays in the off-chain library of your choice. The mindmap below summarizes the pieces covered here.

Annex — Key Terms
| Term | Definition |
|---|---|
| Aiken | a purpose-built, on-chain-only language and single-binary toolchain for writing Cardano validators, with syntax drawn from Rust, Elm, and Gleam. |
| Validator | a pure on-chain function that receives a proposed transaction and returns True (approve) or False (reject); it constrains transactions rather than performing actions. |
| eUTXO (Extended UTXO) | Cardano’s ledger model, in which value is held in immutable unspent outputs and a contract is a validator guarding a UTXO, rather than an account with a mutable balance. |
| UPLC (Untyped Plutus Lambda Calculus) | the single on-chain bytecode every Cardano node executes; every source language compiles to it, so language choice is about ergonomics, not capability. |
| Datum | state data attached to a UTXO. In Plutus V3 a spend handler receives it as an Option, since an output at a script address need not carry a datum. |
| Redeemer | the typed argument the spender supplies when unlocking a script-controlled UTXO, used by the validator to select an action or provide a witness. |
| Script context | the view of the entire transaction (inputs, outputs, mint, signatories, validity range) that the ledger passes to the validator. |
| Validator handler | one entry point per script purpose. Under Plutus V3 the purposes are spend, mint, withdraw, publish, vote, and propose, plus an else fallback. |
CIP-57 blueprint (plutus.json) |
the machine-readable interface description Aiken emits on build, listing each validator, its datum/redeemer/parameter schemas, and the compiled code; off-chain code consumes it in place of the source. |
| Execution units (ex-units) | the CPU and memory budget a script consumes, bounded per transaction by the ledger; aiken check reports ex-units so tests double as cost measurements. |
Frequently Asked Questions
Q: Why is a Cardano smart contract described as a validator rather than a program that executes actions?
Because in the eUTXO model the ledger, not the contract, moves value. A contract is a pure predicate that receives a fully-formed transaction and returns True or False. It cannot initiate a transfer, call another contract imperatively, or read outside state. Everything it can influence, it influences by rejecting transactions that fail its checks. This is what makes execution deterministic: the same transaction always produces the same verdict and the same cost, which can be computed off-chain before submission.
Q: If every language compiles to UPLC, what does choosing Aiken actually change?
Nothing about on-chain semantics, since UPLC and the ledger rules fix those. The choice affects developer ergonomics and the properties of the compiled output. Aiken was built only for writing validators, so it has a small learning curve, produces compact and efficient scripts, ships a built-in test runner, and generates a CIP-57 blueprint automatically. A language such as Plinth gives the full Haskell type system and shared on/off-chain types instead, at the cost of larger scripts and a steeper ramp. The tradeoff is tooling and output size, not capability.
Q: What are the arguments a spend handler receives, and why is the datum wrapped in Option?
A spend handler receives the datum attached to the UTXO being unlocked, the redeemer supplied by the spender, a reference to the specific input being validated (OutputReference), and the whole transaction as context. The datum is an Option because, under Plutus V3, an output at a script address is not required to carry a datum. The handler must therefore account for its absence, typically with expect Some(...) = datum, which rejects the spend when no valid datum is present instead of assuming one exists.
Q: How does a one-shot minting policy guarantee that a token is minted only once?
By parameterizing the validator on a specific UTXO reference and requiring, in the Mint branch, that this exact UTXO be spent in the same transaction. A UTXO can be consumed only once across the entire ledger, so once it is spent the minting condition can never be satisfied again. The gift_card example pairs this with checks that pin the exact asset name and a minted amount of 1, so the policy authorizes precisely one token of one name and nothing else.
Q: How do Aiken’s tests relate to the execution cost a contract pays on-chain?
The test runner evaluates each test against the same execution-budget model the ledger uses and reports the CPU and memory units consumed. A test is therefore also a cost measurement: a change that pushes a validator’s units up shows immediately in aiken check output. This matters because Cardano bounds script execution units per transaction, so a validator that grows too expensive will fail on-chain even if it is logically correct. Property tests extend this by sampling many inputs and shrinking any failure to a minimal counterexample.
Q: A validator passed all its unit tests. Why is a separate eUTXO security review still necessary?
Unit tests confirm the checks a validator does perform behave as intended. They cannot reveal a check the author never wrote. Most eUTXO vulnerabilities are omissions: a minting policy that verifies quantity but forgets the asset name, a spend that trusts a UTXO’s state without an authenticating token, or an output double-counted across two validators. These follow from the model rather than from Aiken, and the compiler cannot flag them. A review against the known eUTXO vulnerability classes checks for the constraints that should exist and do not, which testing of existing logic will not surface.
References
Aiken language and toolchain
- Aiken language site and documentation
- aiken-lang/aiken compiler repository
- aiken-lang/stdlib API documentation
- Aiken installation instructions
- Aiken online playground
- Awesome Aiken
Cardano model and standards
- Cardano developer portal — smart contracts
- CIP-57 — Plutus Contract Blueprint
- CIP-30 — Cardano dApp-Wallet Web Bridge
- UPLC-CAPE — cross-compiler UPLC benchmarks