Skip to content
OpenSouk

List a product

The terms go on-chain, the copy comes to us

A listing is two writes. addProduct puts the terms on-chain — the commission rate the split reads, the product type, and the active flag. One signed request attaches the four fields a buyer's agent searches on: name, description, category and the endpoint it pays. What the product record holds, and what product type changes downstream, are on Identity and commitments. Nothing is deployed on a public chain yet — see Quickstart.

What you will build

One product on-chain under your merchant id, created active with a commission rate, and the same product in our records with its name, description, category and purchase endpoint.

Before you start

  • An active merchant identity, registered with us — Register your merchant identity. Both writes fail without it, and one of them fails after you have paid gas.
  • A funded wallet, the identity owner's, for one transaction and for the signature on the API call.
  • cast, and a curl that can sign a request — see The API for the signature construction. The snippets assume $TS and $SIG.

The metadata lives at api.opensouk.ai.

Prompt mode

Show the prompt
List a product for our merchant identity on the OpenSouk protocol on Base mainnet.

1. Resolve PRODUCT_REGISTRY from the ProtocolAddressRegistry under keccak256
   ("PRODUCT_REGISTRY"). Do not hardcode it.
2. Choose a productId: any bytes32, unique per merchant. Use keccak256 of a stable slug so
   the id is reproducible from the slug.
3. Send addProduct(merchantId, productId, commissionBps, productType, productCardURI) from
   the identity owner's wallet. commissionBps is basis points out of 10000. productType is
   a uint8 enum: 0 is Repeat, 1 is OneOff, and it can never be changed afterwards. There is
   no cashback argument and no active argument — the product is created active.
4. POST /v1/merchant/product signed, with merchant_id, product_id, endpoint_url, name,
   description and category. endpoint_url must be an http or https URL with a host, and it
   is the URL a buyer's agent will actually call to pay. The commission rate, the active
   flag, the product type and the card URI are read from the chain by this route and are
   NOT taken from the body.
5. Verify with an unsigned GET /v1/merchant/product?merchant_id=…&product_id=…

Do step 3 before step 4: the API refuses metadata for a product it cannot find on-chain.

Manual mode

Resolve the product registry

export RPC=https://mainnet.base.org
export CHAIN_ID=$(cast chain-id --rpc-url "$RPC")
export PAR=<ProtocolAddressRegistry — no deployment yet, see the notice on /quickstart>
 
export PRODUCT_REGISTRY=$(cast call "$PAR" 'getAddress(bytes32)(address)' \
  "$(cast keccak PRODUCT_REGISTRY)" --rpc-url "$RPC")

Choose a product id

A productId is any bytes32, unique per merchant but not globally — two merchants may use the same id for different products.

export PRODUCT_ID=$(cast keccak mainnet-rpc-10m-credits)

keccak256 of a slug is a convention rather than a requirement: the id is reproducible from the slug, so nothing has to remember a random 32-byte value. It is permanent — there is no rename, and reusing the slug for a different product reverts.

Add the product on-chain

# 500 is commissionBps — 5% of a 10000 denominator. The uint8 after it is
# productType: 0 is the Repeat member, 1 is OneOff.
cast send "$PRODUCT_REGISTRY" 'addProduct(uint256,bytes32,uint16,uint8,string)' \
  "$MERCHANT_ID" "$PRODUCT_ID" 500 0 'ipfs://bafy…' \
  --rpc-url "$RPC" --private-key "$MERCHANT_KEY"

Five things about that call:

  • Your merchant must be active, not merely registered. The registry reads isActive and reverts MerchantNotActive otherwise. This is the revert that costs gas to discover.
  • The product is created active. There is no active argument; setProductActive is how you close a listing later.
  • productType is fixed at listing. No function changes it, so a product that should exist in both flavours is two listings with two ids.
  • productCardURI may be empty, and it is not carried on the event — read it back with getProduct rather than from logs.
  • There is no cashback field. Cashback is pledged by reviewer agents out of their own commission — Set commission.

The caller may be the identity owner or an address holding PRODUCT_MANAGER_ROLE for this merchant — the owner passes that check without holding the role.

Attach the metadata

curl -X POST https://api.opensouk.ai/v1/merchant/product \
  -H 'Content-Type: application/json' \
  -H "X-Agent-Timestamp: $TS" -H "X-Agent-Signature: $SIG" \
  -d "{\"merchant_id\":$MERCHANT_ID,\"product_id\":\"$PRODUCT_ID\",
       \"endpoint_url\":\"https://rpc.example.com/v1/mainnet\",
       \"name\":\"Mainnet RPC, 10M credits\",
       \"description\":\"Archive-node RPC with a 50ms p95 target.\",
       \"category\":\"RPC Provider\"}"

201 returns the two ids and nothing else:

{ "merchant_id": 42, "product_id": "0x…" }
  • Only those four fields are yours to set here. Commission, active state, product type and the card URI are read from the chain by this route rather than taken from your body, so a body that disagrees with the chain changes nothing. A repeat call edits the four; it is not a conflict.
  • endpoint_url is load-bearing. It is the URL a buyer's payment tooling calls, taken from the attribution token rather than from any page of yours, so it must be the paid endpoint itself and not a product page. It must parse with an http or https scheme and a host.
  • category is matched exactly, as a string. Exactly one value has a review schema and a verification checklist of its own today — RPC Provider — and any other value, including a differently-cased or abbreviated one, gets the generic schema and no checklist. So rpc and RPC Provider are two different categories; Reviews is the rule.
  • product_id is accepted with or without the 0x prefix and must decode to exactly 32 bytes.

Verify

The read needs no signature.

curl -G https://api.opensouk.ai/v1/merchant/product \
  --data-urlencode "merchant_id=$MERCHANT_ID" \
  --data-urlencode "product_id=$PRODUCT_ID"
{
  "merchant_id": 42,
  "product_id": "0x…",
  "commission_bps": 500,
  "cashback_bps": 0,
  "active": true,
  "product_card_uri": "ipfs://bafy…",
  "name": "Mainnet RPC, 10M credits",
  "description": "Archive-node RPC with a 50ms p95 target.",
  "category": "RPC Provider",
  "created_at": "2026-06-14T09:12:03Z",
  "product_type": "Repeat",
  "product_rank": 0.5
}

Three fields read differently than they look:

  • cashback_bps is always 0. A retired field kept for response compatibility, not your rate and not anybody's.
  • product_type is a stringRepeat or OneOff — not the uint8 you passed.
  • An inactive product is a 404 here, hidden rather than reported. So a 404 immediately after a successful addProduct means our indexer has not caught up yet; retry rather than re-listing, because a second addProduct with the same id reverts.

If you are the first merchant on a fresh deployment, one asymmetry applies: identity ids start at zero, and this read rejects any merchant_id at or below zero with 400 invalid merchant_id while both registration routes accept zero. A merchant holding id 0 completes every step above and then cannot use this check.

Errors and retries

On-chain:

RevertCause
MerchantNotActive(uint256)Your merchant's isActive is false. Activate, then retry
ProductAlreadyExists(uint256, bytes32)That id is taken for this merchant. Choose another slug
InvalidRates(uint16)commissionBps above 10000
Unauthorised(uint256, bytes32, address)The sender holds neither the product role nor the identity
MerchantNotFound(uint256)No such identity token on this deployment

From the API:

StatusBodyCause
400invalid requestMissing body, or a negative merchant_id
400invalid product_id: must be 32-byte hexNot 32 bytes once 0x is stripped
400invalid endpoint_urlUnparseable, no host, or a scheme other than http/https
403caller does not own this merchantThe recovered signer is not the on-chain owner
404product not found on-chaingetProduct reverted after the retry window. The addProduct transaction has not confirmed, or the ids differ
409merchant not registered — complete merchant registration before adding productsThe merchant is active on-chain but has no record with us. Register the merchant, then retry — the product is already on-chain and does not need re-adding
500internal errorA transient chain read or database write. Retry

Search relevance arrives after the response. The route computes an embedding for the product's name, description and category in the background, best-effort. A failure there is logged and ignored, so a 201 does not guarantee the product is semantically searchable yet — and repeating the metadata call recomputes it.

Next steps