Agent instructions
Rules for AI coding agents working in an Elci project. Save this page as AGENTS.md, CLAUDE.md or
.cursor/rules/elci.mdc at the project root. For depth, read /ai/starter-pack.md and /llms.txt on
the Elci documentation site before writing code.
What Elci is
Section titled “What Elci is”Elci compiles an Elm 0.19 syntax subset (.elm files) to Plutus V3 validators for Cardano. It is not
browser Elm: there is no Html, Cmd, port, elm.json, Elm package, Float, Char or Set. Elm’s
core modules Basics, List, Maybe, Result, Tuple, String and Dict exist with Elm’s names and
argument orders, but String has only a few functions. A Value module handles the ledger’s multi-asset
values, and the Tx and Interval modules answer questions about the transaction and its validity range.
Evaluation is strict.
Must-follow rules
Section titled “Must-follow rules”- Give every top-level declaration a type signature. Module
A.Blives atsrc/A/B.elm. - Elm’s default imports apply:
Basics exposing (..),List,Maybe exposing (Maybe(..)),Result exposing (Result(..)),String,Tuple,Debug. ImportDict exposing (Dict),Map,Array,Value exposing (Value),Interval,Tx,BytesandJulc.Data as Datawhen you use them.Julc.Ledgerprovides ledger types; expose constructors withType(..). Never name a module or aliasBasics,List,Maybe,Result,String,Tuple,Map,Dict,Array,Set,Value,Interval,TxorElci, a constructorJust,Nothing,TrueorFalse, or a typeInt,Bool,String,Bytes,Data,List,Maybe,Dict,Map,ArrayorFloat. - Use only the core functions that exist (see
/ai/catalog.json). Prelude:min,max,abs,negate,clamp,compare(LT/EQ/GT),identity,always,not,xor,modBy,remainderBy,^.Maybe:withDefault,map,map2,map3,andThen.Result:map,map2,map3,andThen,withDefault,toMaybe,fromMaybe,mapError.String: onlyfromInt,isEmpty,append,concat,join.Listhas all of Elm’s exceptmap4/map5, andDictall of Elm’s. Replacements:Set→Dict k Bool;Debug.log→Debug.trace;String.length s→Bytes.length (Bytes.fromString s). - No
Float,/,%,number, char literals or extensible records. UseInt,//,modByandremainderBy. Operators work as functions:(+),(::). Accessor functions work as in Elm:List.map .amount payouts. Write qualified names without spaces. Tuples have 2 or 3 elements. <,>,<=,>=,compare,maxandList.sortwork on comparable types:Int,String,Bytes, ledger hashes such asPubKeyHash, and lists and tuples of these. Sort records withList.sortBy. A signature variable used with<must be namedcomparable, with++appendable.==does not work on functions,Map,ArrayorTxOut; it works onDict,ValueandData(exact structure:Tx.inlineDatum out == Just (Data.encode expected)). Arguments are strict:Maybe.withDefaultevaluates its default.- Negation is Elm’s:
f -1passes-1; writex - 1to subtract. - Configure validators in
elci.toml, never in source:[validators.<name>]withboundary = "julc-strict-v1", handler keysspend/mint/withdraw/certify/vote/propose="Module.function", optionalparameters = [...]andspend-datum = "required" | "optional" | "absent". Keeptarget = "plutus-v3-pv11-uplc-1.1.0"and thejulc-versionthe compiler prints. - Handler types: parameters first, then the datum (spend only;
Maybe datumwhen optional), the redeemer, andScriptContext, returningBool. Expose every handler. Use()for unused values. - Datum, redeemer and parameter types must be Data-encodable: records, custom types,
Int,Bool,String,Bytes,(), tuples, lists,Dict,Map,Maybe,Resultand ledger types such asPubKeyHashandTxOutRef. Never functions,Array,Value,TxOutorScriptContext; for an amount of tokens storeList ( PolicyId, TokenName, Int )and callValue.fromList. - Records encode with fields sorted by name; custom types by declaration order;
MaybeisNothing0,Just1;ResultisOk0,Err1 (the reverse of PlutusTxEither). Julc optionals differ: convert withData.fromOptional/Data.toOptional. ADictis a Plutus map with strictly ascending keys: unsorted or repeated keys fail to decode, so build test data withData.encode (Dict.fromList ...)and giveelci applysorted keys. - Use
Dictfor data you build or look up by a comparable key, including datums. UseMapfor ledger maps (txInfo.withdrawals,txInfo.redeemers) and keys that are not comparable. Both look up in O(n);Map.fromListis O(n²),Dict.fromListO(n log n). - Keep
PubKeyHashandBytesapart; use.hashwhen raw bytes are needed, includingtoken.hashfor aTokenName(it has no.name). Read the transaction with the coreTxmodule:Tx.signedBy key ctx,Tx.ownRef,Tx.ownInput,Tx.ownPolicy,Tx.outputsTo,Tx.continuingOutputs,Tx.mintedOf,Tx.inlineDatum,Tx.datumOf. Don’t hand-write these walks. - Every
letvalue is evaluated even if unused. Put cheap checks first, inline, in&&chains. Keep expensive work out of top-level values; they run for every purpose of the script. - Check deadlines with the core
Intervalmodule:Interval.isEntirelyAfter deadline ctx.txInfo.validRangeandInterval.isEntirelyBefore deadline ctx.txInfo.validRange. Both are strict and need a finite bound, so a transaction without a validity range fails them.Scenario.validFromis inclusive andScenario.validBeforeexclusive. Don’t use Julc’sIntervalLib, whose bounds use a-1sentinel. - Read and build ledger values (
out.value,ctx.txInfo.mint) with the coreValuemodule:Value.lovelaceOf v,Value.quantityOf policy name v,Value.geq paid required,==(exact). Make asset ids withPolicyId bytesandTokenName bytesfromJulc.Ledger exposing (PolicyId(..), TokenName(..)). A mint holds burns as negative quantities; split it withValue.positiveandValue.negative. Never read values throughJulc.Values(ValuesLib): Java exports that return aValueare rejected (ELCI2105), except the ledger accessors ofContextsLibandOutputLib. - Java libraries need a
[java-modules]mapping, explicitexposinglists, and take the subject first (Output.outputsAt outputs address). Never pass a type containing aDictor aValueto a generic Java function (ELCI3003). - Annotate a
Data.decoderesult when inference cannot see its type (ELCI3001). - Tests live in
tests/. Only test modules may importExpect,ScenarioandProperty. A test is an exposed zero-argument value of typeBool,Expectation,ScenarioorProperty; an exposed function returningPropertyis a property test. Keep helpers unexposed. - Every scenario passes all validator parameters with
Scenario.parameters [ Data.encode x, ... ]. Cover the success path, every failure path (Scenario.expectFailure) and at least one property that generates offsets around each boundary. Pay a key withScenario.toKey key lovelace; attach an inline datum withScenario.withDatumor a datum by hash withScenario.withDatumHash. - Do not weaken tests or add
expectFailureto make a failing test pass. Fix the validator.
Security rules for validators
Section titled “Security rules for validators”The escrow, marketplace and order-swap example projects follow the rules for spend validators and test each attack.
- Tag every payment a spend validator requires: the paying output carries the spent output’s reference,
Tx.inlineDatum output == Just (Data.encode ref)withreffromTx.ownRef ctx. An untaggedTx.valuePaidTocheck lets one output pay for two script inputs (double satisfaction). - When outputs back to the script are matched by address (
Tx.continuingOutputs), allow one input of the script per transaction. - Check the whole value that stays at the script with
Value.geq, not one token, so the spender can’t keep the lovelace or other tokens. To stop padding with junk tokens too, compareValue.withoutLovelaceof both values with==and their lovelace with>=. - Use
Value.geqor>=for “pays at least”;==on values is exact. - Require
Tx.signedByfor every privileged action, and test it with a stranger’s signature. - Bound numbers from datums and parameters before using them: a fee in basis points between 0 and 10000, quantities above 0. Parameters are not validated on-chain.
Tx.outputsTomatches the payment credential; whoever builds the transaction chooses the payee’s staking part. Store and compare the fulloutput.addresswhen the staking part matters.- A minting policy checks the whole mint under its policy:
Dict.toList (Tx.mintedOf policy ctx) == [ ( TokenName name, quantity ) ].
Workflow
Section titled “Workflow”Run these from the project directory after every change and fix every error before moving on:
elci check # compile; prints FLAT size per validatorelci test --seed 42 # run tests; reproduce with the printed seedelci test --match <name> # rerun one test; --fuzz 1000 for deeper property runselci build # write build/validators/<name>/ artifactsUse elci eval only in a function project ([entrypoint]) to evaluate helper logic. Apply
parameters with elci apply --validator <name> --param name='{"bytes": "..."}', which validates them
off-chain. Every command exits with status 0 on success and 1 on any error or failing test.
Reading diagnostics
Section titled “Reading diagnostics”A diagnostic’s first line is path:line:col: ELCIxxxx: message, and its form never changes. Below it come
the source line with a caret under the reported token, and zero or more Hint: lines:
src/Main.elm:8:5: ELCI2005: Unknown name 'double'
8| double 4 ^^^^^^Hint: write `Helper.double`, or expose `double` in its import- Fix the part the message names: the argument, branch, list element, operator side, annotation or field
whose type differs, or each pattern a
caseis missing. - Apply the hint when it fits: a close name (
did you mean), a conversion such asString.fromInt,Bytes.fromString,.hashorMaybe.withDefault, or an import to add. - A run reports every error it finds, in source order, up to 20, with a count such as
3 errors. Each top-level declaration reports its first error, and type errors appear only once syntax, import and qualified-name errors are fixed. Fix what is reported, then rerunelci check. elci explain <code>explains a code, such aselci explain ELCI2002, with a failing example and its fix. The last line of a failed run names the codes to look up.- To parse errors, add
--report=jsontocheck,build,evalortest. Each error is then one JSON object on its own line on stderr (JSON Lines), withcode,path,line,column,message,hints,excerptandcontext.
Look the code up in the starter pack’s error table: ELCI2002 type mismatch, ELCI2003 equality,
ordering or append on a type without it, ELCI2004 missing signature, ELCI2005 unknown name, ELCI2007
non-exhaustive case, ELCI2013 type cannot cross the data boundary, ELCI2014 handler signature does not
match elci.toml, ELCI2102 ambiguous name, ELCI2101/ELCI2104 import problems or a function Elci
lacks, ELCI2105 a Java export Elci can’t use, such as one that returns a Value.
Done means
Section titled “Done means”elci checkandelci testboth exit with status 0.- Every rule of the validator has a passing scenario, and every way to break it has an
expectFailurescenario or a property, including each attack in the security rules that applies. - elm-format 0.8.8
--validatepasses. elm-format corrupts integer literals beyond 64 bits, so re-check any large constant after formatting.