# Gas Optimization

Gas pricing on MultiversX determines both transaction cost and confirmation speed. Use real-time statistics from mx-relayer to submit transactions at the right price.

***

## Background: gasPrice and PPU on MultiversX

Every MultiversX transaction specifies a `gasPrice` in atomic EGLD units (`10^-18 EGLD`) and a `gasLimit` in gas units. The total fee is approximately:

```
fee = gasPrice * gasUsed
```

The network enforces a minimum `gasPrice` of `1,000,000,000` (10^9). Validators prioritize higher-priced transactions, so submitting at the minimum during congestion may delay or miss inclusion.

**PPU (Price Per Unit)** is the effective cost per gas unit of computation. For transactions with a non-empty `data` field, each byte costs an additional 1,500 gas units at the current `gasPrice`. PPU accounts for this, making it a more accurate comparator across transactions with different data sizes.

For swap transactions, arb-algo's `txData` populates the `data` field, often adding several hundred bytes. PPU is therefore higher for swaps than for a plain EGLD transfer at the same `gasPrice`.

***

## How the Sliding Window Works

Gas statistics are computed over a **30-minute sliding window** of **10-second buckets** (180 buckets total).

```
Time →
├─ bucket[179] ─ bucket[178] ─ ... ─ bucket[1] ─ bucket[0] ─┤
│←────────────────── 30 minutes ─────────────────────────────►│
                                                               now
```

As time advances, the oldest bucket expires and drops from the window. Each bucket records all transactions observed in that 10-second interval.

**Spam resistance.** Spam attacks typically flood the mempool with minimum-price transactions in a short burst. A 10-second spike influences the window for at most 30 minutes before being evicted. The `bucketAvg` metric weights each bucket equally regardless of transaction count, so a single high-volume spam bucket does not distort the 30-minute average.

**Percentiles (p50, p75, p90)** are computed across all transactions in the full window using reservoir sampling. Because spam transactions cluster at the minimum price, p50 reflects the median including spam, while p75 and p90 reflect what legitimate transactions actually paid.

| Metric      | What it measures                                   | Effect of spam                              |
| ----------- | -------------------------------------------------- | ------------------------------------------- |
| `avg`       | Simple mean over all transactions in 30-min window | Biased low by spam volume                   |
| `bucketAvg` | Mean of per-bucket averages                        | Less sensitive to volume spikes             |
| `p50`       | 50th percentile gas price                          | Often at or near minimum during spam        |
| `p75`       | 75th percentile gas price                          | Spam-resistant; typical legitimate tx price |
| `p90`       | 90th percentile gas price                          | Upper bound; used during high congestion    |

***

## When to Use p50 vs p75 vs p90

| Scenario                                 | Recommended metric            | Rationale                                      |
| ---------------------------------------- | ----------------------------- | ---------------------------------------------- |
| Non-urgent background operations         | `p50`                         | Cheapest that has historically been included   |
| Standard user-initiated swaps            | `p75`                         | Reliable inclusion without significant overpay |
| Time-sensitive arbitrage or liquidations | `p90`                         | Competitive even during congestion             |
| Mempool pressure > 0.5 (see below)       | One tier higher than baseline | Elevated pressure warrants elevated price      |
| Shard has no recent transactions         | `1000000000` (minimum)        | Shard is idle; minimum is sufficient           |

{% hint style="info" %}
When a shard reports all gas price fields as `1000000000` and PPU as `0`, it has no recent transaction history. The shard has spare capacity and the minimum price is sufficient.
{% endhint %}

***

## Using mempool.pressure from networkStats

The `networkStats` stream (available at `wss://relayer.xoxno.com/ws/stats`, pushed every 250ms) includes a `mempool.pressure` value per shard — the current queue depth relative to the shard's throughput capacity.

```json
{
  "type": "networkStats",
  "data": {
    "shards": [
      {
        "id": 0,
        "tps": 85.3,
        "mempool": {
          "size": 1240,
          "pressure": 0.34
        },
        "blockTimeMsP50ByShard": 6100,
        "validatorHealth": 1.0
      }
    ]
  }
}
```

Use `mempool.pressure` to escalate the gas price percentile dynamically:

```javascript
function selectPercentile(pressure, baselinePriority = 'normal') {
  // pressure: 0.0 (empty) to 1.0 (full)
  if (pressure > 0.75) return 'high';      // heavy congestion → p90
  if (pressure > 0.5)  return 'elevated';  // moderate → p75-p90 blend
  if (pressure > 0.25) return 'normal';    // light → p75
  return baselinePriority === 'low' ? 'low' : 'normal';
}
```

***

## Block Time Expectations

Swap transactions to a DEX contract on the sender's shard finalize in one block time (approximately 6 seconds on mainnet). Cross-shard transactions require two block times: one on the initiating shard and one on the receiving shard.

The `blockTimeMsP50ByShard` field in `networkStats` gives the current median block time per shard. For cross-shard transactions, sum the two relevant shards to estimate confirmation time.

```javascript
function estimateConfirmationMs(senderShard, contractShard, networkStats) {
  const shards = new Map(networkStats.shards.map(s => [s.id, s]));
  const senderTime = shards.get(senderShard)?.blockTimeMsP50ByShard ?? 6000;
  const contractTime = shards.get(contractShard)?.blockTimeMsP50ByShard ?? 6000;

  if (senderShard === contractShard) return senderTime;
  return senderTime + contractTime; // cross-shard: two block times
}
```

***

## Cost vs. Confirmation Speed Tradeoff

The following table shows expected behavior at each gas price tier under different network conditions. Values are illustrative; actual behavior depends on current validator inclusion policy.

| Gas price tier | Metric            | Low congestion         | Moderate congestion    | High congestion        |
| -------------- | ----------------- | ---------------------- | ---------------------- | ---------------------- |
| Minimum        | `1000000000`      | Included in 1-2 blocks | May wait 5-10 blocks   | Risk of dropping       |
| p50            | median tx         | Included in 1-2 blocks | Included in 2-4 blocks | May wait 10+ blocks    |
| p75            | legitimate txs    | Included in next block | Included in 1-2 blocks | Included in 2-4 blocks |
| p90            | high-priority txs | Included in next block | Included in next block | Included in 1-2 blocks |

***

## Adaptive Gas Pricing: Full Example

The following function subscribes to both `gasStats` and `networkStats` and returns a gas price that adapts to current shard conditions.

```javascript
class AdaptiveGasAdvisor {
  constructor() {
    this.gasStats = new Map();     // shard → gasStats data
    this.networkStats = new Map(); // shard → networkStats data

    this.wsGas = new WebSocket('wss://relayer.xoxno.com/ws');
    this.wsNet = new WebSocket('wss://relayer.xoxno.com/ws/stats');

    this.wsGas.onopen = () => {
      this.wsGas.send(JSON.stringify({ action: 'subscribe', topic: 'gasStats' }));
    };

    this.wsGas.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      if (msg.type === 'gasStats') {
        this.gasStats.set(msg.data.shard, msg.data);
      }
    };

    // /ws/stats delivers networkStats unconditionally — no subscribe needed
    this.wsNet.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      if (msg.type === 'networkStats') {
        for (const shard of msg.data.shards) {
          this.networkStats.set(shard.id, shard);
        }
      }
    };
  }

  /**
   * Returns a recommended gasPrice for the given shard.
   *
   * @param {number} shard - Shard ID (0, 1, or 2)
   * @param {'low'|'normal'|'high'} priority - Baseline priority before pressure adjustment
   * @returns {number} gasPrice in atomic units
   */
  getGasPrice(shard, priority = 'normal') {
    const gas = this.gasStats.get(shard);
    const net = this.networkStats.get(shard);

    // Fall back to network minimum if no data yet
    if (!gas) return 1_000_000_000;

    const pressure = net?.mempool?.pressure ?? 0;

    // Escalate tier based on mempool pressure
    let tier = priority;
    if (pressure > 0.75 && tier !== 'high') tier = 'high';
    else if (pressure > 0.5 && tier === 'low') tier = 'normal';

    switch (tier) {
      case 'low':    return gas.gasPrice.bucketAvg;
      case 'normal': return gas.gasPrice.percentiles.p75;
      case 'high':   return gas.gasPrice.percentiles.p90;
      default:       return gas.gasPrice.percentiles.p75;
    }
  }

  /**
   * Returns the estimated confirmation time in milliseconds for a transaction
   * between the two shards.
   */
  estimateConfirmationMs(senderShard, contractShard) {
    const senderData = this.networkStats.get(senderShard);
    const contractData = this.networkStats.get(contractShard);
    const t1 = senderData?.blockTimeMsP50ByShard ?? 6_000;
    const t2 = contractData?.blockTimeMsP50ByShard ?? 6_000;
    return senderShard === contractShard ? t1 : t1 + t2;
  }

  /**
   * Returns true if the shard is under notable congestion.
   */
  isCongested(shard) {
    return (this.networkStats.get(shard)?.mempool?.pressure ?? 0) > 0.5;
  }

  close() {
    this.wsGas.close();
    this.wsNet.close();
  }
}

// Usage
const advisor = new AdaptiveGasAdvisor();

// Allow a short warm-up period for initial stats to arrive
await new Promise(r => setTimeout(r, 200));

const senderShard = 0;
const gasPrice = advisor.getGasPrice(senderShard, 'normal');
const etaMs = advisor.estimateConfirmationMs(senderShard, 2); // shard 2 contract

console.log(`Gas price: ${gasPrice}`);
console.log(`Estimated confirmation: ${etaMs}ms`);

if (advisor.isCongested(senderShard)) {
  console.warn('Shard is congested — consider delaying non-urgent transactions');
}

// Use gasPrice when building the transaction
// advisor.close() when done
```

***

## Practical Guidelines

**For swap integrations:**

* Subscribe to `gasStats` before the first transaction. The first message arrives immediately on subscription.
* Default to `p75`. It provides reliable inclusion without significant overpay under most conditions.
* Check `mempool.pressure` from `networkStats`. If pressure exceeds 0.5, step up to `p90` for user-facing transactions.
* For background or non-urgent operations (e.g., periodic rebalancing), `bucketAvg` is sufficient.

**Avoid:**

* Using the raw `avg` field for pricing decisions. Spam volume biases it low and it may underestimate the price required for inclusion.
* Setting `gasPrice` below the network minimum of `1,000,000,000`. The network rejects transactions with lower prices.
* Caching gas stats for more than a few seconds. Market conditions on MultiversX can shift within a single block.

***

## Related Pages

* [Gas Statistics](https://docs.xoxno.com/developers/relayer-api/gas-stats) — Full `gasStats` message schema and field descriptions
* [Transaction Lifecycle](https://docs.xoxno.com/developers/guides/transaction-lifecycle) — How gas stats fit into the end-to-end swap flow
* [System Architecture](https://docs.xoxno.com/developers/guides/system-architecture) — How mx-relayer and mx-notifier produce these statistics
