Skip to content
LogoLogo

Pay with REST (raw 402 flow)

This is the same economic path as the SDK payer, written as HTTP and Sui steps you can implement in any language. Prefer Pay with the SDK unless you are building a custom client.

Assumptions:

  • Owner registered your public address as a BYO delegated signer.
  • You hold the private key for that address and a connection manifest (package, pool, grant, policies, network, coin type).
  • Resource is a spend-account rail (delegate on the wire) with MPP.

1. Call the paid resource unpaid

POST /v1/your-resource
Content-Type: application/json
 
{"...": "..."}

Expect 402 Payment Required with:

  • WWW-Authenticate: Payment ... (MPP challenge), and possibly
  • x402 offers (may be present even if you will not use x402). The gateway sends them in the PAYMENT-REQUIRED header (base64 JSON) and repeats the same object as the JSON body. A seller built on @cmdoss/suipay-sdk/seller returns them in the 402 JSON body only.

Decode the MPP challenge / offer. Validate at least:

  • offer digest / HMAC (with the deployment's verification material)
  • amount (atomic integer string), asset, decimals
  • payTo, network (sui:testnet etc.), package id
  • settlementRail === "delegate" (spend-account rail tag)
  • method, path, and body binding (bodyHash / content type) for body-bearing requests
  • expiresAt not passed
  • service id / revision match what you intended

Do not proceed if validation fails.

2. Derive binding hashes

payment_id_hash = SHA-256(challengeId)
terms_hash      = SHA-256(offer.digest)

These must match the values you put on chain and the ones the facilitator re-derives from the finalized event.

3. Build spend_account::settle_policy

Transaction sender: your delegated address.

Move call (argument order is package-defined; follow the published package / SDK builder, not this sketch alone):

package::spend_account::settle_policy
  exclusion directory (shared),
  recipient registry (shared),
  pool (shared),
  grant (shared grant object),
  policy_id,
  target_hash,
  amount,
  payment_id_hash,
  terms_hash,
  expires_at_ms,
  clock,
  ...

Resolve real initial shared versions for every shared object (directory, registry, pool, grant) from chain. Using placeholder versions will fail shared-object consensus.

Recipient is not a free argument at pay time: the contract resolves it from the policy target for the target hash.

4. Gas

  • You have SUI: attach gas coin objects owned by the delegate and set budget/price.
  • You do not: use sponsored gas. The agent signs a personal message to prove it holds the key, then gas is sponsored:
    1. POST /api/sponsor/personal-message/challenge with delegateAddress, poolId, grantId, network, and packageId (all required; network / packageId must match the deployment). This returns a single-use nonce.
    2. Sign the domain-separated payload (suipay:sponsor-personal-message:v1) over the pre-sponsor kind hash with the delegated key (signPersonalMessage). The payload binds the nonce, requestId, expiry, delegate, pool, grant, network, and package.
    3. POST /api/sponsor with the kind bytes plus the personal-message fields (nonce, signature, requestId, expiry, and the pool/grant/network/package bindings). The response carries the sponsored transaction bytes, its digest, and a single-use executeToken.
    4. Sign the sponsored bytes with the delegated key, then POST /api/sponsor/execute with digest, signature, and executeToken (plus sponsorBackendToken when step 3 returned one). The gateway executes the transaction and answers with its digest. Without the token it answers 403.

The personal-message challenge authorizes gas only.

5. Sign and execute

With your own gas, sign the full transaction as the delegate and execute it via Sui fullnode / gRPC. With sponsored gas, POST /api/sponsor/execute in step 4 is the execute. Either way, wait until the transaction is finalized success. Keep the digest.

If execute times out after a possible broadcast, or /api/sponsor/execute fails, treat the payment as ambiguous. Do not build a second payment for the same challenge without reconciling chain state.

6. Replay with MPP proof

Retry the same URL, method, headers, and body with:

Authorization: Payment /* MPP PaymentProof including digest, payer, challenge, offer */

Never send MPP and x402 credentials together. Never forward payment credentials across an origin-changing redirect.

7. Confirm delivery and receipt

Success responses include settlement evidence:

DialectReceipt header
MPPPayment-Receipt
x402PAYMENT-RESPONSE

Require challenge ID, network, amount, asset, payer, and non-empty txDigest. Cross-check with GET /v1/settlements?challengeId=... when authenticated, and with Suiscan.

Errors and idempotency

SituationAction
402 validation failsDo not sign
Grant / policy rejects on chainFix authority; do not retry blindly with a larger amount
Ambiguous after submitReconcile digest + challenge; one payment id, one settlement
Gateway refuses proofDigest may still be final; reconcile before a new challenge

Exactly-once on chain is the pool-scoped replay marker. Your client should still keep a durable "attempt in flight / done" ledger so concurrent retries do not double-sign.