v0.1 · draft// SPECIFICATION

Machine Payments Protocol — Sui

The MPP standard, bound to Sui. USDC as the settlement currency.

Preamble#

The Machine Payments Protocol (MPP) is an open standard, originally specified by Stripe and Tempo Labs, for autonomous agent-to-service payments over HTTP. This document defines the Sui binding of MPP — the rules a server and client follow when settling MPP challenges on the Sui chain using USDC.

This document, version 0.1, is a draft. Implementers should expect breaking changes between draft revisions. The reference implementation is @suimpp/mpp at version 0.7 or later, layered over mppx. The normative behavior is described in sections 1 through 9. Appendices are informative.

Terminology#

The key words MUST, SHOULD, and MAY in this document are to be interpreted as described in RFC 2119.

TermDefinition
AgentThe HTTP client initiating paid requests on behalf of a human or another program.
ServerThe HTTP server advertising paid endpoints and verifying payments.
GatewayAn application layer that wraps one or more upstream APIs in MPP semantics.
ChallengeThe structured 402 response naming the endpoint's price, recipient, and currency.
CredentialThe signed payload an agent attaches to a retry to satisfy a challenge.
DigestA Sui transaction digest — the canonical reference to a settled payment.
Coin typeA Move type tag identifying the asset being transferred, e.g. 0xdba…::usdc::USDC.
Sponsored transactionA Sui transaction whose gas is paid by a sponsor object, leaving the sender to pay zero SUI.

Scope#

This binding defines:

  • The shape of the HTTP 402 Payment Required challenge a Sui-MPP server returns.
  • The shape of the credential a client sends to satisfy that challenge.
  • The verification a server MUST perform against the Sui chain before responding 200.
  • The OpenAPI extension a server SHOULD use to advertise its terms.

Out of scope: how the agent acquires USDC, how the server prices its endpoints, how off-chain receipts are stored, or how clients select among multiple offered payment methods.

1. The 402 challenge#

When a request arrives at a paid endpoint without a valid Authorization header, the server MUST respond with HTTP status 402 Payment Required and one or more WWW-Authenticate headers using the Payment scheme. Each header carries a single offered payment method:

HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment id="a1b2c3...",
  realm="api.example.com",
  method="sui",
  intent="charge",
  request="eyJhbW91bnQiOiIwLjAxMiIsImN1cnJlbmN5IjoiMHhkYmEzNDY3M..."

The parameters carry the following meanings:

ParameterMeaning
idAn opaque challenge identifier, unique per 402 response. Used by the client to bind its credential to a specific challenge.
realmAn opaque realm string (typically the server's hostname). Clients SHOULD echo this back in any UI.
methodAlways "sui" for this binding. Servers MAY emit multiple WWW-Authenticate headers to offer alternative methods.
intentAlways "charge" for this binding.
requestA base64url-encoded JSON object carrying the price terms. The agent MUST decode it to obtain amount, currency, and recipient.
expiresOptional. ISO 8601 datetime (RFC 3339) after which the server MUST reject any credential bound to this challenge.

The decoded request object is shaped:

{
  "amount":    "0.012",                  // decimal string, in units of currency
  "currency":  "0xdba…::usdc::USDC",     // full Sui Move type tag
  "recipient": "0xabc…def012"            // Sui address that MUST receive the transfer
}

Servers MUST reject any retry whose decoded request mismatches the original challenge by amount, currency, or recipient — agents MUST NOT tamper with these fields.

2. The Payment credential#

To satisfy a 402 challenge, the client MUST retry the original request with an additional Authorization header using the Payment scheme:

POST /api/resource HTTP/1.1
Host: api.example.com
Authorization: Payment eyJjaGFsbGVuZ2UiOnsiaWQiOiJhMWIyYzMuLi4iLCJyZWFs...

The portion following Payment is a base64url-encoded JSON object containing:

{
  "challenge": { … },           // the deserialized challenge, echoed back verbatim
  "payload": {
    "digest":    "Hp4oHHs...",  // the Sui transaction digest
    "signature": "ALxw..."      // grief-protection signature (REQUIRED since 0.7)
  }
}

The payload.signature field is a Sui personal-message signature over a deterministic message binding the sender's identity to this exact challenge and digest. Without it, an attacker who observes a digest on-chain or in transit could submit it as their own credential and consume the paid request. The signed message is:

{
  "domain":       "suimpp.sui.payment-proof",
  "version":      1,
  "method":       "sui",
  "intent":       "charge",
  "challengeId":  "<challenge.id>",
  "amount":       "<challenge.request.amount>",
  "currency":     "<challenge.request.currency>",
  "recipient":    "<challenge.request.recipient>",
  "digest":       "<payload.digest>"
}

The client MUST sign this message with the same Sui keypair that signed the on-chain settlement transaction (§3).

3. Settlement on Sui#

The client builds a Sui programmable transaction block (PTB) that transfers amount of currency to recipient, signs it, and submits it. The transaction MUST:

  • Be executed against the network advertised in the 402 challenge (see Appendix A for canonical coin types per network).
  • Reach status success before the credential is sent.
  • Transfer at least amount of currency to recipient, net of any coin splits. Overpayment is permitted but not refunded.
  • Be signed by a keypair whose address equals the on-chain sender. Multi-sig and sponsored signing schemes are permitted as long as the eventual on-chain sender matches.

Sui's sponsored transaction capability is supported. When the canonical USDC coin type is used (Appendix A), the reference SDK requests a sponsor object from the Sui Foundation gasless tier and constructs the PTB so the sender pays zero SUI for gas. Servers MUST NOT reject a payment for being sponsored.

4. Verification#

On receiving a retry with an Authorization: Payment header, the server MUST perform all of the following before responding 200:

  1. Decode the credential and extract challenge and payload.
  2. If the original challenge carried an expires parameter and it has passed, reject with a fresh 402.
  3. If payload.digest is already present in the server's digest store, reject with a fresh 402 Payment Required (replay defense — see §9).
  4. Fetch the transaction from a trusted Sui RPC. The reference implementation uses client.core.getTransaction({digest, include: {balanceChanges, transaction}}).
  5. Verify the transaction status is success.
  6. Verify the balance changes include an entry where coinType equals request.currency, the recipient address (normalized) equals request.recipient, and the transferred amount (in raw units) is greater than or equal to request.amount converted using the currency's decimals.
  7. Verify the grief-protection signature: recover the signing public key from payload.signature over the message defined in §2. Reject if the recovered Sui address does not equal the on-chain transaction sender.
  8. Persist payload.digest in the digest store. The store MUST retain the digest for at least the challenge expiry window.
  9. Handle the original request and respond 200 OK. The response SHOULD include a Payment-Receipt header carrying the receipt token.

Any failure in steps 2 through 7 MUST result in a fresh 402 Payment Required response. The server MUST NOT consume agent balance or partially fulfill a request that fails verification.

5. OpenAPI advertisement#

Servers SHOULD publish an OpenAPI 3.x document at /openapi.json. This makes the server discoverable by tooling — including the @suimpp/discovery validator — without out-of-band coordination.

The document SHOULD mark every paid operation with the x-payment-info extension defined in §6, and SHOULD declare a "402" response describing the challenge body shape.

6. x-payment-info#

The x-payment-info object is attached to an OpenAPI operation to declare the endpoint's price terms:

paths:
  /v1/chat/completions:
    post:
      x-payment-info:
        method: sui
        amount: "0.005"
        currency: "0xdba…::usdc::USDC"
        recipient: "0xabc…def012"
        network: mainnet
        unit: per_request
      responses:
        "200": { … }
        "402": { … }

Allowed values for unit are per_request, per_token, per_second, and tiered. When per_token or tiered is used, the actual challenge amount is computed at request time and is only authoritative in the 402 response.

7. Payment reporting (informative)#

Gateways MAY report settled payments to a directory service for ecosystem visibility. Reporting is opt-in and MUST NOT block a 200 response.

The library emits on-chain context via an onPayment callback. The host enriches it with HTTP context (which service / endpoint was called) and POSTs the joined record to any registry. A single report carries all fields:

POST https://<registry>/api/report
Content-Type: application/json

{
  "digest":    "Hp4oHHs...",
  "sender":    "0xagent...",
  "recipient": "0xabc...def012",
  "amount":    "0.012",
  "currency":  "0xdba...::usdc::USDC",
  "network":   "mainnet",
  "serverUrl": "https://api.example.com",
  "service":   "openai",
  "endpoint":  "/v1/chat/completions"
}

8. Errors#

Errors are conveyed via HTTP status codes. Servers MUST use the codes below; clients MUST treat unrecognized codes per RFC 9110.

StatusMeaning
402Payment required, or the supplied credential is invalid, expired, replayed, or fails verification. The server MUST include a fresh WWW-Authenticate: Payment challenge in the response.
400Malformed Authorization: Payment header or credential JSON.
404Endpoint exists but is not paid (no challenge to issue).
503Sui RPC unreachable — server cannot verify. Client MAY retry.

9. Security considerations#

  • Replay defense. A digest MUST be redeemable exactly once. Servers MUST persist consumed digests for at least the challenge expiry window. The reference implementation requires a non-default DigestStore in production; InMemoryDigestStore is for development only.
  • Grief protection. Without the §2 signature, anyone observing a digest in mempool or on-chain could submit it as their own credential and consume the paid request. Servers MUST verify the recovered signer equals the on-chain transaction sender.
  • Recipient verification. The recipient address in the decoded request MUST exactly match the one found on-chain. Address normalization (case, padding, zero-extension) is the server's responsibility.
  • Network confusion. Servers MUST reject digests from networks other than the one they configured — a testnet digest MUST NOT satisfy a mainnet challenge.
  • Amount precision. Servers MUST compare transferred and requested amounts in raw integer units (BigInt). Floating-point arithmetic introduces rounding errors that can be exploited to underpay.
  • RPC trust. Verification is only as trustworthy as the Sui RPC the server queries. Production servers SHOULD consult more than one independent RPC or run a self-hosted full node.
  • Privacy. The sender address is on-chain and public. Servers SHOULD avoid logging sender addresses alongside personally identifying request context unless required.

Appendix A · Canonical USDC types#

The following coin types are recognized as USDC by the reference implementation. Servers SHOULD accept any matching their declared network; clients SHOULD default to the mainnet type.

NetworkCoin type
mainnet0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC
testnet0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC

Both have 6 decimals. The constants are exported from @suimpp/mpp/server (or the package root @suimpp/mpp) as USDC and USDC_TESTNET.

Appendix B · References#