Amaru Treasury May 2026
This case study runs twelve SPARQL queries over a real Amaru Treasury
May 2026 on-chain lattice built end-to-end from the cq-rdf runtime
pipe (overlay + body + blueprint), the closure fetched by
transaction CBOR, and Apache Jena, and validates two invariants on
that lattice via SHACL shapes.
The dataset is the 101-transaction lattice rooted in the May 2026 operator batch: 30 seed transactions and 71 closure parents. The seed batch contains 3 disbursements, 5 reorganize transactions, 20 swap-order transactions, 1 swap cancel, and 1 scoop dive. The closure was assembled at depth 1 by fetching transaction CBOR for each consumed parent UTxO so cross-transaction joins have both sides of each input reference in the same graph.
Dataset and queries
Dataset assembly is documented in Dataset selection, with
the full transaction list in selections.txt, the
case-local operator overlay in overlay.yaml, and the
reproduce pipe in README.md. The SPARQL evidence is split into
one page per query: Q0 conservation,
Q1 monthly totals, Q2 USDM landing,
Q3 ADA scope flow, Q4 multisig shape,
Q5 vendor chain, Q6 disbursement detection,
Q7 USDM scope flow, Q8 scoop detection,
Q9 reference-script reuse, Q10 scoop-recipient resolution,
and Q11 self-swap validation.
Invariants
The operator ships SHACL shapes that turn the human-readable
invariants into machine-checked gates. cq-rdf shacl --shapes
shapes/ runs every shape against the typed lattice and exits
non-zero on any violation. Two shapes ship today; the directory
is meant to grow as new invariants are added.
| Shape | Invariant |
|---|---|
shapes/self-swap.shacl.ttl |
Every operator-issued Sundae OrderDatum routes back to network_compliance. Machine form of Q11. |
shapes/attested-disbursement.shacl.ttl |
Every treasury:OffChainEntity with treasury:paidVia has at least one treasury:Attestation linked. |
See shapes/README.md for the reproduce pipe.
Data flow
flowchart LR
subgraph blockfrost[Blockfrost / Koios]
kc["cq-rdf body --provider blockfrost<br/>(CBOR pulled per txid)"]
end
kc -->|body TTL| join
overlay[("overlay.yaml<br/>entities + attestations<br/>(imports: treasury)")] --> ov
ov["cq-rdf overlay<br/>(YAML → overlay TTL)"] -->|overlay TTL| join
bp[("blueprints/<br/>sundae-order-typed.cip57.json")] --> bpass
join["cat overlay.ttl bodies.ttl"] -->|TTL| bpass
bpass["cq-rdf blueprint<br/>(typed-decode pass)"] -->|package.ttl| lattice
shapes[("shapes/<br/>self-swap + attested-disbursement")] --> shacl
lattice --> shacl["cq-rdf shacl<br/>(invariant validation, exits 0 on conform)"]
subgraph lattice["101-tx typed lattice (package.ttl)"]
direction LR
seeds["30 seeds<br/>(May 2026 batch)"]
parents["71 parents<br/>(consumed UTxOs + reference/collateral parents)"]
parents -. cardano:fromTxOutRef .-> seeds
end
lattice --> jena
jena["Apache Jena<br/>SPARQL 1.1 engine"] -->|12 queries| results["real on-chain answers"]
Findings
The conservation query balances exactly: seed inputs equal seed
outputs plus fees, with a zero-lovelace gap. The 30 seed transactions
paid 19.931398 ADA in total fees. The most expensive single
transaction in the batch is the reorganize 71ff129b… at
1.572508 ADA — an 11-input reorganize at the network_compliance
scope. The contingency disburse 18d57a4f… is not the most
expensive seed; it pays 0.415814 ADA in fee (2 inputs). The
per-tx fee in this batch tracks input count and script execution
units, not multisig shape.
The May flow moves 205,000 ADA from contingency to network_compliance,
then spends network_compliance ADA into SundaeSwap order flow and receives
USDM back through scoops. The vendor bridge sends 418,750 USDM to
amaru.cag-payee, linked in the overlay to Antithesis and Castellum IPFS
attestations.
The lattice exposes two swap-order consumption events in the selected
batch: the 9-order scoop dive 4e2642080c8d171aad05baed11b076de498b76acecc1c2412660048fae8aefa3
and the one-order swap cancel a8bab7bfe1e2ed9d3a5b40189c8de51c5974a6e05c71fc1000a6abd57500b365.
Reference-script reuse is concentrated in four hot parent UTxOs, matching
the expected reference-input pattern for the treasury and swap scripts.
How to reproduce
The reproduce pipe is documented in README.md. In
summary, from this directory:
cq-rdf overlay --in overlay.yaml > overlay.ttl
xargs -P8 -n1 cq-rdf body --provider blockfrost < selections.txt > bodies.ttl
cat overlay.ttl bodies.ttl \
| cq-rdf blueprint --blueprints blueprints/ \
> package.ttl
cq-rdf shacl --shapes shapes/ < package.ttl
# Then save any query page's SPARQL block as qN.rq and run it:
arq --data package.ttl --query q0-conservation-check.rq
Substitute --provider koios (or any other supported provider) if
Blockfrost is not on hand. cq-rdf shacl --shapes shapes/ reads
package.ttl on stdin and exits non-zero on a violation, so a clean
exit is also a clean invariant check.
Limitations to be solved on our side
Each of these is a real gap in the present-day pipeline that limits SPARQL expressiveness; each has a known fix path.
1. Body emitter did not emit cardano:hasIndex on outputs
Impact: a tx output's index in its parent tx is needed for
the closure JOIN (?orderOut cardano:hasIndex ?ix) but the
prior body emitter encoded the index only in the bnode label
(_:output1, _:output2, …). Blank-node labels are not
semantic in RDF.
Resolved (#100, in Cardano.Tx.Graph.Emit.Project.emitOutput):
the body emitter now emits cardano:hasIndex (zero-based) on every
output as part of the canonical Turtle. The scripts/tx-lattice
post-processing block has been removed.
2. Body emitter did not emit the tx's own hash
Resolved (#100, in Cardano.Tx.Graph.Emit.Project.emitTxBlock):
the body emitter now hashes the Conway tx body via
Cardano.Ledger.Hashes.hashAnnotated and pins it as
<urn:cardano:tx:<HEX>> cardano:hasTxId <urn:cardano:id:TxId:<HEX>> —
using the same Identifier-typed IRI pattern as inputs' parent-txid
references, so SPARQL JOINs across the closure use
cardano:hasTxId/cardano:bytesHex uniformly.
3. CIP-57 blueprint binding rejects two scripts sharing a blueprint
Resolved (#101, in Cardano.Tx.Graph.Rules.Load.Resolve.Imports.dedupBlueprints):
the loader now distinguishes two cases when a predicate URI is
declared twice. If both registrations point at the same parsed
Blueprint value, both script-hash bindings are accepted — this
is the operator-intended "shared parameterised contract" pattern
(Amaru contingency vs network_compliance both spending the
treasury.treasury.spend contract). A true cross-blueprint
predicate-URI collision still fails fast with
DuplicateBlueprintPredicate. The presentation's
blueprints/ directory can now register the treasury blueprint
against both treasury scopes and surface the typed redeemer
decode on either side once gap #4 lands.
4. Typed redeemer decode not firing on live mainnet
Resolved (#112 — landed in the same PR as #103). The root
cause was that
Cardano.Tx.Graph.Emit.Witness.resolveRedeemerPurposeHash for
ConwaySpending derives the spending script hash by reading the
consumed input's TxOut from a ResolvedUTxO map. The earlier
lattice path never populated that map, so dispatch silently fell back
to NoBlueprintRegistered.
The current path composes one graph per CBOR via the cq-rdf
subcommand pipe:
cq-rdf bodyemits a single transaction graph with tx-scoped positional blank nodes and stable content-addressed identifiers.- The operator pipe (
cq-rdf overlay+xargs … cq-rdf body+cat) writes the operator overlay once and appends one body graph per transaction. Cross-transaction queries join through the emittedTxId,TxOutRef, address, asset, and credential identifiers rather than through a special merge command — for the seed batch and BFS-walked ancestors alike.
(The original #112 fix was an on-disk --closure-dir DIR
resolver that read parent CBORs from disk at emit time. #114
reduced that disk handshake into the pure-transformation
contract: the body emitter now sees the whole lattice as its
input, not as a side-channel directory.)
Verified on a 7-tx closure of contingency disburse
18d57a4f…: the seed's redeemerData bnodes now carry
:TreasurySpendRedeemer_amount _:redeemerData1_amount triples,
materialising the Reorganize / SweepTreasury / Fund / Disburse
constructor distinction the SPARQL queries can JOIN on.
5. Sundae V3 order — typed datum still opaque
Resolved (#103 — and reclassified). The script at hash
fa6a58bb… is SundaeSwap V3's order.spend validator, not
an Amaru contract (authoritatively named
sundaeOrderScriptHashMainnet in
/code/amaru-treasury-tx/lib/Amaru/Treasury/Constants.hs). The
upstream Aiken plutus.json now ships under
test/fixtures/tx-graph/blueprints/sundaeswap-v3/ pinned
at commit be33466b… of
github.com/SundaeSwap-finance/sundae-contracts (Apache-2.0).
What lands:
- Typed redeemer decode — Sundae V3's
OrderRedeemerisScoop | Cancel. Once registered against an entity namedsundae.swap.v3.order, every redeemer that spends a Sundae order UTxO emits a:OrderRedeemer_Scoopor:OrderRedeemer_Cancelpredicate. New SPARQL queries: "count scoops vs cancels in the month", "list every cancelled order".
What still doesn't land:
- Typed datum decode — Sundae's CIP-57 blueprint types the
swap-order datum as
Data(intentional, by their design). The 6-field on-chain shape stays opaque. Q10 keeps the scoop-join workaround for resolving the human recipient.
Update for #66 Phase 4: when the operator points cq-rdf
blueprint at the upstream Sundae plutus.json, the
documentation.spend validator's typed OrderDatum schema (with
the MultisigScript recursive subtree degraded to opaque
Data) decodes the datum's named fields — including
destination.address.payment_credential. The
shapes/self-swap.shacl.ttl shape
uses that typed path to enforce the self-swap invariant on every
operator-issued order. See
Q11 for the explanatory
SPARQL.
overlay.yaml in this case-study folder names the entity
sundae.swap.v3.order (the authoritative name) — the older
amaru.swap.v2 label has been retired throughout the
presentation. Q3's narrative "sundae.swap.v3.order + pools +
scoopers" row, Q8's prose, and Q10's mermaid all refer to this
same Sundae V3 order script.
6. tx-lattice is a shell prototype
Resolved (epic #66, Phase 5): the case study no longer ships an
orchestrator shell script. The runtime is reached through four
narrow pure subcommands — cq-rdf overlay, cq-rdf body,
cq-rdf blueprint, cq-rdf shacl — composed by a documented Unix
pipe in README.md. Parallel CBOR fetch is delegated
to xargs -P8, an operator-tunable knob rather than a hard-coded
loop. The "closure walk" pre-filter remains an operator concern;
operators stream txids in from any source (hand-edited file,
gh api, future cq-select service) without the runtime needing
to know.
7. cq-rdf overlay rejects overlay files carrying off-chain entities
Resolved (#105, across Cardano.Tx.Graph.Rules.Load.{Types,
Parse.Yaml, Emit.Overlay}): a single overlay file can now carry
on-chain entities, off-chain overlay vendors, and IPFS-anchored
attestations side by side. Concretely:
- An entity in the
entities:list with no on-chain identifier shape (nofrom-address/script/asset/pool/drep/keys+bytes) but withpaid-via:is accepted as an off-chain overlay node and emitted as:slug a treasury:OffChainEntity. - A new top-level
attestations:block declares IPFS-anchored artefacts; each entry emits a[] a treasury:Attestation ; rdfs:label "..." ; treasury:attests :slug ; treasury:ipfs <ipfs://...>block in the overlay. - New optional
role:andpaid-via:keys are accepted on any entity (on-chain or off-chain); they emittreasury:roleandtreasury:paidViatriples respectively.
The May 2026 presentation can now drop the overlay.ttl companion
file and ship a single overlay.yaml; Q5 (vendor-payment chain)
runs unchanged against the merged document.
8. Scope mapping is hard-coded inside each SPARQL query
Resolved (#100, in Cardano.Tx.Graph.Rules.Load.Emit.Overlay):
every entity declared via from-address: now emits a top-level
:slug cardano:bech32 "<addr>" triple. The per-scope queries
(Q3, Q5, Q7) can be rewritten to JOIN on
?entity rdfs:label ?scope . ?entity cardano:bech32 ?bech instead
of carrying hard-coded bech32 literals in VALUES blocks — a
follow-up to this issue will land that refactor.
9. Overlay blank-node inflation under per-tx --rules
Resolved (epic #66, Phases 2 and 5): the legacy tx-graph
--rules mode re-emitted the overlay (including blank-node
sub-structure for off-chain attestations and vendor metadata) once
per body call. Entity IRIs deduplicated as triples, but blank
nodes did not — Q5 and Q11 had to add SELECT DISTINCT to
compensate. The new pipe emits the overlay exactly once
(cq-rdf overlay --in overlay.yaml > overlay.ttl) and concatenates
it with one body graph per transaction (cq-rdf body). No
overlay blank node ever appears twice in the lattice, and the
SELECT DISTINCT workarounds in Q5 / Q11 are no longer load-bearing.