Knot Documentation

A practical guide to how Knots work, how they don't, and every way you can interact with the contracts.

Each purchase mints a sequential NFT (1, 2, 3, …), routes ADA on-chain according to fixed ratios, assigns the current prize-eligibility to the latest buyer, and extends a countdown. If the timer ever reaches zero, the last buyer can withdraw the prize.

What it creates

  • Fully transparent rules and state
  • Automatic on-chain buybacks during growth phases
  • Dual incentives: early exits + last buyer prize
  • Owned by the deployer. KnotCash takes no fee in perpetuity.

What it is not

  • No admins, pause, or backdoors
  • No off-chain data feeds, batchers, or custodians
  • No mutable parameters post-deploy
  • Not stoppable

Where to start

  • Browse active markets: /knots
  • Deploy your own: /deploy
  • Read the Aiken Validators: /validators
  • Read on for contract-level details

Each purchase updates shared state and returns a newly minted, numbered NFT to the buyer. The shared state thread token lives at the Oracle script and its datum includes the current count, the current winner, and value routing information used to enforce outputs match configuration.

On each purchase

  • Mint and send NFT number count to the buyer
  • Pay fixed portions to treasury, pot, and fee addresses
  • Update the Oracle datum: winner := buyer, count := count + 1
  • Extend the countdown by the configured slot increase.

If no new purchases occur until the timer elapses, the recorded winner can withdraw the pot.

Immutable at Deploy

  • Fixed ADA routing: treasury_price, pot_price, fee_price
  • Addresses: treasury_address, pot_address, fee_address
  • Timing: slot_start, slot_increase per purchase
  • Policy references for minting/burning the sequential NFTs

Oracle Datum (shared state)

  • count: number of NFTs minted so far
  • winner: payment key hash eligible for the pot
  • timer: derived from slot_start + count × slot_increase

1) Buy / Mint NFT

Spend the Oracle input and mint one new NFT with the next number. The transaction must route treasury_price, pot_price, and fee_price to their respective addresses and update the Oracle datum with the new count and winner.

2) Burn NFT (Early Exit)

Burn an owned NFT to withdraw a pro-rata share from the treasury according to the configured rules. The burn checks the NFT number against the current count and uses a divisor rule to constrain how many early exits can occur at a time. A reference to the Oracle datum is required.

3) Withdraw Pot (Last Buyer)

When the current slot is beyond slot_start + count × slot_increase, the recorded winner can withdraw the pot from the pot script. The transaction must prove the winner's signature.

4) Secondary Market

List and sell NFTs through the marketplace script. On Buy, the seller receives the agreed price and the marketplace owner receives the configured fee. Sellers can cancel their own listings.

Oracle

  • Holds the shared state (count, winner)
  • Purchase path: updates datum; enforces payments and NFT mint
oracle.ak
// 🖲️ store and spend the state thread game logic
use cardano/transaction.{Input,
  Output, OutputReference, Transaction, find_input}
use cocktail.{inputs_at_with_policy, outputs_at_with_policy}
use utils/datum_oracle.{OracleDatum}
use utils/find_policy_id_of_first_token.{find_policy_id_of_first_token}
use utils/redeemer_oracle.{MintDsNFT, OracleRedeemer}
use utils/utils_oracle.{is_datum_updated, is_output_value_clean, is_value_paid}
 
pub fn oracle_spend(
  datum_opt: Option<OracleDatum>,
  redeemer: OracleRedeemer,
  input: OutputReference,
  tx: Transaction,
) {
  // find the input being spent
  expect Some(own_input) = find_input(tx.inputs, input)
  // find the NFT from its own input
  let oracle_nft_policy = find_policy_id_of_first_token(own_input)
  // find the address of the input being spent
  let own_address = own_input.output.address
  // destructure input datum
  expect Some(input_datum) = datum_opt
  when
    (
      redeemer,
      inputs_at_with_policy(tx.inputs, own_address, oracle_nft_policy),
      outputs_at_with_policy(tx.outputs, own_address, oracle_nft_policy),
    )
  is {
    (
      // when the redeemer includes a new_winner
      MintDsNFT { winner: new_winner },
      // when 1 input with oracle nft is present
      [_],
      // when 1 output with oracle nft is present
      [only_output],
    ) ->
      and {
        is_output_value_clean(only_output),
        is_datum_updated(input_datum, only_output, new_winner),
        is_value_paid(tx, input_datum),
      }
    _ -> False
  }
}
 
validator oracle {
  spend(
    datum_opt: Option<OracleDatum>,
    redeemer: OracleRedeemer,
    input: OutputReference,
    tx: Transaction,
  ) {
    oracle_spend(datum_opt, redeemer, input, tx)
  }
 
  else(_) {
    fail
  }
}
 

DS NFT

  • Sequential NFT mint policy and burn policy
  • Burn path ties into treasury withdrawal constraints
ds_nft.ak
// 🫀 mint and burn ds_nft (ds is a nod to the original contract I called Double Spent)
use aiken/primitive/bytearray.{from_string}
use cardano/assets.{PolicyId}
use cardano/transaction.{InlineDatum, Input, Transaction}
use cocktail.{
  check_policy_only_burn, convert_int_to_bytes, inputs_with, inputs_with_policy,
  only_minted_token,
}
use utils/datum_oracle.{OracleDatum}
use utils/utils_ds_nft.{MintPolarity, RBurn, RMint, check_game_mode}
use utils/config.{config_receipt_name_string}
 
pub fn ds_nft_mint(
  oracle_nft: PolicyId,
  redeemer: MintPolarity,
  policy_id: PolicyId,
  tx: Transaction,
) {
  when redeemer is {
    RMint ->
      when inputs_with_policy(tx.inputs, oracle_nft) is {
        // when 1 oracle input is present
        [oracle_input] ->
          // There are 2 nfts from this policy so we make sure the receipt is not present, and infer the real oracle nft is being spent
          when
            inputs_with(
              [oracle_input],
              oracle_nft,
              from_string(config_receipt_name_string),
            )
          is {
            [_] -> False
            _ -> {
              // Oracle NFT is being spent
              // with inline datum
              expect InlineDatum(input_datum) = oracle_input.output.datum
              expect OracleDatum { count, mode, slot_start, slot_increase, .. } =
                input_datum
              and {
                only_minted_token(
                  tx.mint,
                  policy_id,
                  convert_int_to_bytes(count),
                  1,
                ),
                check_game_mode(tx, slot_start, slot_increase, count, mode),
              }
            }
          }
        _ -> False
      }
    // all minting values are negative 
    RBurn -> check_policy_only_burn(tx.mint, policy_id)
  }
}
 
validator ds_nft(oracle_nft: PolicyId) {
  mint(redeemer: MintPolarity, policy_id: PolicyId, tx: Transaction) {
    ds_nft_mint(oracle_nft, redeemer, policy_id, tx)
  }
 
  else(_) {
    fail
  }
}
 

Treasury

  • Early exit withdrawals when burning eligible NFTs
  • Validates asset number and divisor rules against Oracle state
treasury.ak
// 💰 burn a ds_nft to unlock utxos length of divisor
use aiken/builtin.{divide_integer, less_than_integer}
use aiken/collection/list
use aiken/primitive/int.{from_utf8}
use cardano/assets.{PolicyId}
use cardano/transaction.{
  InlineDatum, Output, OutputReference, Transaction, find_input,
}
use cocktail.{inputs_at, inputs_with_policy, only_minted_token}
use utils/datum_oracle.{OracleDatum}
use utils/redeemer_treasury.{TreasuryRedeemer, Withdraw}
 
pub fn treasury_spend(
  oracle_nft: PolicyId,
  nft_policy: PolicyId,
  redeemer: TreasuryRedeemer,
  input: OutputReference,
  tx: Transaction,
) {
  when
    (
      redeemer,
      inputs_with_policy(tx.reference_inputs, oracle_nft),
      find_input(tx.inputs, input),
    )
  is {
    (
      // when redeemer has asset name
      Withdraw { asset_name },
      // when 1 input reference with oracle nft is present
      [oracle_input_ref],
      // when we are spending our own input
      Some(own_input),
    ) ->
      when (oracle_input_ref.output.datum, from_utf8(asset_name)) is {
        (
          // when oracle utxo has some inline datum
          InlineDatum(oracle_datum),
          // when the redeemer's asset name converts to an integer from utf8
          Some(asset_name_int),
        ) -> {
          // oracle datum is valid
          expect OracleDatum { count, divisor, .. } = oracle_datum
          // use asset_name to check for burn
          let is_burned = only_minted_token(tx.mint, nft_policy, asset_name, -1)
          // use asset_name_int to check if less than count divided by divisor
          let under_threshold =
            less_than_integer(asset_name_int, divide_integer(count, divisor))
          // use divisor to check amount of inputs being spent
          let is_correct_input_count =
            list.length(inputs_at(tx.inputs, own_input.output.address)) == divisor
          is_burned? && under_threshold? && is_correct_input_count?
        }
        _ -> {
          trace @"Invalid oracle datum or nft name integer"
          False
        }
      }
    _ -> {
      trace @"Invalid redeemer, oracle input reference, or own input"
      False
    }
  }
}
 
validator treasury(oracle_nft: PolicyId, nft_policy: PolicyId) {
  spend(
    _datum_opt: Option<Data>,
    redeemer: TreasuryRedeemer,
    input: OutputReference,
    tx: Transaction,
  ) {
    treasury_spend(oracle_nft, nft_policy, redeemer, input, tx)
  }
 
  else(_) {
    fail
  }
}
 

Pot

  • Last-buyer prize withdrawal after timer elapses
  • Requires winner signature and timer condition
pot_withdraw.ak
// 🪺 be the winner and wait for timer to expire to withdraw the pot utxos
use aiken_design_patterns/stake_validator
use cardano/address.{Credential}
use cardano/assets.{PolicyId}
use cardano/transaction.{InlineDatum, Transaction}
use cocktail.{inputs_with_policy, key_signed, valid_after}
use utils/datum_oracle.{OracleDatum}
 
pub fn pot_withdraw_withdraw(
  oracle_nft: PolicyId,
  own_credential: Credential,
  tx: Transaction,
) {
  stake_validator.withdraw(
    // This logic runs once per transaction rather than once per utxo
    withdrawal_logic: fn(_own_validator, tx) {
      when inputs_with_policy(tx.reference_inputs, oracle_nft) is {
        [oracle_input_ref] ->
          when oracle_input_ref.output.datum is {
            // some datum is present
            InlineDatum(oracle_datum) -> {
              // oracle datum is valid
              expect OracleDatum {
                count,
                slot_start,
                slot_increase,
                winner,
                ..
              } = oracle_datum
              and {
                // is_winner?,
                key_signed(tx.extra_signatories, winner),
                // is_timer_expired?,
                valid_after(
                  tx.validity_range,
                  slot_start + count * slot_increase,
                ),
              }
            }
            _ -> {
              trace @"Invalid oracle datum"
              False
            }
          }
        _ -> {
          trace @"Invalid redeemer or oracle reference"
          False
        }
      }
    },
    stake_cred: own_credential,
    tx: tx,
  )
}
 
validator pot_withdraw(oracle_nft: PolicyId) {
  withdraw(_redeemer: Void, own_credential: Credential, tx: Transaction) {
    pot_withdraw_withdraw(oracle_nft, own_credential, tx)
  }
 
  else(_) {
    fail
  }
}
 

Marketplace

  • Buy: pays seller and fee receiver
  • Close: listing seller cancels
marketplace.ak
// 🔀 Trade NFTs peer to peer
use cardano/address.{Address}
use cardano/assets.{from_lovelace, lovelace_of}
use cardano/transaction.{Input, OutputReference, Transaction, find_input}
use cocktail.{
  address_pub_key, get_all_value_to, inputs_at, key_signed, value_geq,
}
use utils/datum_marketplace.{MarketplaceDatum}
use utils/redeemer_marketplace.{Buy, Close, MarketplaceRedeemer}
 
pub fn marketplace_spend(
  owner: Address,
  fee_percentage_basis_point: Int,
  datum_opt: Option<MarketplaceDatum>,
  redeemer: MarketplaceRedeemer,
  input: OutputReference,
  tx: Transaction,
) {
  expect Some(datum) = datum_opt
  when redeemer is {
    Buy -> {
      expect Some(own_input) = find_input(tx.inputs, input)
      let own_address = own_input.output.address
      let is_only_one_input_from_own_address =
        when inputs_at(tx.inputs, own_address) is {
          [_] -> True
          _ -> False
        }
      let is_proceed_paid =
        get_all_value_to(tx.outputs, datum.seller)
          |> value_geq(
              from_lovelace(datum.price + lovelace_of(own_input.output.value)),
            )
      let is_fee_paid =
        get_all_value_to(tx.outputs, owner)
          |> value_geq(
              from_lovelace(datum.price * fee_percentage_basis_point / 10000),
            )
      is_only_one_input_from_own_address && is_fee_paid && is_proceed_paid
    }
    Close -> {
      expect Some(pub_key) = address_pub_key(datum.seller)
      key_signed(tx.extra_signatories, pub_key)
    }
  }
}
 
validator marketplace(owner: Address, fee_percentage_basis_point: Int) {
  spend(
    datum_opt: Option<MarketplaceDatum>,
    redeemer: MarketplaceRedeemer,
    input: OutputReference,
    tx: Transaction,
  ) {
    marketplace_spend(
      owner,
      fee_percentage_basis_point,
      datum_opt,
      redeemer,
      input,
      tx,
    )
  }
 
  else(_) {
    fail
  }
}
 

The data structures that define the state and actions within the Knot system. Datums hold the current state information, while redeemers specify the actions that can be performed on each validator.

Oracle Datum

The central state container that tracks the game progress, including the current count, winner, timer values, and economic parameters. This datum is updated with each NFT purchase.

Key Fields:

  • count: Current participation count
  • winner: Public key hash of last buyer
  • treasury_price: ADA locked in treasury per mint
  • pot_price: ADA added to prize pool per mint
  • slot_start: Game start time
  • slot_increase: Timer extension per purchase
  • mode: Game difficulty (0=hard, 1=easy)
datum_oracle.ak
use cardano/address.{Address}
 
pub type OracleDatum {
  count: Int,
  // collection count, gets incremented each mint
  treasury_address: Address,
  // treasury address
  pot_address: Address,
  // pot address
  fee_address: Address,
  // developer's wallet
  winner: ByteArray,
  // pub key hash of last nft purchaser, the wallet who can claim the prize pool
  treasury_price: Int,
  // lovelace to lock in treasury upon mint
  pot_price: Int,
  // lovelace to lock in the pot upon mint
  fee_price: Int,
  // lovelace that leaves the protocol upon mint and pays devs
  slot_start: Int,
  // timer: slot of oracle deployment ( includes arbitrary offset like + 6 months  )
  slot_increase: Int,
  // timer: slot increase for each new mint
  divisor: Int,
  // label of the game
  name: ByteArray,
  // game mode 0 is hard, 1 is easy
  mode: Int,
}
 

Oracle Redeemer

Defines the actions that can be performed on the Oracle validator, primarily minting new DS NFTs when users purchase participation.

Actions:

  • MintDsNFT: Creates new participation NFT with winner info
redeemer_oracle.ak
pub type OracleRedeemer {
  MintDsNFT { winner: ByteArray }
}
 

Marketplace Datum

Contains listing information for NFTs being sold on the secondary market, including seller address, price, and token details.

Fields:

  • seller: Address of NFT owner
  • price: Asking price in lovelace
  • policy: NFT policy ID
  • tokenName: Specific token identifier
datum_marketplace.ak
use cardano/address.{Address}
 
pub type MarketplaceDatum {
  MarketplaceDatum {
    seller: Address,
    price: Int,
    policy: ByteArray,
    tokenName: ByteArray,
  }
}
 

Marketplace Redeemer

Controls the two main marketplace operations: purchasing listed NFTs and closing/canceling listings.

Actions:

  • Buy: Purchase the listed NFT
  • Close: Cancel the listing
redeemer_marketplace.ak
pub type MarketplaceRedeemer {
  Buy
  Close
}
 

Treasury Redeemer

Enables early exit withdrawals from the treasury when users burn their eligible NFTs, allowing them to claim their locked ADA.

Actions:

  • Withdraw: Claim treasury funds with asset name
redeemer_treasury.ak
pub type TreasuryRedeemer {
  Withdraw { asset_name: ByteArray }
}
 

Pot Redeemer

Allows the current winner to claim the prize pool after the timer expires, providing the final incentive for the last participant.

Actions:

  • Withdraw: Claim the accumulated prize pool
redeemer_pot.ak
pub type PotRedeemer {
  Withdraw
}
 

DS NFT Utilities

Helper functions and types for managing the DS NFT minting and burning logic, including game mode validation and mint polarity definitions.

Features:

  • check_game_mode: Validates timing based on mode
  • MintPolarity: Defines mint/burn operations
  • Easy/Hard Mode: Different timing constraints
utils_ds_nft.ak
use cardano/transaction.{Transaction}
use cocktail.{valid_before}
 
pub fn check_game_mode(
  tx: Transaction,
  slot_start: Int,
  slot_increase: Int,
  count: Int,
  mode: Int,
) -> Bool {
  when mode is {
    1 ->
      // easy mode
      valid_before(
        tx.validity_range,
        // tx.uppper < d.upper
        slot_start + slot_increase * count,
      )
    // hard mode
    0 -> True
    _ -> False
  }
}
 
pub type MintPolarity {
  RMint
  RBurn
}
 

Deployers mint a single Oracle NFT to authorize the Oracle script. This requires spending a specific UTxO referenced in the parameters, ensuring the NFT is one-per-deploy.

How does a knot keep track of its count?

A knot maintains its participation count using a state-threaded token (STT) initialized with a datum count of 1. During each new NFT minting process, validation rules require incrementing this by 1. The count is used as the asset name for the newly minted NFT, which is returned to the buyer. This mechanism ensures a verified, ordering of participants while storing minimal information on-chain and affording buyers maximum flexibility.

1 NFT = Count + 1

Can the deployer change rules later?

No. All economic parameters are fixed at deploy time.

What happens if no one buys for a long time?

When the timer reaches the derived deadline, the current winner can withdraw the pot.

What happens if everyone keeps buying faster than the timer can count down?

Timer will continue to grow each mint, there is no upper limit on how far it can extend into the future.

How does time work?

The countdown timer is configurated before deployment, and starts counting down immmediately. When someone buys an NFT, the timer extends by adding the slot_increase value. If buying outpaces the timer will continue to grow. If buying fails to keep pace the timer will count down until zero, unlocking the pot value to whomever bought the last NFT.

Is profit / yield / roi guaranteed?

No, its impossible to predict how the market will behave. Knots let you prepare for growth, so that if it does happen, there is roi available to some members not all, but it hinges on future growth which is impossible to predict, similar to all markets.

Can I sell my NFT without burning?

Yes, potential value is stored within the NFT, and awarded to the wallet who burns it, not the wallet who minted it. Use your favorite secondary market or use the Knots marketplace path to list, sell, or transfer NFTs.

How are early exits limited?

The treasury script validates NFT numbers and a divisor rule relative to the current count, ensuring controlled and fair early exits relative to the total number of NFTs minted.

How does the pot grow?

Each valid purchase contributes the configured pot_price to the pot. Over time, pot balance ≈ previous pot + pot_price × number of completed purchases − any prior withdrawals. Fees and treasury routing do not reduce the pot.

Who is the current winner?

Always the latest buyer recorded in the Oracle datum. A new purchase immediately replaces the previous winner and extends the timer.

What exactly do I pay when I buy?

The total ADA you send is split on-chain into three buckets: treasury_price, pot_price, and fee_price. These immutable amounts are defined at deployment.

What exactly do I get when I buy?

You receive a newly minted, numbered NFT sent to your wallet (asset name = current count). You also become the current winner and the countdown is extended. The NFT can be held, sold, or—when eligible—burned to withdraw a pro‑rata share from the treasury. If the timer reaches its deadline while you are still the winner, you can withdraw the pot. No ADA returns to you at purchase time beyond the NFT and these rights.

What makes an NFT eligible to burn for an early exit?

Eligibility is enforced by the Treasury validator and DS NFT policy. In brief, lower-numbered NFTs become eligible based on the current count and a divisor rule which throttles how many can exit at once. The burn must reference the Oracle datum, and the asset name must be a valid number that is under the threshold calculated from count / divisor.

Growth is required post minting for new NFTs to become eligible to burn.

Why is a new NFT farthest from an early exit?

Early‑exit eligibility is enforced by the Treasury validator's divisor rule relative to the current count. Lower‑numbered NFTs unlock first, and higher‑numbered (newer) NFTs unlock last. When you buy, your NFT has the highest number, so it starts farthest from eligibility. As more purchases occur (the count increases) and earlier numbers burn, your NFT moves closer to eligibility. You can still sell your NFT at any time via the marketplace.

Is there any admin key, pause, or upgrade path?

No. There are no privileged controls post-deploy. The system cannot be paused or upgraded; state only changes through user transactions that satisfy validator rules.

Can a buyer grief the next buyer?

Buyers cannot alter rules. They can only become the winner by performing a valid purchase that routes funds and updates the datum. The next buyer can always replace them by buying before the timer ends.

Do I need a special wallet?

Use a Cardano wallet with dApp connector support. The UI builds transactions with the correct script inputs, reference data, mint/burn actions, and redeemers.

Do I need the NFT to withdraw the last-buyer prize?

No. The pot withdrawal checks the Oracle datum winner and the timer condition. The winning signature is required; the NFT is not.

What if two people try to buy at the same time?

In the UTxO model, only one transaction can spend the exact Oracle input first. The other must rebuild against updated state and try again.

What is easy mode?

Easy mode will prevent new buying if the timer is reached. Normally buying is never restricted thus the process of spending pot value is done under competition, making it much more difficult to claim all value. Easy mode lets the winner spend the pot at their leisure. Contributed by @gulla0

Totals

  • Purchase total = treasury_price + pot_price + fee_price
  • Pot after n purchases ≈ initial pot + n × pot_price − withdrawals

Timer

  • Deadline = slot_start + count × slot_increase
  • Valid last-buyer withdrawal: current slot ≥ deadline and winner signature present

Early Member

Entering early gives access to burn-based exits when eligible, at deterministic values. Liquidity comes from the treasury path and secondary markets.

Last Buyer

Waiting risks being outbid by the next buyer, but the prize grows predictably with each purchase. Optimal timing depends on your view of flow and attention.

Purchase Fails

  • Insufficient ADA to cover total and network fees
  • Oracle input missing or datum not updated correctly
  • Incorrect mint policy or token name for the next number

Burn Fails

  • NFT number not yet eligible under divisor rule
  • Asset name formatting doesn't match expected numeric format
  • Missing reference to the Oracle datum

Pot Withdraw Fails

  • Timer not yet expired (current slot < deadline)
  • Transaction not signed by the recorded winner

Marketplace Fails

  • Listing seller signature missing on cancel
  • Price or fee routing does not match configured rules
  • Immutable economics; no privileged upgrades or pauses
  • On-chain enforcement of routing and eligibility rules
  • Deterministic mechanics; no external oracles or custody
  • Outcomes depend on user flow and network conditions
oracle.ak
// 🖲️ store and spend the state thread game logic
use cardano/transaction.{Input,
  Output, OutputReference, Transaction, find_input}
use cocktail.{inputs_at_with_policy, outputs_at_with_policy}
use utils/datum_oracle.{OracleDatum}
use utils/find_policy_id_of_first_token.{find_policy_id_of_first_token}
use utils/redeemer_oracle.{MintDsNFT, OracleRedeemer}
use utils/utils_oracle.{is_datum_updated, is_output_value_clean, is_value_paid}
 
pub fn oracle_spend(
  datum_opt: Option<OracleDatum>,
  redeemer: OracleRedeemer,
  input: OutputReference,
  tx: Transaction,
) {
  // find the input being spent
  expect Some(own_input) = find_input(tx.inputs, input)
  // find the NFT from its own input
  let oracle_nft_policy = find_policy_id_of_first_token(own_input)
  // find the address of the input being spent
  let own_address = own_input.output.address
  // destructure input datum
  expect Some(input_datum) = datum_opt
  when
    (
      redeemer,
      inputs_at_with_policy(tx.inputs, own_address, oracle_nft_policy),
      outputs_at_with_policy(tx.outputs, own_address, oracle_nft_policy),
    )
  is {
    (
      // when the redeemer includes a new_winner
      MintDsNFT { winner: new_winner },
      // when 1 input with oracle nft is present
      [_],
      // when 1 output with oracle nft is present
      [only_output],
    ) ->
      and {
        is_output_value_clean(only_output),
        is_datum_updated(input_datum, only_output, new_winner),
        is_value_paid(tx, input_datum),
      }
    _ -> False
  }
}
 
validator oracle {
  spend(
    datum_opt: Option<OracleDatum>,
    redeemer: OracleRedeemer,
    input: OutputReference,
    tx: Transaction,
  ) {
    oracle_spend(datum_opt, redeemer, input, tx)
  }
 
  else(_) {
    fail
  }
}
 
oracle_nft.ak
// 🖲️ one shot nft policy for state thread
use cardano/address.{Address}
use cardano/assets.{PolicyId}
use cardano/transaction.{OutputReference, Transaction}
use utils/utils_oracle_nft.{
  listing_is_sent, nft_names, one_time, oracle_is_sent, tokens_are_minted,
}
 
pub fn oracle_nft_mint(
  utxo_ref: OutputReference,
  oracle_address: Address,
  knot_address: Address,
  knot_fee: Int,
  policy_id: PolicyId,
  tx: Transaction,
) {
  // [utxo_ref.transaction_id, "Knot A Receipt"]
  expect [oracle_name, receipt_name] = nft_names(utxo_ref)
 
  and {
    // Makes sure utxo_ref is a part of the inputs
    one_time(tx.inputs, utxo_ref)?,
    // Checks if only two tokens are minted and their names are orcale_name and receipt_name
    tokens_are_minted(tx.mint, policy_id, oracle_name, receipt_name, 1)?,
    // Checks if oracle_nft is (name and policy) is sent to oracle_address
    oracle_is_sent(tx.outputs, policy_id, oracle_address, oracle_name)?,
    // Checks if correct fee is sent to knot_address. 
    listing_is_sent(tx.outputs, knot_address, knot_fee)?,
  }
}
 
validator oracle_nft(
  utxo_ref: OutputReference,
  oracle_address: Address,
  knot_address: Address,
  knot_fee: Int,
) {
  mint(_redeemer: Data, policy_id: PolicyId, tx: Transaction) {
    oracle_nft_mint(
      utxo_ref,
      oracle_address,
      knot_address,
      knot_fee,
      policy_id,
      tx,
    )
  }
 
  else(_) {
    fail
  }
}
 
ds_nft.ak
// 🫀 mint and burn ds_nft (ds is a nod to the original contract I called Double Spent)
use aiken/primitive/bytearray.{from_string}
use cardano/assets.{PolicyId}
use cardano/transaction.{InlineDatum, Input, Transaction}
use cocktail.{
  check_policy_only_burn, convert_int_to_bytes, inputs_with, inputs_with_policy,
  only_minted_token,
}
use utils/datum_oracle.{OracleDatum}
use utils/utils_ds_nft.{MintPolarity, RBurn, RMint, check_game_mode}
use utils/config.{config_receipt_name_string}
 
pub fn ds_nft_mint(
  oracle_nft: PolicyId,
  redeemer: MintPolarity,
  policy_id: PolicyId,
  tx: Transaction,
) {
  when redeemer is {
    RMint ->
      when inputs_with_policy(tx.inputs, oracle_nft) is {
        // when 1 oracle input is present
        [oracle_input] ->
          // There are 2 nfts from this policy so we make sure the receipt is not present, and infer the real oracle nft is being spent
          when
            inputs_with(
              [oracle_input],
              oracle_nft,
              from_string(config_receipt_name_string),
            )
          is {
            [_] -> False
            _ -> {
              // Oracle NFT is being spent
              // with inline datum
              expect InlineDatum(input_datum) = oracle_input.output.datum
              expect OracleDatum { count, mode, slot_start, slot_increase, .. } =
                input_datum
              and {
                only_minted_token(
                  tx.mint,
                  policy_id,
                  convert_int_to_bytes(count),
                  1,
                ),
                check_game_mode(tx, slot_start, slot_increase, count, mode),
              }
            }
          }
        _ -> False
      }
    // all minting values are negative 
    RBurn -> check_policy_only_burn(tx.mint, policy_id)
  }
}
 
validator ds_nft(oracle_nft: PolicyId) {
  mint(redeemer: MintPolarity, policy_id: PolicyId, tx: Transaction) {
    ds_nft_mint(oracle_nft, redeemer, policy_id, tx)
  }
 
  else(_) {
    fail
  }
}
 
treasury.ak
// 💰 burn a ds_nft to unlock utxos length of divisor
use aiken/builtin.{divide_integer, less_than_integer}
use aiken/collection/list
use aiken/primitive/int.{from_utf8}
use cardano/assets.{PolicyId}
use cardano/transaction.{
  InlineDatum, Output, OutputReference, Transaction, find_input,
}
use cocktail.{inputs_at, inputs_with_policy, only_minted_token}
use utils/datum_oracle.{OracleDatum}
use utils/redeemer_treasury.{TreasuryRedeemer, Withdraw}
 
pub fn treasury_spend(
  oracle_nft: PolicyId,
  nft_policy: PolicyId,
  redeemer: TreasuryRedeemer,
  input: OutputReference,
  tx: Transaction,
) {
  when
    (
      redeemer,
      inputs_with_policy(tx.reference_inputs, oracle_nft),
      find_input(tx.inputs, input),
    )
  is {
    (
      // when redeemer has asset name
      Withdraw { asset_name },
      // when 1 input reference with oracle nft is present
      [oracle_input_ref],
      // when we are spending our own input
      Some(own_input),
    ) ->
      when (oracle_input_ref.output.datum, from_utf8(asset_name)) is {
        (
          // when oracle utxo has some inline datum
          InlineDatum(oracle_datum),
          // when the redeemer's asset name converts to an integer from utf8
          Some(asset_name_int),
        ) -> {
          // oracle datum is valid
          expect OracleDatum { count, divisor, .. } = oracle_datum
          // use asset_name to check for burn
          let is_burned = only_minted_token(tx.mint, nft_policy, asset_name, -1)
          // use asset_name_int to check if less than count divided by divisor
          let under_threshold =
            less_than_integer(asset_name_int, divide_integer(count, divisor))
          // use divisor to check amount of inputs being spent
          let is_correct_input_count =
            list.length(inputs_at(tx.inputs, own_input.output.address)) == divisor
          is_burned? && under_threshold? && is_correct_input_count?
        }
        _ -> {
          trace @"Invalid oracle datum or nft name integer"
          False
        }
      }
    _ -> {
      trace @"Invalid redeemer, oracle input reference, or own input"
      False
    }
  }
}
 
validator treasury(oracle_nft: PolicyId, nft_policy: PolicyId) {
  spend(
    _datum_opt: Option<Data>,
    redeemer: TreasuryRedeemer,
    input: OutputReference,
    tx: Transaction,
  ) {
    treasury_spend(oracle_nft, nft_policy, redeemer, input, tx)
  }
 
  else(_) {
    fail
  }
}
 
pot_spend.ak
// 🪺 store utxos for winner - spend logic offloaded to pot_withdraw validator
use aiken/crypto.{ScriptHash}
use aiken_design_patterns/stake_validator
use cardano/transaction.{OutputReference, Transaction}
 
pub fn pot_spend_spend(withdraw_script_hash: ScriptHash, tx: Transaction) {
  stake_validator.spend_minimal(
    withdraw_script_hash: withdraw_script_hash,
    tx: tx,
  )
}
 
validator pot_spend(withdraw_script_hash: ScriptHash) {
  spend(
    _datum_opt: Option<Data>,
    _redeemer: Void,
    _input: OutputReference,
    tx: Transaction,
  ) {
    pot_spend_spend(withdraw_script_hash, tx)
  }
 
  else(_) {
    fail
  }
}
 
pot_withdraw.ak
// 🪺 be the winner and wait for timer to expire to withdraw the pot utxos
use aiken_design_patterns/stake_validator
use cardano/address.{Credential}
use cardano/assets.{PolicyId}
use cardano/transaction.{InlineDatum, Transaction}
use cocktail.{inputs_with_policy, key_signed, valid_after}
use utils/datum_oracle.{OracleDatum}
 
pub fn pot_withdraw_withdraw(
  oracle_nft: PolicyId,
  own_credential: Credential,
  tx: Transaction,
) {
  stake_validator.withdraw(
    // This logic runs once per transaction rather than once per utxo
    withdrawal_logic: fn(_own_validator, tx) {
      when inputs_with_policy(tx.reference_inputs, oracle_nft) is {
        [oracle_input_ref] ->
          when oracle_input_ref.output.datum is {
            // some datum is present
            InlineDatum(oracle_datum) -> {
              // oracle datum is valid
              expect OracleDatum {
                count,
                slot_start,
                slot_increase,
                winner,
                ..
              } = oracle_datum
              and {
                // is_winner?,
                key_signed(tx.extra_signatories, winner),
                // is_timer_expired?,
                valid_after(
                  tx.validity_range,
                  slot_start + count * slot_increase,
                ),
              }
            }
            _ -> {
              trace @"Invalid oracle datum"
              False
            }
          }
        _ -> {
          trace @"Invalid redeemer or oracle reference"
          False
        }
      }
    },
    stake_cred: own_credential,
    tx: tx,
  )
}
 
validator pot_withdraw(oracle_nft: PolicyId) {
  withdraw(_redeemer: Void, own_credential: Credential, tx: Transaction) {
    pot_withdraw_withdraw(oracle_nft, own_credential, tx)
  }
 
  else(_) {
    fail
  }
}
 
marketplace.ak
// 🔀 Trade NFTs peer to peer
use cardano/address.{Address}
use cardano/assets.{from_lovelace, lovelace_of}
use cardano/transaction.{Input, OutputReference, Transaction, find_input}
use cocktail.{
  address_pub_key, get_all_value_to, inputs_at, key_signed, value_geq,
}
use utils/datum_marketplace.{MarketplaceDatum}
use utils/redeemer_marketplace.{Buy, Close, MarketplaceRedeemer}
 
pub fn marketplace_spend(
  owner: Address,
  fee_percentage_basis_point: Int,
  datum_opt: Option<MarketplaceDatum>,
  redeemer: MarketplaceRedeemer,
  input: OutputReference,
  tx: Transaction,
) {
  expect Some(datum) = datum_opt
  when redeemer is {
    Buy -> {
      expect Some(own_input) = find_input(tx.inputs, input)
      let own_address = own_input.output.address
      let is_only_one_input_from_own_address =
        when inputs_at(tx.inputs, own_address) is {
          [_] -> True
          _ -> False
        }
      let is_proceed_paid =
        get_all_value_to(tx.outputs, datum.seller)
          |> value_geq(
              from_lovelace(datum.price + lovelace_of(own_input.output.value)),
            )
      let is_fee_paid =
        get_all_value_to(tx.outputs, owner)
          |> value_geq(
              from_lovelace(datum.price * fee_percentage_basis_point / 10000),
            )
      is_only_one_input_from_own_address && is_fee_paid && is_proceed_paid
    }
    Close -> {
      expect Some(pub_key) = address_pub_key(datum.seller)
      key_signed(tx.extra_signatories, pub_key)
    }
  }
}
 
validator marketplace(owner: Address, fee_percentage_basis_point: Int) {
  spend(
    datum_opt: Option<MarketplaceDatum>,
    redeemer: MarketplaceRedeemer,
    input: OutputReference,
    tx: Transaction,
  ) {
    marketplace_spend(
      owner,
      fee_percentage_basis_point,
      datum_opt,
      redeemer,
      input,
      tx,
    )
  }
 
  else(_) {
    fail
  }
}