Uniswap X is an intent-based, auction-driven routing protocol that delivers better prices, gas-free swaps, and built-in MEV protection by letting swappers sign orders that competing fillers execute onchain. The same efficiency, though, leaves most offchain indexers effectively blind. The data is emitted. It’s just not in a format their offchain parsers can easily recognize. It’s no problem for Sim IDX. This guide breaks down exactly how Sim IDX’s unique onchain architecture turns this challenge into a straightforward indexing task.
What you’ll learn in this guide
  • How the Quoter pattern lets an onchain listener pull fully-decoded swap data straight from Uniswap X reactors. This is something conventional offchain indexers can’t do.
  • Why a single Sim IDX listener can index every Uniswap X swap, regardless of reactor type or order variant.
  • The fee-handling and onchain interaction techniques that make your indexer both accurate and future-proof.

Why is Uniswap X Hard to Index?

The core difficulty in indexing Uniswap X stems from its order-based architecture. Reactors settle signed orders and verify execution, but they don’t emit explicit logs of the final token amounts. Instead, the critical details live inside encoded calldata that only the reactors themselves can decode. This complexity is compounded by:
  1. Multiple Reactor Types: Uniswap X relies on several reactor contracts, each tailored to a different kind of order with its own decoding logic. An indexer must replicate the nuances of every variant, which is a significant engineering challenge.
  2. Opaque Logs: onchain events expose only an order hash and the filler address, leaving out the tokens traded, the final amounts, and even the recipient of the swap proceeds.
  3. Decoded State is Private/Internal: All the logic required to understand an order’s final state is contained within the reactor contracts themselves. Traditional indexers, which operate offchain, cannot easily access or execute this onchain logic.
An attempt to index this with a traditional indexing tool would require painstakingly re-implementing every reactor’s decoding logic offchain and keeping it in sync with any protocol upgrades.

The Sim IDX Solution

Sim IDX overcomes these challenges by fundamentally changing the relationship between the indexer and the protocol. Where traditional indexers are passive, offchain observers, a Sim IDX listener is a smart contract that lives onchain. The listener is a smart contract, so it can do more than just read events. It can interact with other onchain contracts. The listener can call their functions, pass them arguments, and read their return data, just like any other contract would. This capability lets us use an elegant technique we call the Quoter Pattern. Instead of painstakingly re-implementing Uniswap X’s sophisticated decoding logic, we simply ask the Reactor to run its own logic for us. We treat the protocol itself as an on-demand decoding oracle. The following sections highlight the most unique parts of this implementation. To see the code in its entirety, we encourage you to explore the full repository.

View Full Code on GitHub

Explore the complete, unabridged source code for this Uniswap X indexer.

Index by ABI, Not by Address

The first challenge in indexing Uniswap X is its scale. The protocol uses multiple Reactor contracts across several chains, and new ones can be deployed at any time. Maintaining a hardcoded list of addresses is not a scalable solution. Sim IDX solves this with ABI-based triggers. Instead of targeting specific addresses, you can instruct your listener to trigger on any contract that matches a given ABI signature. This makes your indexer automatically forward-compatible. We achieve this with the ChainIdAbi helper in our Triggers contract. The code below sets up our listener to trigger on the execute function family for any contract on Ethereum, Unichain, and Base that implements the IReactor interface.
listeners/src/Main.sol
import "sim-idx-sol/Simidx.sol";
contract Triggers is BaseTriggers {
    function triggers() external virtual override {
        Listener listener = new Listener();
        addTrigger(ChainIdAbi(1, IReactor$Abi()), listener.triggerPreExecuteFunction());
        addTrigger(ChainIdAbi(1, IReactor$Abi()), listener.triggerPreExecuteBatchFunction());
        addTrigger(ChainIdAbi(1, IReactor$Abi()), listener.triggerPreExecuteBatchWithCallbackFunction());
        addTrigger(ChainIdAbi(1, IReactor$Abi()), listener.triggerPreExecuteWithCallbackFunction());
        addTrigger(ChainIdAbi(130, IReactor$Abi()), listener.triggerPreExecuteFunction());
        addTrigger(ChainIdAbi(130, IReactor$Abi()), listener.triggerPreExecuteBatchFunction());
        addTrigger(ChainIdAbi(130, IReactor$Abi()), listener.triggerPreExecuteBatchWithCallbackFunction());
        addTrigger(ChainIdAbi(130, IReactor$Abi()), listener.triggerPreExecuteWithCallbackFunction());
        addTrigger(ChainIdAbi(8453, IReactor$Abi()), listener.triggerPreExecuteFunction());
        addTrigger(ChainIdAbi(8453, IReactor$Abi()), listener.triggerPreExecuteBatchFunction());
        addTrigger(ChainIdAbi(8453, IReactor$Abi()), listener.triggerPreExecuteBatchWithCallbackFunction());
        addTrigger(ChainIdAbi(8453, IReactor$Abi()), listener.triggerPreExecuteWithCallbackFunction());
    }
}
This single pattern ensures our listener will capture swaps from all current and future Uniswap X reactors without needing any code changes.
To learn more about ChainAbi, see the Listener Features guide.

Decode Opaque Data with the Quoter Pattern

Once triggered, our listener receives the opaque order bytes. Replicating the decoding logic for every reactor type offchain would be a massive, brittle engineering effort. With Sim IDX, we don’t have to. Because our listener is an onchain contract, we can use the “Quoter Pattern” to have the Reactor contract decode its own data for us. This pattern is implemented in the OrderQuoter.sol contract, which our main Listener inherits from. It revolves around two primary functions:
  1. The quote function, called by our listener’s handler, simulates a fill by calling the Reactor’s executeWithCallback inside a try/catch block. It knows this call will revert and is designed to catch the revert reason.
    listeners/src/OrderQuoter.sol
    /// @notice Quote the given order, returning the ResolvedOrder object which defines
    /// the current input and output token amounts required to satisfy it
    /// Also bubbles up any reverts that would occur during the processing of the order
    /// @param order abi-encoded order, including `reactor` as the first encoded struct member
    /// @param sig The order signature
    /// @return result The ResolvedOrder
    function quote(bytes memory order, bytes memory sig) external returns (ResolvedOrder memory result) {
        try IReactor(getReactor(order)).executeWithCallback(SignedOrder(order, sig), bytes("")) {}
        catch (bytes memory reason) {
            result = parseRevertReason(reason);
        }
    }
    
  2. The reactorCallback function is the endpoint the Reactor calls on our contract. It receives the fully decoded ResolvedOrder, encodes it, and immediately places it into a revert message.
    listeners/src/OrderQuoter.sol
    /// @notice Reactor callback function
    /// @dev reverts with the resolved order as reason
    /// @param resolvedOrders The resolved orders
    function reactorCallback(ResolvedOrder[] memory resolvedOrders, bytes memory) external pure {
        if (resolvedOrders.length != 1) {
            revert OrdersLengthIncorrect();
        }
        bytes memory order = abi.encode(resolvedOrders[0]);
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(32, order), mload(order))
        }
    }
    
This interaction that calls a contract to make it call you back with data you capture from a deliberate revert is the heart of the solution. It’s a powerful technique only possible because Sim IDX listeners operate onchain.

Build the Perfect Event Onchain

After decoding the raw order, we need to refine it into a complete and useful record. The listener acts as an onchain data assembly line, composing other onchain logic for accuracy and making live calls to enrich the data. First, to ensure financial accuracy, we must account for protocol fees. We do this by porting Uniswap’s own onchain fee logic into a FeeInjector library. Our handler calls this library to inject any applicable fees into the ResolvedOrder, ensuring the final amounts are perfect.
listeners/src/Main.sol
function preExecuteFunction(PreFunctionContext memory ctx, ...) external override {
    // 1. Get the decoded order using the Quoter Pattern.
    ResolvedOrder memory order = this.quote(inputs.order.order, inputs.order.sig);

    // 2. Inject protocol fees for accuracy.
    FeeInjector._injectFees(order, IReactor(order.info.reactor).feeController());

    // 3. Emit the final, perfected event.
    emitTradesFromOrder(order, ctx.txn.call.caller());
}
Next, we enrich the data. Instead of just storing token addresses, we can get human-readable symbols and names. The listener makes a live call to each token contract to retrieve its metadata.
listeners/src/Main.sol
function emitUniswapXTrade(
    address makingToken,
    address takingToken,
    address maker,
    address taker,
    uint256 makingAmount,
    uint256 takingAmount,
    address platformContract
) internal {
    (string memory makingTokenSymbol, string memory makingTokenName, uint256 makingTokenDecimals) =
        makingToken == address(0) ? ("ETH", "Ether", 18) : getMetadata(makingToken);
    (string memory takingTokenSymbol, string memory takingTokenName, uint256 takingTokenDecimals) =
        takingToken == address(0) ? ("ETH", "Ether", 18) : getMetadata(takingToken);
    emit Swap(
        SwapData(
            uint64(block.chainid),
            txnHash,
            blockNumber(),
            uint64(block.timestamp),
            makingToken,
            makingAmount,
            makingTokenSymbol,
            makingTokenName,
            uint64(makingTokenDecimals),
            takingToken,
            takingAmount,
            takingTokenSymbol,
            takingTokenName,
            uint64(takingTokenDecimals),
            tx.origin,
            maker,
            taker,
            platformContract
        )
    );
}

function emitTradesFromOrder(ResolvedOrder memory order, address taker) internal {
    (InputToken memory input, OutputToken memory output) = getIoTokensFromOrder(order);
    emitUniswapXTrade(
        input.token, output.token, output.recipient, taker, input.amount, output.amount, address(order.info.reactor)
    );
}
This onchain assembly process that consists of decoding, correcting for fees, and enriching with live metadata creates a complete data record before it’s written to the database.

Define Your API Directly in Solidity

The final step in the Sim IDX workflow demonstrates its most powerful abstraction: your onchain event definition is your API schema. In our Listener contract, we define a SwapData struct and a Swap event. This struct contains all the decoded, fee-corrected, and enriched data points we assembled in the previous steps.
listeners/src/Main.sol
struct SwapData {
    uint64 chainId;
    bytes32 txnHash;
    uint64 blockNumber;
    uint64 blockTimestamp;
    address makerToken;
    uint256 makerAmt;
    string makerTokenSymbol;
    string makerTokenName;
    uint64 makerTokenDecimals;
    address takerToken;
    uint256 takerAmt;
    string takerTokenSymbol;
    string takerTokenName;
    uint64 takerTokenDecimals;
    address txnOriginator;
    address maker;
    address taker;
    address reactor;
}

event Swap(SwapData);

function emitTradesFromOrder(ResolvedOrder memory order, address taker) internal {
    (InputToken memory input, OutputToken memory output) = getIoTokensFromOrder(order);
    emitUniswapXTrade(
        input.token, output.token, output.recipient, taker, input.amount, output.amount, address(order.info.reactor)
    );
}
When this Swap event is emitted, Sim IDX automatically creates a swap table in your database with columns matching the fields in SwapData. It then generates a type-safe Drizzle schema for you to use in your API. The result is that your API code becomes trivially simple. You can query your swap table without writing any complex data transformation pipelines or ORM configurations.
apis/src/index.ts
import { swap } from "./db/schema/Listener";
import { db, App } from "@duneanalytics/sim-idx";

const app = App.create()

// This endpoint returns the 5 most recent swaps.
app.get("/*", async (c) => {
  try {
    const result = await db.client(c).select().from(swap).limit(5);
    return Response.json({ result });
  } catch (e) {
    console.error("Database operation failed:", e);
    return Response.json({ error: (e as Error).message }, { status: 500 });
  }
});

export default app;
From a complex, gas-optimized protocol to a simple, queryable API, the entire process is orchestrated through onchain logic. Your work as a developer is focused on Solidity.

Conclusion

You have successfully designed an indexer for one of DeFi’s most complex protocols. This guide demonstrates the power of Sim IDX’s architecture. By enabling your listener to act as an onchain contract, you can solve sophisticated indexing problems with surprisingly simple and robust code. This “Quoter” pattern can be adapted to any protocol where data is resolved through onchain logic rather than emitted in simple events.