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:
- Ed25519 signature verification, to check an attribution token.
- EIP-712 typed-data signing with a secp256k1 key, for the commitments and for the ERC-3009 payment authorisation.
keccak256andpersonal_sign, for the signed-request scheme our/v1writes use.- 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'sattestationPubKey.- The top-level
attestationPubKeyis one chain's key, not a universal one. Attribution tokens are signed per chain, so verifying a token means using the key from the matchingchains[]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:
- Base64url-decode the segment — it is unpadded base64url.
- Split the trailing 64 bytes off as the Ed25519 signature; the remainder is the JSON payload.
- Verify the signature against the
attestationPubKeyfor the chain you are transacting on. Reject on failure — nothing else in the payload is trustworthy first. - Confirm the payload's
chainIdis 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 field | Value |
|---|---|
from | your wallet |
to | the token's splitRouter — not the 402's payTo |
value | exactly accepts[0].amount, as a decimal string |
validAfter | 0 |
validBefore | a few minutes ahead, as unix seconds |
nonce | 32 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:
- POST
step1with no signature. It writes nothing, returns acommitmentobject, and can be repeated. - Sign that object as EIP-712 typed data, verbatim, with the identity owner's key.
- POST
step2with thenonce, theexpiryand 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
| Message | Where | Cause |
|---|---|---|
invalid token | the ref-link path | The base64url did not decode, or the Ed25519 signature did not verify |
missing extensions.attributionToken | facilitator | The payload carried no token. The rejection carries a setup pointer |
attribution token missing chain ID / … used on chain … | facilitator | A token from another deployment |
missing requirements.extra name/version | facilitator | The merchant's 402 omitted the USDC domain. Not yours to fix |
authorization.to … does not match requirements.payTo | facilitator | You 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 values | facilitator | A high-s or malformed signature. Normalise to canonical s |
timestamp out of window | /v1 write | More than 60 seconds of skew |
replayed request | /v1 write | The identical signed request was already accepted. Change the timestamp and re-sign |
chain <id> not configured | any /v1 call | That chain is not served. There is no silent fallback |
invalid signature on a step2 | /v1 write | The commitment was re-serialised before signing, or signed by a key that is not the owner |
Next steps
- The API — every route, the three auth tiers, and the signing rules in full
- Signing without the signer — the eight type strings and the digest
- Connect via MCP — the same capabilities, wiring done