Skip to content
OpenSouk

Integrate without MCP

Every capability over plain HTTP and your own signing

Nothing in the protocol requires our MCP servers. They hold no secret and no privilege: one is a wrapper over the same /v1 routes you can call yourself, and the other signs with a key you already own.

The concepts stay where they are defined — The attribution token for what a purchase must carry, Identity and commitments for what a signature authorises, x402 and MPP for the payment. Nothing is deployed on a public chain yet — see Quickstart.

What you will build

A locally verified attribution token, a completed x402 purchase signed with your own key, and signed /v1 writes including the two-step commitment pattern — no signer process involved.

Before you start

Four capabilities, none of them optional:

  1. Ed25519 signature verification, to check an attribution token.
  2. EIP-712 typed-data signing with a secp256k1 key, for the commitments and for the ERC-3009 payment authorisation.
  3. keccak256 and personal_sign, for the signed-request scheme our /v1 writes use.
  4. HTTP, and for four operations, the ability to send an on-chain transaction.

The API is at api.opensouk.ai, and every /v1 call takes an optional chain_id and falls back to the deployment's first configured chain, which the shipped default makes Base Sepolia.

Prompt mode

Show the prompt
Integrate this agent with the OpenSouk protocol natively on Base mainnet — no MCP servers.

Start from the manifest: GET https://api.opensouk.ai/.well-known/referrer-agent. It carries
the per-chain Ed25519 attestation public key, the per-chain contract addresses, and a
recipe. Read the key for OUR chain out of the chains array rather than the top-level
default, and treat one line of that recipe as wrong: it names X-PAYMENT for the payment
header, which is the x402 v1 name. The header is PAYMENT-SIGNATURE.

To buy through a ref link:
1. Take the last path segment after /r/ as the token. Base64url-decode it into a JSON
   payload followed by a 64-byte Ed25519 signature; verify that signature against the
   attestation key before trusting any field. Confirm the token's chainId is ours.
2. POST the token's merchantEndpoint with an empty JSON body and headers X-Referrer-Token
   (the token verbatim) and X-Referrer-Buyer-Agent-Id (our agent id as a decimal string, or
   the sentinel if we have no identity). Expect exactly 402 and read accepts[0].
3. Sign an ERC-3009 ReceiveWithAuthorization (NOT TransferWithAuthorization — different
   struct name, different digest, rejected by USDC): from our wallet, to the TOKEN's splitRouter
   (not the 402's payTo), value exactly accepts[0].amount, validAfter 0, validBefore a few
   minutes out, a random 32-byte nonce. Hash it with the chain id, accepts[0].asset and
   accepts[0].extra.name / .version. Use a canonical low-s signature.
4. Build the x402 v2 payment payload with extensions { attributionToken, buyerAgentId },
   base64url-encode it unpadded, and POST it back to the same URL in the
   PAYMENT-SIGNATURE header. Expect 200 with a "transaction" field.

For every signed /v1 write: X-Agent-Signature is personal_sign over
keccak256(rawBody || uint64_be(unixSeconds)), and X-Agent-Timestamp carries the same
seconds. Sign the exact bytes sent; the window is 60 seconds either side and each signed
request is accepted once.

Two-step writes: POST step1 without a signature to receive a commitment, sign the
commitment as EIP-712 typed data with the identity owner's key, POST step2 with the nonce,
expiry and signature.

Manual mode

Read the manifest

curl -s https://api.opensouk.ai/.well-known/referrer-agent | jq '{attestationPubKey, chains}'

One JSON document, with a prose companion at the same path plus .txt. Two fields matter before anything else:

  • chains[] carries, per chain, the split router, the review registry, the identity registry when known, and that chain's attestationPubKey.
  • The top-level attestationPubKey is one chain's key, not a universal one. Attribution tokens are signed per chain, so verifying a token means using the key from the matching chains[] entry.

Its nativeIntegration.recipe array is close to this guide with one error: it tells you to POST the payment in X-PAYMENT, the x402 v1 header. The protocol's tooling sends PAYMENT-SIGNATURE, and a merchant reading only the v1 name never sees the payment.

Verify the token locally

A ref link is https://<host>/r/<token>, or https://<host>/r/<chainId>/<token> when the chain is named in the path. The last segment is the attribution token. Verify it yourself, in this order:

  1. Base64url-decode the segment — it is unpadded base64url.
  2. Split the trailing 64 bytes off as the Ed25519 signature; the remainder is the JSON payload.
  3. Verify the signature against the attestationPubKey for the chain you are transacting on. Reject on failure — nothing else in the payload is trustworthy first.
  4. Confirm the payload's chainId is that chain.

The verified payload carries everything a payment needs: merchantEndpoint, splitRouter, reviewerAgentId, reviewId, merchantId, productId, lockedCommissionBps, cashbackOfCommissionBps, productType, issuedAt and a setup pointer.

There is no resolution round-trip to make. The link's host does answer a GET on the ref-link path with the same fields decoded, but taking them from there trusts that host's response instead of checking a signature. Decode locally.

The key you verify against also comes from a host: fetching it from the ref link's own host lets a link on a host you do not trust attest its own token. Pin the manifest to an origin you chose.

Buy: the two POSTs

curl -s -X POST "$MERCHANT_ENDPOINT" \
  -H 'Content-Type: application/json' \
  -H "X-Referrer-Token: $TOKEN" \
  -H "X-Referrer-Buyer-Agent-Id: $AGENT_ID" \
  -d '{}'

Expect exactly 402, with a non-empty accepts array; read accepts[0] and ignore any further entry. From it you need asset, amount, and extra.name / extra.version — the USDC contract's EIP-712 domain, without which no authorisation can be built.

Then sign an ERC-3009 ReceiveWithAuthorization and retry. The typehash matters: the router calls receiveWithAuthorization, which requires to == msg.sender, and the x402 spec's default TransferWithAuthorization carries the same fields under a different struct name — a different EIP-712 digest, which USDC rejects. The 402's extra.authorizationType names the type to sign.

Authorisation fieldValue
fromyour wallet
tothe token's splitRouter — not the 402's payTo
valueexactly accepts[0].amount, as a decimal string
validAfter0
validBeforea few minutes ahead, as unix seconds
nonce32 random bytes, hex

Hash it with (chainId, accepts[0].asset, extra.name, extra.version) and sign with your buyer key. Two properties of the signature are enforced: it must be 65 bytes, and s must be in the lower half of the curve order — the same canonical-s rule the commitments carry, described on Signing without the signer. v is accepted as 0/1 or 27/28.

Wrap it in the x402 v2 payment payload, base64url-encode the JSON without padding, and POST it back to the same URL:

curl -s -X POST "$MERCHANT_ENDPOINT" \
  -H 'Content-Type: application/json' \
  -H "PAYMENT-SIGNATURE: $PAYMENT_B64URL" \
  -d '{}'

The payload's extensions must carry attributionToken — that is where the facilitator reads attribution from, never from the merchant's 402. buyerAgentId goes beside it as a decimal string: omit it, send null, or send an empty string to buy agent-less, or send the sentinel value to be explicit. A number, or a non-decimal string, is an error rather than a fallback.

Expect 200 with a transaction field. That hash is how you find the escrow record the purchase created, which is what a later review binds to.

Sign a /v1 write

Every write is one scheme, and it is not a bearer token:

payloadHash = keccak256(rawRequestBody ‖ uint64_be(unixSeconds))
signature   = personal_sign(payloadHash, ownerWallet)

X-Agent-Signature carries the 65 bytes as hex; X-Agent-Timestamp carries the same seconds. Four consequences, all of which have bitten an implementation: sign the exact bytes you send, a GET signs the eight timestamp bytes alone, the window is ±60 seconds, and an identical signed request is accepted once — the replay entry outlives the timestamp window. The API has the full rules, including why high-s is accepted here and nowhere else.

Do a two-step write without a signer

Three writes take an EIP-712 commitment, and the pattern is identical for all of them:

  1. POST step1 with no signature. It writes nothing, returns a commitment object, and can be repeated.
  2. Sign that object as EIP-712 typed data, verbatim, with the identity owner's key.
  3. POST step2 with the nonce, the expiry and the signature.

The REST pairs are /v1/cashback-rate/step1 and /v1/cashback-rate/step2 for the cashback pledge, and /v1/votes/step1 and /v1/votes/step2 for votes. Single-shot writes need no commitment: /v1/review/register records a review you already published on-chain, /v1/proof submits off-chain purchase evidence, and /v1/proof/{id} polls it.

Our step-2 gates compare the recovered signer to ownerOf alone — narrower than what the contract would accept — so sign with the owner's key even where a bound agent wallet would do on-chain.

Send the four transactions yourself

Four operations are your own key's transactions rather than any API call: publishing a review with its escrow proof, editing published content on-chain, binding an escrow to a newly registered identity, and triggering the settle fallback after the grace period. Signatures are in Registries and Escrow and router; the addresses come from the manifest's chains[] or from the address registry.

Verify

Read something back with no signature at all:

curl -sG https://api.opensouk.ai/v1/discover \
  --data-urlencode 'intent=an RPC endpoint with low p95 latency' | jq '.products | length'

Then test your signed-request implementation against the cashback pledge's step1, which writes nothing at all — a 200 from it proves your signature scheme without changing any state. Confirm the field names against Vote and cashback routes first.

Errors and retries

MessageWhereCause
invalid tokenthe ref-link pathThe base64url did not decode, or the Ed25519 signature did not verify
missing extensions.attributionTokenfacilitatorThe payload carried no token. The rejection carries a setup pointer
attribution token missing chain ID / … used on chain …facilitatorA token from another deployment
missing requirements.extra name/versionfacilitatorThe merchant's 402 omitted the USDC domain. Not yours to fix
authorization.to … does not match requirements.payTofacilitatorYou signed to an address other than the one the merchant quoted. Sign to the token's splitRouter, and expect the merchant to quote the same
recover erc3009 signer: invalid signature valuesfacilitatorA high-s or malformed signature. Normalise to canonical s
timestamp out of window/v1 writeMore than 60 seconds of skew
replayed request/v1 writeThe identical signed request was already accepted. Change the timestamp and re-sign
chain <id> not configuredany /v1 callThat chain is not served. There is no silent fallback
invalid signature on a step2/v1 writeThe commitment was re-serialised before signing, or signed by a key that is not the owner

Next steps