Skip to main content

Build a marketplace Application

In this tutorial we'll be building a simple NFT Marketplace application, where users are able to deposit a unique token to be sold at a fixed price, then other users are able to purchase and withdraw these purchased tokens to their wallet.

This Tutorial is built using an Object oriented approach and aims to cover, application creation, Notice, Voucher and Report generation, we'll also be decoding and consuming the payload passed alongside advance and inspect requests.

How the Marketplace Works

Now that the basic setup is done, we can focus on how the marketplace application actually works. This tutorial uses an Object-Oriented approach. This means we will create a main Marketplace object that stores the application state and provides methods to update or retrieve that state. Then we'll build other handler functions that utilizes other helper functions to decode user requests then call the appropriate method to update the marketplace object.

For simplicity, our marketplace will support only:

  • One specific ERC-721 (NFT) contract: This is the only collection users can list.

  • One specific ERC-20 token: This is the token users will use to purchase NFTs.

Advance Requests

1. Depositing / Listing NFTs

  • A user sends an NFT to the Cartesi application through the ERC-721 Portal.
  • When the application receives the deposit payload, it automatically lists the NFT for sale at a fixed price.
  • The price is the same for every NFT in this tutorial to keep things simple.

2. Depositing Payment Tokens

  • Users can then send the marketplace’s payment token to the application through the ERC-20 Portal.
  • The application keeps track of how many tokens each user has deposited.

3. Buying an NFT

  • When a user decides to buy an NFT listed on the marketplace, the application checks:

    • Does the user have enough deposited tokens?
    • Is the NFT still listed?
  • If the transaction is valid, the marketplace transfers the payment token to the seller then creates a voucher that sends the purchased NFT to the buyer.

Inspect Requests

The Inspect route will support three simple read-only queries:

1. get_user_erc20_balance

Description: Shows how many tokens a user has stored in the marketplace.

Input: User's address

Output: Amount of ERC-20 tokens deposited.

2. get_token_owner

Description: Returns the current owner of a specific NFT.

Input: Token ID

Output: Address of current token owner.

3. get_all_listed_tokens

Description: Shows all NFTs currently listed for sale.

Input: (none)

Output: Array of Token IDs currently listed for sale.

Set up your environment

To create a template for your project, we run the below command, based on your language of choice:

cartesi create marketplace --template javascript

Install Project Dependencies

Since this project would be covering hex payload encoding and decoding, as well as Output (Voucher, Notice, Report) generation, it's important that we install the necessary dependencies to aid these processes.

 npm add viem ethers

NOTE:: For python developers, add the below snippet to line 26 of your Dockerfile. It should come immediately after the line COPY ./requirements.txt ..

This command would help install essential compilers to help compile some dependencies we'll be using.

# Install build dependencies for compiling native extensions
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
python3-dev && \
rm -rf /var/lib/apt/lists/*

Implement the Application Logic

Based on the programming language you selected earlier, copy the appropriate code snippet, then paste in your local entry point file (dapp.py or src/main.rs or src/index.js), created in the setup step:

import { encodeFunctionData, getAddress, toHex, zeroHash } from "viem";
import { ethers } from "ethers";

const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
console.log("HTTP rollup_server url is " + rollup_server);

const asBigInt = (v) => (typeof v === "bigint" ? v : BigInt(v));
const normAddr = (a) => a.toLowerCase();
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
const erc721Abi = [
{
name: "transferFrom",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "tokenId", type: "uint256" },
],
outputs: [],
},
];

class Storage {
constructor(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price, dappAddressRelay) {
this.erc721_portal_address = erc721_portal_address;
this.erc20_portal_address = erc20_portal_address;
this.erc721_token = erc721_token;
this.erc20_token = erc20_token;
this.application_address = normAddr(ZERO_ADDRESS);
this.list_price = list_price;
this.dappAddressRelay = dappAddressRelay;

this.listed_tokens = [];
this.users_erc20_token_balance = new Map();
this.user_erc721_token_balance = new Map();
this.erc721_id_to_owner_address = new Map();
}

getListedTokens() {
return this.listed_tokens;
}

getAppAddress() {
return this.application_address;
}

setAppAddress(app_addr) {
this.application_address = normAddr(app_addr);
}

getUserERC721TokenBalance(userAddress) {
return this.user_erc721_token_balance.get(normAddr(userAddress));
}

getERC721TokenOwner(tokenId) {
return this.erc721_id_to_owner_address.get(asBigInt(tokenId));
}

getUserERC20TokenBalance(userAddress) {
return this.users_erc20_token_balance.get(normAddr(userAddress)) || 0n;
}

increaseUserBalance(userAddress, amount) {
const addr = normAddr(userAddress);
const current = this.users_erc20_token_balance.get(addr) || 0n;
this.users_erc20_token_balance.set(addr, current + BigInt(amount));
}

async reduceUserBalance(userAddress, amount) {
const addr = normAddr(userAddress);
const current = this.users_erc20_token_balance.get(addr);

if (current === undefined || current < BigInt(amount)) {
await emitReport(`User ${addr} record not found`);
console.log("User balance record not found");
return;
}
this.users_erc20_token_balance.set(addr, current - BigInt(amount));
}

depositERC721Token(userAddress, tokenId) {
const addr = normAddr(userAddress);
const tid = asBigInt(tokenId);
this.erc721_id_to_owner_address.set(tid, addr);

let previous_owner = this.getERC721TokenOwner(tid);

if (normAddr(previous_owner) === normAddr(ZERO_ADDRESS)) {
this.changeERC721TokenOwner(tid, addr, normAddr(ZERO_ADDRESS));
} else {
const tokens = this.user_erc721_token_balance.get(addr) || [];
if (!tokens.some((t) => t === tid)) tokens.push(tid);
this.user_erc721_token_balance.set(addr, tokens);
}
}

listTokenForSale(tokenId) {
const tid = asBigInt(tokenId);
if (!this.listed_tokens.some((id) => id === tid)) this.listed_tokens.push(tid);
}

changeERC721TokenOwner(tokenId, newOwner, oldOwner) {
const tid = asBigInt(tokenId);
const newAddr = normAddr(newOwner);
const oldAddr = normAddr(oldOwner);

this.erc721_id_to_owner_address.set(tid, newAddr);

const newOwnerTokens = this.user_erc721_token_balance.get(newAddr) || [];
if (!newOwnerTokens.some((id) => id === tid)) newOwnerTokens.push(tid);
this.user_erc721_token_balance.set(newAddr, newOwnerTokens);

const oldOwnerTokens = this.user_erc721_token_balance.get(oldAddr) || [];
this.user_erc721_token_balance.set(oldAddr, oldOwnerTokens.filter((id) => id !== tid));
}

async purchaseERC721Token(buyerAddress, erc721TokenAddress, tokenId) {
const tid = asBigInt(tokenId);

if (!storage.listed_tokens.includes(tokenId)) {
await emitReport(`Token ${erc721TokenAddress} with id ${tid} is not for sale`);
console.log("Token is not for sale");
return false;
}
const owner = this.getERC721TokenOwner(tid);
if (!owner) {
await emitReport(`Token owner for token ${erc721TokenAddress} with id ${tid} not found`);
console.log("Token owner not found");
return false;
}

await this.reduceUserBalance(buyerAddress, storage.list_price);
this.increaseUserBalance(owner, storage.list_price);
this.changeERC721TokenOwner(tid, ZERO_ADDRESS, owner);
this.listed_tokens = this.listed_tokens.filter((id) => id !== tid);
}
}

async function handleERC20Deposit(depositorAddress, amountDeposited, tokenAddress) {
if (normAddr(tokenAddress) === normAddr(storage.erc20_token)) {
try {
storage.increaseUserBalance(depositorAddress, amountDeposited);
console.log("Token deposit processed successfully");
} catch (error) {
console.log("error, handing ERC20 deposit ", error)
await emitReport(error.toString());
}
} else {
console.log("Unsupported token deposited");
await emitReport("Unsupported token deposited");
}
}

async function handleERC721Deposit(depositorAddress, tokenId, tokenAddress) {
if (normAddr(tokenAddress) === normAddr(storage.erc721_token)) {
try {
storage.depositERC721Token(depositorAddress, tokenId);
storage.listTokenForSale(tokenId);
console.log("Token deposit and Listing processed successfully");
emitNotice("Token ID: " + tokenId + " Deposited by User: " + depositorAddress)
} catch (error) {
console.log("error, handing ERC721 deposit ", error)
await emitReport(error.toString());
}
} else {
console.log("Unsupported token deposited");
await emitReport("Unsupported token deposited");
}
}

function erc721TokenDepositParse(payload) {
try {
const erc721 = getAddress(ethers.dataSlice(payload, 0, 20));
const account = getAddress(ethers.dataSlice(payload, 20, 40));
const tokenId = parseInt(ethers.dataSlice(payload, 40, 72));

return {
token: erc721,
receiver: account,
amount: tokenId,
};
} catch (e) {
emitReport(`Error parsing ERC721 deposit: ${e}`);
}
}

function erc20TokenDepositParse(payload) {
try {
let inputData = [];
inputData[0] = ethers.dataSlice(payload, 0, 1);
inputData[1] = ethers.dataSlice(payload, 1, 21);
inputData[2] = ethers.dataSlice(payload, 21, 41);
inputData[3] = ethers.dataSlice(payload, 41, 73);

if (!inputData[0]) {
emitReport("ERC20 deposit unsuccessful: invalid payload");
throw new Error("ERC20 deposit unsuccessful");
}
return {
token: getAddress(inputData[1]),
receiver: getAddress(inputData[2]),
amount: BigInt(inputData[3]),
};
} catch (e) {
emitReport(`Error parsing ERC20 deposit: ${e}`);
}
}

async function extractField(json, field) {
const value = json[field];

if (typeof value === "string" && value.trim() !== "") {
return value;
} else {
await emitReport(`Missing or invalid ${field} field in payload`);
console.log(`Missing or invalid ${field} field in payload`);
}
}

async function handlePurchaseToken(callerAddress, userInput) {
try {
const erc721TokenAddress = normAddr(storage.erc721_token);
const tokenId = BigInt(await extractField(userInput, "token_id"));

try {
if (await storage.purchaseERC721Token(callerAddress, erc721TokenAddress, tokenId) === false) {
return;
}
console.log("Token purchased successfully");
let voucher = structureVoucher({
abi: erc721Abi,
functionName: "transferFrom",
args: [storage.application_address, callerAddress, tokenId],
destination: storage.erc721_token,
})
emitVoucher(voucher);
} catch (e) {
await emitReport(`Failed to purchase token: ${e.message}`);
console.log(`Failed to purchase token: ${e.message}`);
}
} catch (error) {
console.log("error purchasing token: ", error);
await emitReport(error.toString());
}
}

function hexToString(hex) {
if (typeof hex !== "string") return "";
if (hex.startsWith("0x")) hex = hex.slice(2);
return Buffer.from(hex, "hex").toString("utf8");
}

async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));

const sender = data["metadata"]["msg_sender"];

const payload = hexToString(data.payload);

if (normAddr(sender) == normAddr(storage.dappAddressRelay)) {
if (normAddr(storage.application_address) == normAddr(ZERO_ADDRESS)) {
storage.setAppAddress(data.payload);
}
} else if (normAddr(sender) == normAddr(storage.erc20_portal_address)) {
let { token, receiver, amount } = erc20TokenDepositParse(data.payload);
await handleERC20Deposit(receiver, amount, token);
} else if (normAddr(sender) == normAddr(storage.erc721_portal_address)) {
let { token, receiver, amount } = erc721TokenDepositParse(data.payload)
await handleERC721Deposit(receiver, asBigInt(amount), token);
} else {
const payload_obj = JSON.parse(payload);
let method = payload_obj["method"];
switch (method) {
case "purchase_token": {
await handlePurchaseToken(sender, payload_obj);
break;
}
default: {console.log("Unwupported method called!!"); emitReport("Unwupported method called!!")}
}
}
return "accept";
}

async function handle_inspect(data) {
console.log("Received inspect request data " + JSON.stringify(data));

const payload = hexToString(data.payload).trim();

let payload_arry = payload.split("/");

switch (payload_arry[0]) {
case "get_user_erc20_balance": {
const user_address = payload_arry[1];
const bal = storage.getUserERC20TokenBalance(normAddr(user_address));
await emitReport(`User: ${user_address} Balance: ${bal.toString()}`);
break;
}
case "get_token_owner": {
const token_id = BigInt(payload_arry[1]);
const token_owner = storage.getERC721TokenOwner(token_id);
await emitReport(`Token_id: ${token_id.toString()} owner: ${token_owner ?? "None"}`);
break;
}
case "get_all_listed_tokens": {
const listed_tokens = storage.getListedTokens();
await emitReport(`All listed tokens are: ${listed_tokens.map(String).join(",")}`);
break;
}
default: {
console.log("Unsupported method called!!");
await emitReport("Unsupported inspect method");
}
}
return "accept";
}

function stringToHex(str) {
if (typeof str !== "string") {
console.log("stringToHex: input must be a string");
}
const utf8 = Buffer.from(str, "utf8");
return "0x" + utf8.toString("hex");
}

function structureVoucher({ abi, functionName, args, destination, value = 0n }) {
const payload = encodeFunctionData({
abi,
functionName,
args,
});

return {
destination,
payload
}
}

const emitVoucher = async (voucher) => {
try {
await fetch(rollup_server + "/voucher", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(voucher),
});
} catch (error) {
emitReport("error emitting Voucher");
console.log("error emitting voucher: ", error)
}
};

const emitReport = async (payload) => {
let hexPayload = stringToHex(payload);
try {
await fetch(rollup_server + "/report", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload: hexPayload }),
});
} catch (error) {
console.log("error emitting report: ", error)
}
};

const emitNotice = async (payload) => {
let hexPayload = stringToHex(payload);
try {
await fetch(rollup_server + "/notice", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload: hexPayload }),
});
} catch (error) {
emitReport("error emitting Notice");
console.log("error emitting notice: ", error)
}
};

var handlers = {
advance_state: handle_advance,
inspect_state: handle_inspect,
};

let erc721_portal_address = "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87";
let erc20_portal_address = "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB";
let erc20_token = "0x92C6bcA388E99d6B304f1Af3c3Cd749Ff0b591e2";
let erc721_token = "0xc6582A9b48F211Fa8c2B5b16CB615eC39bcA653B";
let dappAddressRelay = "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE";
let list_price = BigInt("100000000000000000000");

var storage = new Storage(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price, dappAddressRelay);

var finish = { status: "accept" };

(async () => {
while (true) {
const finish_req = await fetch(rollup_server + "/finish", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "accept" }),
});

console.log("Received finish status " + finish_req.status);

if (finish_req.status == 202) {
console.log("No pending rollup request, trying again");
} else {
const rollup_req = await finish_req.json();
var handler = handlers[rollup_req["request_type"]];
finish["status"] = await handler(rollup_req["data"]);
}
}
})();

Build and Run your Application

Once you have your application logic written out, the next step is to build the application, this is done by running the below commands using the Cartesi CLI:

cartesi build
  • Expected Logs:
user@user-MacBook-Pro marketplace % cartesi build
View build details: docker-desktop://dashboard/build/multiarch/multiarch0/vzzzuxvcznba66icpyk3wyde9

What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview
copying from tar archive /tmp/input

.
/ \
/ \
\---/---\ /----\
\ X \
\----/ \---/---\
\ / CARTESI
\ / MACHINE
'

[INFO rollup_http_server] starting http dispatcher service...
[INFO rollup_http_server::http_service] starting http dispatcher http service!
[INFO actix_server::builder] starting 1 workers
[INFO actix_server::server] Actix runtime found; starting in Actix runtime
[INFO rollup_http_server::dapp_process] starting dapp: python3 dapp.py
INFO:__main__:HTTP rollup_server url is http://127.0.0.1:5004
INFO:__main__:Sending finish

Manual yield rx-accepted (0x100000000 data)
Cycles: 3272156820
3272156820: 3903552ee499ef4a10b2c8ffba6b8d49088a0a8b9137b8d10be359910080432a
Storing machine: please wait

The build command compiles your application then builds a Cartesi machine that contains your application.

This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. wound next be started by running the command:

cartesi run

If the run command is successful, you should see logs similar to this:

user@user-MacBook-Pro marketplace % cartesi run
Attaching to prompt-1, validator-1
validator-1 | 2025-11-24 21-27-29 info remote-cartesi-machine pid:109 ppid:68 Initializing server on localhost:0
prompt-1 | Anvil running at http://localhost:8545
prompt-1 | GraphQL running at http://localhost:8080/graphql
prompt-1 | Inspect running at http://localhost:8080/inspect/
prompt-1 | Explorer running at http://localhost:8080/explorer/
prompt-1 | Bundler running at http://localhost:8080/bundler/rpc
prompt-1 | Paymaster running at http://localhost:8080/paymaster/
prompt-1 | Press Ctrl+C to stop the node

Interacting with your Marketplace Application

Once your Marketplace application is up and running (via cartesi run), you can interact with it in two main ways — either by sending on-chain transactions through the local Anvil network (Advance requests) or by making HTTP requests directly to the Rollups HTTP server’s Inspect endpoint (Inspect requests).

In this section, we’ll focus on using the Cartesi CLI to send Advance requests, since it provides a much simpler and faster way to test your application locally.

1. Mint an ERC-721 Token and Grant Approval

With your Marketplace application now deployed, the first step is to mint the NFT you plan to list and grant approval for it to be transferred via the ERC-721 portal. Since our app uses the test ERC-721 and ERC-20 contracts automatically deployed by the CLI, you can use the commands below to mint your token and set the necessary approvals.

  • Mint token ID 1:
cast send 0xc6582A9b48F211Fa8c2B5b16CB615eC39bcA653B \
"safeMint(address, uint256, string)" \
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1 "" \
--rpc-url http://localhost:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

This command calls the safeMint function in the testNFT contract deployed by the CLI, minting token ID 1 to the address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266.

  • Grant approval to the ERC721-Portal:

Before an NFT can be deposited into the application, the portal contract must have permission to transfer it on behalf of the owner. Use the following command to grant that approval:

cast send 0xc6582A9b48F211Fa8c2B5b16CB615eC39bcA653B \
"setApprovalForAll(address,bool)" \
0xab7528bb862fB57E8A2BCd567a2e929a0Be56a5e true \
--rpc-url http://localhost:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

2. Relay Application address

Before any interaction with the application, it's important that the application logic is aware of Its contract address, as this is important during voucher generation. To register the application address, we use the Cartesi CLI to trigger a call from the DappAddressRelayer contract to our application, this can be achieved through the below process:

cartesi send

Run the command above, then choose Send DApp address input to the application. as the action. Press Enter twice to accept Foundry as the interaction chain. Next, press Enter to use the default RPC URL, then press Enter three more times to select Mnemonic as the authentication method and confirm both the default mnemonic and the default wallet address. Finally, press Enter one last time to confirm the displayed application address.

At this point, the CLI will initiate the request and forward the application’s address to your Cartesi application. The application codebase already includes logic to verify the caller and ensure that the received application address is correctly recognized as the dappAddressRelayer.

3. Deposit NFT with ID 1 to the Marketplace [advance request]

Now that the NFT is minted and approved, it’s time to list it on the marketplace. We’ll do this by depositing it using the Cartesi CLI:

cartesi send erc721

The CLI will prompt you for the chain, RPC URL, Wallet, Mnemonic, Account and Application address, for those sections you can simply keep hitting enter to use the default values, when the CLI Prompts for token address, you enter the address 0xc6582A9b48F211Fa8c2B5b16CB615eC39bcA653B, token ID (enter 1). Under the hood, the CLI transfers the NFT from your address to the ERC-721 portal, which then sends the deposit payload to your application.

Once the deposit succeeds, the terminal running your application should show logs similar to:

validator-1  | [INFO  rollup_http_server::http_service] Received new request of type ADVANCE
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 601 "-" "node" 0.001152
validator-1 | Received finish status 200
validator-1 | Received advance request data {"metadata":{"msg_sender":"0x237f8dd094c0e47f4236f12b4fa01d6dae89fb87","epoch_index":0,"input_index":2,"block_number":298,"timestamp":1764095710},"payload":"0xc6582a9b48f211fa8c2b5b16cb615ec39bca653bf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}
validator-1 | Token deposit and Listing processed successfully
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /notice HTTP/1.1" 201 11 "-" "node" 0.000704
validator-1 | [INFO rollup_http_server::http_service] Received new request of type INSPECT
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 98 "-" "node" 0.000960
validator-1 | Received finish status 200

4. View All NFTs Listed on the Marketplace [inspect request]

After depositing, your NFT is automatically listed. To verify this, you can query the marketplace for all listed tokens using an Inspect request:

curl http://localhost:8080/inspect/get_all_listed_tokens

This call returns a hex payload containing a list of all listed tokens on the marketplace.

{"status":"Accepted","reports":[{"payload":"0x416c6c206c697374656420746f6b656e73206172653a2031"}],"processed_input_count":6}

The payload hex 0x416c6...2653a2031 when decoded, returns All listed tokens are: [1]. Thereby confirming that the token with Id 1 has successfully been listed.

5. Deposit ERC20 token for making purchases [advance request]

With the NFT successfully listed for sale, it's time to attempt to purchase this token, but before we do that, we'll need first deposit the required amount of tokens to purchase the listed NFT. Since our marketplace lists all NFT's at the price of 100 testTokens we'll be transferring 100 tokens to the new address we'll be using to purchase, before proceeding with the purchase.

  • Transfer required tokens to purchase address.
cast send 0x92C6bcA388E99d6B304f1Af3c3Cd749Ff0b591e2 \
"transfer(address,uint256)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 100000000000000000000 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://localhost:8545
  • Deposit 100 tokens to the marketplace application.
  cartesi send erc20

The CLI will prompt you for the interaction chain, select Foundry, then press Enter twice to accept the default RPC URL. Next, choose Mnemonic as the authentication method. When asked to select an account, choose the second address in the list: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8.

After that, select the default application address. When prompted for the ERC-20 token address, enter: 0x92C6bcA388E99d6B304f1Af3c3Cd749Ff0b591e2.

Finally, enter the amount of tokens you want to deposit (100). The CLI will then automatically handle the necessary approvals and complete the deposit into your application.

On a successful deposit our application should return logs that look like this:

validator-1  | [INFO  rollup_http_server::http_service] Received new request of type ADVANCE
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 347 "-" "node" 0.001024
validator-1 | Received finish status 200
validator-1 | Received advance request data {"metadata":{"msg_sender":"0x9c21aeb2093c32ddbc53eef24b873bdcd1ada1db","epoch_index":0,"input_index":4,"block_number":305,"timestamp":1764095745},"payload":"0x0192c6bca388e99d6b304f1af3c3cd749ff0b591e270997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000821ab0d4414980000"}
validator-1 | Token deposit processed successfully
validator-1 | [INFO rollup_http_server::http_service] Received new request of type ADVANCE
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 285 "-" "node" 0.001024
validator-1 | Received finish status 200

6. Purchase Token with ID 1 [advance request]

Now that the buyer has deposited funds, we can proceed to purchase the NFT. To do this we make an advance request to the application using the Cartesi CLI by running the command:

  cartesi send generic

The CLI will prompt you for the interaction chain, select Foundry, then press Enter twice to accept the default RPC URL. Next, choose Mnemonic as the authentication method. When asked to select an account, choose the second address in the list: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8.

After that, select the default application address. When prompted for Input method, select String encoding.

Finally, pass the below Json object to the terminal as the input:

{"method": "purchase_token", "token_id": "1"}

This command notifies the marketplace that the address 0x7099797....0d17dc79C8 which initially deposited 100 tokens, would want to purchase token ID 1, The marketplace proceeds to run necessary checks like verifying that the token is for sale, and that the buyer has sufficient tokens to make the purchase, after which it executes the purchase and finally emits a voucher that transfers the tokens to the buyer's address. On a successful purchase, you should get logs similar to the below.

validator-1  | [INFO  rollup_http_server::http_service] Received new request of type ADVANCE
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 285 "-" "node" 0.001024
validator-1 | Received finish status 200
validator-1 | Received advance request data {"metadata":{"msg_sender":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","epoch_index":0,"input_index":5,"block_number":306,"timestamp":1764095750},"payload":"0x7b226d6574686f64223a2270757263686173655f746f6b656e222c22746f6b656e5f6964223a2234227d"}
validator-1 | Token purchased successfully
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /voucher HTTP/1.1" 201 11 "-" "node" 0.000896
validator-1 | [INFO rollup_http_server::http_service] Received new request of type ADVANCE
validator-1 | [INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 285 "-" "node" 0.001024
validator-1 | Received finish status 200

7. Recheck NFTs Listed on the Marketplace [inspect request]

Finally, we can confirm that the purchased NFT has been removed from the listings by running the inspect query again:

curl http://localhost:8080/inspect/get_all_listed_tokens

This call returns a hex payload like below:

  {"status":"Accepted","exception_payload":null,"reports":[{"payload":"0x416c6c206c697374656420746f6b656e73206172653a20"}],"processed_input_count":7}

The payload hex 0x416c6...6172653a20 when decoded, returns All listed tokens are: . Thereby verifying that the token with Id 1 has successfully been sold and no longer listed for sale in the marketplace.

Conclusion

Congratulations!!!

You’ve successfully built and interacted with your own Marketplace application on Cartesi.

This example covered essential Cartesi concepts such as routing, asset management, voucher generation, and the use of both Inspect and Advance routes.

For a more detailed version of this code, you can check the marketplace folder for your selected language in this repository

We use cookies to ensure that we give you the best experience on our website. By using the website, you agree to the use of cookies.