XChain Platform Action - DISPENSER
This action creates a dispenser (vending machine) to dispense TICK when triggered
PARAMS
| Name | Type | Description |
|---|---|---|
VERSION |
String | Format Version |
GIVE_COIN |
String | COIN name (BTC, LTC, DOGE, etc) |
GIVE_TICK |
String | Ticker name or Ticker ID |
GIVE_AMOUNT |
String | Quantity of GIVE_TICK to DISPENSE when triggered |
GIVE_ESCROW |
String | Quantity of GIVE_TICK to escrow in dispenser |
GET_COIN |
String | COIN name (BTC, LTC, DOGE, etc) |
GET_TICK |
String | Ticker name or Ticker ID |
GET_AMOUNT |
String | Quantity of GET_COIN or GET_TICK required to DISPENSE |
GET_ADDRESS |
String | Address for dispenser to operate on (default=SOURCE) |
FIAT_CODE |
String | Code for FIAT currency your dispenser is priced in (USD, JPY, GBP, etc.) |
FIAT_AMOUNT |
String | Amount of FIAT currency required to trigger a DISPENSE (ignored when ORACLE_ADDRESS is set) |
ORACLE_ADDRESS |
String | Optional address of a user oracle (PRICE v1) that prices the dispensed token in FIAT_CODE |
EXPIRATION |
String | Timestamp of when dispenser should close, in Unix time |
ALLOW_LIST |
String | ACTION_INDEX of a LIST of addresses allowed to trigger dispenser |
BLOCK_LIST |
String | ACTION_INDEX of a LIST of addresses NOT allowed to trigger a dispenser |
GIVE_OWNERSHIP |
String | When 1, dispense ownership of GIVE_TICK instead of a balance amount (single-shot; default 0) |
MEMO |
String | An optional memo to include |
DISPENSER_ACTION_INDEX |
String | ACTION_INDEX of existing DISPENSER |
Formats
Version 0 - Create Dispenser
VERSION|GIVE_COIN|GIVE_TICK|GIVE_AMOUNT|GIVE_OWNERSHIP|GIVE_ESCROW|GET_COIN|GET_TICK|GET_AMOUNT|GET_ADDRESS|FIAT_CODE|FIAT_AMOUNT|ORACLE_ADDRESS|EXPIRATION|ALLOW_LIST|BLOCK_LIST|MEMO
Version 1 - Cancel Dispenser
VERSION|DISPENSER_ACTION_INDEX|MEMO
Version 2 - Edit Dispenser
VERSION|DISPENSER_ACTION_INDEX|GIVE_ESCROW|EXPIRATION|ALLOW_LIST|BLOCK_LIST|MEMO
Examples
DISPENSER|0|BTC|JDOG|1|10|BTC||0.01|1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev|||||||Creating JDOG dispensers at 0.01 BTC each
This example creates a dispenser, escrows 10 JDOG tokens in it, and dispenses 1 JDOG token when 0.01 BTC is sent to 1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev (no FIAT pricing, no oracle)
DISPENSER|1|1234|Canceling JDOG Dispenser
This example cancels the dispenser in example 1 with `ACTION_INDEX` 1234
DISPENSER|2|1234|100||||Refilling with 100
This example refills a dispenser with `ACTION_INDEX` 1234 with 100 JDOG tokens
DISPENSER|2|1234|||9876|5432|Updating allow/block lists
This example updates the allow and block lists for dispenser with `ACTION_INDEX` 1234
DISPENSER|0|BTC|PEPECASH|100|100|BTC||0|1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev|USD|0.05||0|||Selling PEPECASH for $0.05 USD each
FIAT dispenser using validator price oracle (PRICE v0): escrows 100 PEPECASH and dispenses 100 at a time when a buyer sends BTC equivalent to $0.05 USD per token. GET_AMOUNT is set to 0 because the effective BTC price is determined dynamically via validator BTC/USD snapshots. ORACLE_ADDRESS is empty.
DISPENSER|0|BTC|PEPECASH|100|100|BTC||0|1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev|JPY||1OracleSourceAddrXXXXXXXXXXXXXXXXX|0|||Using user oracle for JPY pricing
FIAT dispenser using a user-run TOKEN/FIAT oracle (PRICE v1): the oracle at `1OracleSourceAddrXXX...` publishes PEPECASH/JPY prices. FIAT_AMOUNT is empty because the oracle provides the price. The system combines the user oracle PEPECASH/JPY with the validator BTC/JPY snapshot to compute the BTC equivalent.
DISPENSER|0|BTC|JDOG|1|10|BTC||0.01|1FreshAddrZZZZZZZZZZZZZZZZZZZZZZZZ|||||||Opening a dispenser on a brand-new address from my main wallet
Fresh-address pattern: SOURCE is the user's main wallet, GET_ADDRESS is a newly-generated address with no prior on-chain activity. Allowed by the fresh-address exception, so DISPENSER_PREFERENCE on `1FreshAddr...` does not need to be pre-configured. SOURCE retains cancel authority; on cancel, escrow returns to SOURCE.
DISPENSER|0|BTC|JDOG||1||BTC||0.50000000|1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev|||||||First buyer of 0.5 BTC gets JDOG ownership
Ownership dispenser: `GIVE_OWNERSHIP=1` with empty `GIVE_AMOUNT`/`GIVE_ESCROW` escrows the JDOG ownership record. The first valid 0.5 BTC payment transfers ownership to the payer; the dispenser auto-closes after the single dispense.
DISPENSER|0|BTC|JDOG||1||BTC||0|1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev|USD|500.00||||Selling JDOG ownership for $500 USD
FIAT-priced ownership dispenser using the validator BTC/USD oracle. Payer sends BTC equivalent to $500 USD at current snapshot price; ownership transfers and the dispenser closes.
DISPENSER|0|BTC|JDOG||1||BTC|PEPECASH|10000000.00000000|1JDogZS6tQcSxwfxhv6XKKjcyicYA4Feev|||||||Selling JDOG ownership for 10M PEPECASH via dispenser
Token-priced ownership dispenser: first matcher who delivers 10,000,000 PEPECASH receives JDOG ownership.
Rules
- A dispenser may be created on an address other than
SOURCEonly if either: (a) the target address has setDISPENSER_PREFERENCE=2via anADDRESSaction, or (b) the target address has never appeared on chain as ofBLOCK_INDEX − 1(fresh-address exception) - When
GET_ADDRESS == SOURCEthe dispenser is always allowed (owner self-opening); preference and freshness are not consulted - Dispensers can be closed by the dispenser
GET_ADDRESSorSOURCEaddress which first opened the dispenser - If a dispenser is closed by the dispenser
GET_ADDRESS, tokens escrowed in the dispenser are returned toGET_ADDRESS - If a dispenser is closed by the dispenser
SOURCE, tokens escrowed in the dispenser are returned toSOURCE - If a dispenser is closed via
SWEEP(DISPENSERS=1), remaining escrowed tokens — or the escrowed ownership record, ifGIVE_OWNERSHIP=1— are credited to the SWEEPDESTINATION(seeSWEEP) - If a dispenser closes due to
EXPIRATION(no canceller), tokens escrowed in the dispenser are returned toSOURCE FIAT_CODEandFIAT_AMOUNTmust both be provided together (or both empty), unlessORACLE_ADDRESSis setORACLE_ADDRESSrequiresFIAT_CODEto be set (the oracle prices the token in that fiat currency)ORACLE_ADDRESSmakesFIAT_AMOUNToptional/ignored — the oracle provides the priceORACLE_ADDRESSmust be a valid crypto address
Token Ownership Dispensers
GIVE_OWNERSHIP=1requires SOURCE to be the current owner ofGIVE_TICK;GIVE_AMOUNTandGIVE_ESCROWmust be empty; the ownership record moves into a protocol-held escrow state- Ownership dispensers are single-shot: max dispenses =
1; the dispenser auto-closes after the first successful dispense - FIAT pricing (validator oracle and user oracle) and reverse-price-matching (24-hour window) apply unchanged
- Existing
GET_ADDRESSrules (DISPENSER_PREFERENCE/ fresh-address / self-open) apply unchanged - While ownership is escrowed, the following actions targeting the escrowed
TICKare rejected:ISSUEVersions 1–5 (description/mint/lock/callback/list edits)CALLBACK,SLEEPLINKusing thisTICK’sISSUEasCOIN2_ACTION_INDEXFILEwithGATE_TICKER= thisTICK- New child
ISSUEusing thisTICKas a parent (period-separated name) - Additional
ORDER/SWAP/DISPENSERownership offers for thisTICKfrom the original owner
- Holder-side actions are unaffected:
SEND,MINT(if mint window open),DIVIDENDpayouts,DEPOSIT/WITHDRAW,DESTROYof held balance - Child
TICKs (e.g.JDOG.SUB1) have independent ownership records and are not transferred when a parent’s ownership is sold - On successful dispense: ownership transfers atomically to the payer’s address
- On cancel (Version 1) /
EXPIRATION: ownership returns to the SOURCE that opened the dispenser - On
SWEEP-closure: ownership is delivered to the SWEEPDESTINATION
Notes
- Dispensers are closed and any escrowed funds returned after a set amount of time (1 hour)
- Dispenser
LISTedits are delayed a set amount of time (1 hour) - Dispensers are limited to a set maximum number of dispenses (1,000)
FIAT_CODEaccepts the following 12 currencies:USD= US DollarCAD= Canadian DollarAUD= Australian DollarMXN= Mexican PesoGBP= Great Britain PoundJPY= Japanese YenCNY= Chinese YuanCHF= Swiss FrancBRL= Brazilian RealINR= Indian RupeeEUR= EuroKRW= South Korean Won
FIAT_AMOUNTformat isX.XXEXPIRATIONbegins the process of closing a dispenser after a set block delay- Use
^(caret) as prefix when passingTICK_IDforTICKfield (^1234 =TICK_ID1234)
FIAT Dispensers
FIAT dispensers allow token sellers to price their tokens in a traditional currency (e.g., USD, JPY) while accepting payment in the native coin (BTC, LTC, DOGE). This enables non-XChain users to trigger a dispense by sending a simple bare coin payment — no XChain transaction required.
There are two pricing modes:
Mode 1: Validator Price Oracle (no ORACLE_ADDRESS)
The dispenser sets FIAT_CODE and FIAT_AMOUNT directly. Pricing uses the validator COIN/FIAT snapshot for the GET_COIN/FIAT pair.
Example: Dispenser sells PEPECASH for BTC at $0.05 USD per token. Buyer sends BTC; system uses the validator BTC/USD price to compute the equivalent BTC amount.
Calculation:
btc_per_token = FIAT_AMOUNT / btc_usd_price (from validator PRICE v0)
units = floor(COIN_AMOUNT / btc_per_token)
Mode 2: User Oracle (ORACLE_ADDRESS set)
The dispenser delegates pricing to a user-run TOKEN/FIAT oracle (PRICE v1). The oracle publishes the token’s price in the specified FIAT currency. The system combines the user oracle price with the validator COIN/FIAT price for cross-conversion.
Example: Dispenser sells PEPECASH for BTC, with the oracle pricing PEPECASH at ¥7.50 JPY. Buyer sends BTC; system uses the validator BTC/JPY price to convert.
Calculation:
oracle_price = pepecash_jpy (from user PRICE v1, oracle's effective price)
coin_price = btc_jpy (from validator PRICE v0)
units = floor((COIN_AMOUNT × coin_price) / oracle_price)
Why both oracles? The user oracle prices the token, the validator oracle prices the coin — both are needed when the buyer is paying in a coin different from the oracle’s quote currency. Both must use the same FIAT currency as the bridge.
Reverse Price Matching
Because blockchain payments can take time to confirm, oracle prices may change between when the buyer sends the payment and when it lands in a block. The system handles this by searching historical oracle price snapshots within a 24-hour window (86400 seconds) before the payment’s block_time.
Algorithm (validator path):
- Retrieve all finalized validator price snapshots for the COIN/FIAT pair within the 24-hour window, ordered newest-first
- For each snapshot (newest to oldest):
- Calculate
btc_per_token = FIAT_AMOUNT / snapshot.price - Calculate
units = floor(COIN_AMOUNT / btc_per_token) - If
units >= 1: match found — dispenseunits × GIVE_AMOUNTtokens
- Calculate
- The first (most recent) matching snapshot is used — the buyer most likely used the latest price they saw
- If no snapshot produces at least 1 unit, the dispense is invalid
Algorithm (user oracle path):
- Retrieve all user oracle prices for the
(ORACLE_ADDRESS, GIVE_COIN, GIVE_TICK, FIAT_CODE)combination within the 24-hour window, ordered newest-first - For each oracle price (gated by the 24-hour lock window via
effective_at <= block_time):- Fetch the validator GET_COIN/FIAT price at the oracle’s effective time
- Calculate
units = floor((COIN_AMOUNT × coin_price) / oracle_price) - If
units >= 1: match found
- Returns the first matching combination
Overpayment / Tips
If a buyer sends slightly more coin than the exact calculated amount, the system floors the unit count and absorbs the excess as overpayment. The extra amount does not trigger additional dispenses. For example, if the exact cost for 5 units is 0.000005 BTC and the buyer sends 0.0000055 BTC, the system dispenses 5 units.
Oracle Front-Running Protection
User oracles (PRICE v1) have a built-in anti-front-running mechanism: the first price for a (ORACLE_ADDRESS, COIN, TICK, FIAT) combination takes effect immediately, but all subsequent updates are delayed by 24 hours. This means an oracle operator cannot see an incoming dispenser payment and rush a price update to manipulate the exchange rate — pending payments settle at the old price before the new one activates.
Recommendations
- Buyers should send with a high transaction fee to ensure confirmation within the 24-hour price window
- If a payment is stuck in the mempool for longer than 24 hours, no matching price snapshot will be found and the dispense will be invalid
Dispenser Close Window
Dispensers have a 1-hour close delay (DISPENSER_CLOSE_DELAY). When a dispenser is cancelled or runs out of tokens, it enters a “cancelling” state for 1 hour before fully closing. FIAT dispense payments that confirm during this window are still processed normally — the dispenser honors pending dispenses until the close window elapses.
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.