Algorand-Global x402 Challenge-Blog-Banner-1

Is your x402 endpoint showing up in the facilitator leaderboard? How to troubleshoot if not.

As more developers build x402-powered commerce applications on Algorand, the same question keeps landing in our inbox. Payments settle. The API responds exactly as designed. And yet the endpoint never shows up in the facilitator leaderboard or the Bazaar.

The question points to one of two different issues, and it's easy to confuse them because they look similar:

  1. You're not in the Bazaar. A discovery issue. Any x402 merchant on Algorand can run into this if discovery isn't configured correctly.

  2. You're not in the Global x402 Challenge leaderboard. An attribution problem. Your payments are settling, but they aren't being counted as part of the Global x402 Challenge because the required challenge tag isn't being included.

In this blog post, we’ll help you find the answer. Part 1 explains how to determine whether the issue is related to the discovery layer. Part 2 covers what you need to check to ensure your project is on the right track for the Global x402 Challenge.

Part 1: Getting indexed in the Bazaar

Payment and discovery are two different systems. This is where the confusion starts, so let's be blunt about it:

  What it does What turns it on
Payment Verifies and settles on-chain paymentMiddleware(routes, server)
Discovery (Bazaar) Catalogs your endpoint so agents and the dashboard can find it extensions: declareDiscoveryExtension({...}) on the route

A facilitator can settle thousands of payments for you and still have nothing to publish.

Think about what a settlement actually contains. An address paid an amount on a network. That's it. There's no method in there, no input shape, no output example, nothing that describes what your endpoint does. Building a catalog entry from it would be guesswork.

The discovery extension carries that description. It rides inside your 402 Payment Required response, and the facilitator files it away when a client pays against the route.

Check the discovery API first

Before changing anything, ask the facilitator whether your resource already exists:

curl -s
"https://facilitator.goplausible.xyz/discovery/resources?includeTestnets=true&limit=1000" \

  | jq '.items[] | select(.resourceUrl | contains("your-domain"))'

If that returns nothing while payments are settling, you are dealing with a discovery gap. 

Add the discovery extension to your route

You need two changes in the resource server.

Import the helper:

import { declareDiscoveryExtension } from "@x402-avm/extensions";

Then attach it on the route you already protect with payment middleware:

const routes = {
  "GET /api/quote": {
    accepts: {
      scheme: "exact",
      network: ALGORAND_MAINNET_CAIP2,
      payTo: PAY_TO_ADDRESS,
      price: "$0.01",
      extra: {
        feePayer: FEE_PAYER,
      },
    },
    description: "Live ALGO/USD quote with confidence interval.",
    extensions: declareDiscoveryExtension({}),
  },
};

app.use(paymentMiddleware(routes, server));

An empty declareDiscoveryExtension({}) is a valid discovery extension. That alone is enough to get cataloged. Extra fields improve the listing, but they are not required for the row to appear.

You do not need a separate registerExtension call, and you do not need to touch x402ResourceServer for this. The framework binding looks for a bazaar key under extensions and registers the server-side extension on the first paid request. That behavior is the same in @x402-avm/hono, @x402-avm/express, and @x402-avm/next.

Empty config is enough

A common question is whether the discovery extension needs additional metadata before an endpoint can be catalogued. It doesn't.

A route configured with declareDiscoveryExtension({}), followed by one successful $0.01 payment, appeared in /discovery/resources straight away. You can always add richer metadata later, but an empty configuration is enough to get started.

{
  "resourceUrl": "http://localhost:8788/x402-empty-config-probe",
  "method": "GET",
  "merchantId": "Rjc2VFZIQVRHMkxKT01VT0VGTlNESk1F",
  "settleCount": 1,
  "discoveryInfo": {
    "input": { "type": "http", "queryParams": {}, "method": "GET" }
  }
}

Notice the resource URL is localhost. The facilitator builds the catalog entry from the payment payload. It does not crawl your host. That is useful during development. It is also why localhost traffic ends up in the DEV source bucket later (see Part 2) if you are trying to accumulate challenge volume.

If you want to validate the declaration before spending money, the package includes a helper:

const { declareDiscoveryExtension, validateDiscoveryExtension } = require("@x402-avm/extensions");
validateDiscoveryExtension(declareDiscoveryExtension({}).bazaar); // { valid: true }

Pass a bare {}, or an { info: { output: … } } object with no schema, and validation fails with "schema must be object or boolean".

That second case is easy to miss in production. A malformed declaration does not break payment. Verify still works. Settle still works. Funds still arrive. The catalog entry simply never appears, and nothing in the payment path tells you why.

Verify it in three commands

The discovery API is the source of truth, and updates show up as soon as a payment is processed.

# 1. Your 402 response carries the discovery extension
curl -i https://your-domain/api/quote | grep -i bazaar

# 2. Your resource exists in the catalog
curl -s "https://facilitator.goplausible.xyz/discovery/resources?includeTestnets=true&limit=1000" \
  | jq '.items[] | select(.resourceUrl | contains("your-domain"))'

# 3. Your merchant exists
curl -s "https://facilitator.goplausible.xyz/discovery/merchants?includeTestnets=true&limit=500" \
  | jq '.items[] | select(.addresses.avm == "YOUR_PAYTO_ADDRESS")'

Your merchantId in that API is the Base64 encoding of the first 24 characters of your payTo address, you can compute it locally if you want to search by id:

echo -n "YOUR_PAYTO_ADDRESS" | cut -c1-24 | tr -d '\n' | base64
Between step one and step two you need one successful payment

Seeing bazaar in the 402 response is necessary but not sufficient. Cataloging happens when a client actually pays. The resource row appears after the first successful payment against that route.

Pay the endpoint yourself once with a test client. One settlement is enough to get indexed.

Brand the merchant listing

Once discovery/resources returns your endpoint you're discoverable, and it's worth some attention on how your API looks in the Bazaar.

Add this metadata at your domain root:

  • og:site_name

  • og:title

  • og:description

  • og:image

For example:

<head> <title>Acme Market Data API</title> <meta name="description" content="Real-time market data for AI agents and applications"> <meta property="og:site_name" content="Acme"> <meta property="og:title" content="Acme Market Data API"> <meta property="og:description" content="Real-time market data for AI agents and applications"> <meta property="og:image" content="https://api.acme.com/logo.png"> </head>

Use a publicly accessible logo, then trigger one more successful payment so the facilitator picks up the refreshed metadata.

One more thing worth wiring up: Universal Receipts

Receipts are not part of discovery, but they follow the same pattern:

Settling a payment does not mint a receipt automatically. Universal Receipts are a GoPlausible facilitator extension. The receipt is created on first request:

GET https://facilitator.goplausible.xyz/api/receipt/{txId}

/receipt/{txId} is an alias of the same endpoint. That txId is the Algorand transaction ID of the settled payment, which is the transaction field of the facilitator's SettleResponse, and it reaches your client in the payment-response header.

The first request builds the receipt. The operation is idempotent, keyed by a SHA-256 of the txId, so repeated calls return the same receipt. The response is a 302 to the hosted page:

https://goplausible.xyz/api/receipt/{receiptId}

Sender, receiver, amount, asset and note all come from the “settle” the facilitator already captured. You do not pass those fields yourself.

Status codes you may hit:

Status Meaning
400 The transaction settled, though not on Algorand MainNet. Receipts are MainNet-only.
404 That transaction wasn't settled through the GoPlausible facilitator.
503 Receipts temporarily unavailable (storage binding missing).

Example:

TX=6AUOPFBS4UEFQWK5PCRGNDXYI4SEA47ZVP6VKVZ2EUKUY7SYQUZA
curl -sD - -o /dev/null "https://facilitator.goplausible.xyz/api/receipt/$TX" | grep -i location
# location: https://goplausible.xyz/api/receipt/3ca83580c6ee78d9805666ba1a221ced

The API reference calls receiptId a uuid. In practice it is the first 32 hex characters of sha256(txId):

require("crypto").createHash("sha256").update(txId).digest("hex").slice(0, 32);
// 3ca83580c6ee78d9805666ba1a221ced

If you store the transaction id, you can rebuild the receipt URL later without calling the endpoint again.
The hosted page includes settlement facts (amount, asset, network, UTC timestamp), both counterparties, the on-chain fee with an explorer link, agent owner and auth method, the note for the paid endpoint, the settling facilitator, protocol badges (TXN, x402, MPP, AP2, UCP), a QR code, and share links. Receipts are valid for 90 days.

In application code, take txId from the settle response and either mint the receipt immediately or keep the id and mint on demand. Client libraries can pull the receipt reference from the PAYMENT-RESPONSE header and store it next to request logs.

 

Part 2: The Global x402 Challenge

If you only needed a discoverable, machine-payable API, you can stop here. If you are competing in the x402 Global Challenge ($100K USD + 500K ALGO), keep going.

First, how the leaderboards actually work

"Am I on the leaderboard?" is ambiguous until you know which filter is active.

The Bazaar and the Global Challenge are both views over the same settlement dataset. Open /dashboard/leaderboards?cat=merchants and you will see filters like these:

Filter Values
NETWORK MAINNET · TESTNET
SOURCE ALL · BAZAAR · DIRECT · DEV · X402-GLOBAL-CHALLENGE
CHAIN ALL · ALGORAND · BASE · SOLANA
CURRENCY ALL · USDC · ALGO · EURD
category merchants · payers · resources · assets · networks · countries
range 24h · 7d · 30d · all

Any settled payment can show up somewhere. The Global Challenge is one SOURCE value.

That is why a merchant can rank fine with no source filter and disappear when X402-GLOBAL-CHALLENGE is selected. Check attribution explicitly:

BASE=https://facilitator.goplausible.xyz/data/leaderboards
for s in x402-global-challenge bazaar direct dev; do
  echo "src=$s"
  curl -s "$BASE?cat=merchants&limit=200&range=all&env=mainnet&src=$s" \
    | jq '.items[] | select(.address=="YOUR_PAYTO_ADDRESS") | {rank,settles,volume}'
done

If volume shows under src=direct or src=dev instead of src=x402-global-challenge, attribution is the problem. The next steps fix that.

Step 1: Register

Fill out the registration form.

The form, the official rules and the current timeline all live on the program page: Global x402 Challenge - registration page

Your endpoint has to be deployed and reachable on Algorand Mainnet, and using the GoPlausible facilitator, so volume gets tracked automatically in the public dashboard.

Step 2: Tag your resource

Add the challenge tag to extra in the route config:

extra: {
  feePayer: FEE_PAYER,
  tag: "x402-global-challenge",
},

That tag tells the facilitator to attribute activity on this endpoint to the challenge.

Without it, payments still settle and discovery still works. Challenge leaderboard attribution may not.

The tag has no effect on settlement mechanics and no effect on whether you appear in the Bazaar. It is attribution metadata only.

Attribution is written at settlement time. Payments that settled before you added the tag are not reclassified. Add the tag before you start driving real traffic.

Step 3: Know what counts as real volume

DEV is not a tag you set. The facilitator assigns it for localhost traffic and for repeating loop patterns (pings, bots, retry storms).

Two practical consequences:

Localhost settlements can still get discovery rows, as shown in Part 1, but that volume is filed under DEV and does not help challenge totals.

Self-payment loops are treated the same way. A cron job, health check, or internal orchestrator that repeatedly pays your own route looks like the traffic DEV detection is built to catch. The payments can be real Mainnet settlements and still land in the developer bucket.

This is the failure mode that shows up after people have already registered, tagged, and deployed. Settlements look fine. Challenge volume does not move. Run the source loop above and see which bucket your address is in before assuming the leaderboard is wrong.

Step 4: Drive real volume

Leaderboard volume is measured over an unannounced window, so last-day spikes are not a reliable strategy.

Judging covers real usage, use-case quality, technical execution, and long-term potential. The official rules have the full criteria.

If discovery is on, the challenge tag is present, and you are generating real Mainnet usage through GoPlausible, you are competing on the right dataset.

Happy hacking!