EVM to Cardinal communication is an experimental feature. This API is subject to change.

This documentation assumes knowledge of the EVM and Solidity. If you are unfamiliar with these concepts, we recommend checking out the Ethereum documentation on the EVM for a comprehensive overview.

Sending messages and queries to Cardinal is normally done through clients interacting with the game relayer, Nakama, however messages and queries can also be sent via EVM smart contracts.

The World Engine EVM shard provides a precompile, called Router, that enables smart contracts to read data from and send messages to Cardinal game shards.

For messages, Router operates asynchronously, which requires a callback mechanism to be used. Messages are only forwarded from Router’s sendMessage method once the EVM transaction that called it successfully executes. Shortly after, the result will be available for consumption via Router’s messageResult method.

Queries are synchronous and do not require a callback.


Before using the Router, a user must first authorize the contract address that will be utilizing the Router. This will let the linked address act on behalf of the persona tag. This process is done through the AuthorizePersonaAddress system, built into Cardinal.




    "personaTag" : "cool_mage20"
    "address" : "0xeF68bBDa508adF1FC4589f8620DaD9EDBBFfA0B0"

Precompile Address


Precompile Interface

interface IRouter {
    function sendMessage(bytes memory message, string memory messageID, string memory namespace)
        returns (bool);

    function messageResult(string memory txHash)
        returns (bytes memory, string memory, uint32);

    function query(bytes memory request, string memory resource, string memory namespace)
        returns (bytes memory);

Method Reference


The sendMessage method enables smart contracts to send messages to the game shard specified by the given namespace. The messageID must be the fully qualified name of the message. For Cardinal game shards, the fully qualified name is of the form “groupName.MessageName”. For example, the create-persona message’s fully qualified name is persona.create-persona. Messages without a specified custom group in Cardinal have a default group called “game”.


messagebytesABI encoded message struct.
messageIDstringFully qualified message identifier.
namespacestringThe namespace of the game shard to send the message to.

Return Value

boolIndicates the success of the message being queued for sending.


The messageResult method enables smart contracts to retrieve the result of a cross-shard message.


txHashThe hash of the EVM transaction that triggered the sendMessage call.

Return Values

bytesABI encoded result struct.
stringError string, if any. Empty string means no error.
uint32A numeric value representing the result status.


Success0Transaction executed successfully.
TxFailed1Transaction execution failed.
NoResult2No result available for the operation.
ServerUnresponsive3Game Shard is unresponsive.
Unauthorized4Unauthorized access or action.
UnsupportedTransaction5Transaction type is not supported.
InvalidFormat6Data or format is invalid.
ConnectionError100Error in establishing or maintaining a connection.
ServerError101Internal error with the game shard.


The query router method enables smart contracts to read data from a game shard specified by the given namespace.


requestbytesABI encoded query request struct.
resourcestringThe resource identifier for the query. (query name)
namespacestringThe namespace of the game shard.

Return Values

bytesABI encoded query result struct.

Structuring Messages and Query Requests

The query and sendMessage Router methods both take in a parameter of type bytes. These bytes must be formed by ABI encoding a struct with the exact same field types as their Cardinal game shard counterparts.

For example:

If the Cardinal game shard defines this message input:

type SendEnergy struct {
    PlanetTo string
    PlanetFrom string
    Amount uint64

Then the Solidity counterpart should be exactly:

struct SendEnergy {
    string PlanetTo;
    string PlanetFrom;
    uint64 Amount;


contract GameAgent {
    IRouter internal router;
    string internal namespace;

    constructor(address _precompileAddr, string _namespace) {
        router = IRouter(_precompileAddr);
        namespace = _namespace;

    struct SendEnergy {
        string PlanetTo;
        string PlanetFrom;
        uint64 Amount;

    struct SendEnergyResult {
        bool Success;

    string internal sendEnergyID = "game.send-energy";

    struct QueryPlanetEnergyRequest {
        string PlanetID;

    struct QueryPlanetEnergyResponse {
        uint64 Energy;

    string internal queryPlanetEnergyID = "planet-energy";

    function sendSomeEnergy(string calldata _planetTo, string calldata _planetFrom, uint64 _amount) public {
        SendEnergy memory sendEnergyMsg = SendEnergy(_planetTo, _planetFrom, _amount);
        bytes memory encoded = abi.encode(sendEnergyMsg);
        bool ok = router.sendMessage(encoded, sendEnergyID, namespace);
        if (!ok) {
            revert("router failed to send message");

    function sendEnergyResult(string calldata _txHash) public returns (SendEnergyResult memory, string memory, uint32) {
        (bytes memory txResult, string memory errMsg, uint32 code) =  router.messageResult(_txHash);
        SendEnergyResult memory res = abi.decode(txResult, (SendEnergyResult));
        return (res, errMsg, code);

    function queryPlanetEnergy(string calldata _planetID) public returns (uint64) {
        QueryPlanetEnergyRequest memory q = QueryPlanetEnergyRequest(_planetID);
        bytes memory queryBz = abi.encode(q);
        bytes memory bz = router.query(queryBz, queryPlanetEnergyID, namespace);
        QueryPlanetEnergyResponse memory res = abi.decode(bz, (QueryPlanetEnergyResponse));
        return res.Energy;