Sui Charge Method
v1.0The sui charge method enables on-chain USDC micropayments on the Sui network for the Machine Payments Protocol (MPP).
Overview
MPP uses HTTP 402 Payment Required to negotiate payments between clients and servers. The sui charge method implements this negotiation using USDC transfers on the Sui blockchain, providing sub-second finality and trustless verification.
Protocol Flow
Every MPP payment follows a four-step request-challenge-pay-verify cycle:
- 1RequestClient sends a standard HTTP request to the server endpoint.
- 2Challenge (402)Server responds with
402 Payment Requiredand aWWW-Authenticateheader containing the charge method, amount, currency, and recipient address. - 3PayClient parses the challenge, builds a Sui transaction transferring the requested USDC amount to the recipient, executes it on-chain, and retries the original request with the transaction digest as a credential.
- 4Verify & DeliverServer queries the Sui RPC, confirms the transaction succeeded, verifies the payment amount and recipient, then returns the API response.
Challenge Format
When a server requires payment, it responds with 402 and a challenge encoded in the WWW-Authenticate header:
HTTP/1.1 402 Payment Required
WWW-Authenticate: MPP method="sui",
amount="0.01",
currency="0xdba346...::usdc::USDC",
recipient="0xYOUR_SUI_ADDRESS"| Parameter | Type | Description |
|---|---|---|
| method | string | "sui" — identifies this charge method |
| amount | string | Human-readable amount (e.g. "0.01" = 1 cent USDC) |
| currency | string | Sui coin type — fully qualified Move type |
| recipient | string | Sui address to receive payment (0x-prefixed, 64 hex chars) |
Credential Format
After executing the on-chain payment, the client retries the request with the credential in the Authorization header:
Authorization: MPP method="sui", digest="<TX_DIGEST>"| Parameter | Type | Description |
|---|---|---|
| method | string | "sui" — matches the challenge method |
| digest | string | Sui transaction digest (Base58-encoded, 44 chars) |
Method Schema
The sui charge method is defined using the mppx Method schema:
import { Method, z } from 'mppx';
export const suiCharge = Method.from({
intent: 'charge',
name: 'sui',
schema: {
credential: {
payload: z.object({
digest: z.string(),
}),
},
request: z.object({
amount: z.string(),
currency: z.string(),
recipient: z.string(),
}),
},
});Server Verification
When the server receives a credential, it performs four verification steps:
- 1Fetch transactionQuery the Sui RPC with
getTransactionusing the provided digest. IncludebalanceChangesin the response. - 2Check successVerify
status.success === true. Reject failed or pending transactions. - 3Find paymentScan
balanceChangesfor an entry where:- •
coinTypematches the requested currency - •
addressmatches the recipient (normalized) - •
amountis positive (incoming transfer)
- •
- 4Check amountConvert the challenge
amountto raw units using the currency's decimals (USDC = 6). Verify the transferred amount >= requested amount.
const tx = await client.getTransaction({ digest, include: { balanceChanges: true } });
if (!tx.status.success) throw new Error('Transaction failed');
const payment = tx.balanceChanges.find(
(bc) =>
bc.coinType === currency &&
normalize(bc.address) === normalize(recipient) &&
BigInt(bc.amount) > 0n,
);
if (!payment) throw new Error('Payment not found');
const transferredRaw = BigInt(payment.amount);
const requestedRaw = parseAmountToRaw(amount, 6); // USDC = 6 decimals
if (transferredRaw < requestedRaw) throw new Error('Underpaid');Client Payment
When a client receives a 402 challenge, it builds and executes a Sui transaction:
import { Transaction, coinWithBalance } from '@mysten/sui/transactions';
const tx = new Transaction();
tx.setSender(walletAddress);
const amountRaw = parseAmountToRaw(challenge.amount, 6);
const payment = coinWithBalance({ balance: amountRaw, type: challenge.currency });
tx.transferObjects([payment], challenge.recipient);
const result = await client.signAndExecuteTransaction({ transaction: tx });
// Credential: { digest: result.digest }The coinWithBalance helper automatically splits coins from the sender's balance. The transaction digest becomes the credential's proof of payment.
USDC on Sui
The canonical USDC coin type on Sui mainnet:
0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC| Property | Value |
|---|---|
| Decimals | 6 |
| 1 USDC | 1,000,000 raw units |
| $0.01 | 10,000 raw units |
| Min practical | $0.000001 (1 raw unit) |
| Issuer | Circle (native issuance on Sui) |
Amount Parsing
Amounts in challenges and credentials are human-readable strings (e.g. "0.01"). Both client and server must convert to raw units for on-chain operations:
function parseAmountToRaw(amount: string, decimals: number): bigint {
const [whole = '0', frac = ''] = amount.split('.');
const paddedFrac = frac.padEnd(decimals, '0').slice(0, decimals);
return BigInt(whole + paddedFrac);
}
// Examples:
// parseAmountToRaw("0.01", 6) → 10000n
// parseAmountToRaw("1", 6) → 1000000n
// parseAmountToRaw("0.5", 6) → 500000nSecurity Considerations
BigInt for amount comparisons. Floating-point arithmetic can produce rounding errors that allow underpayment.normalizeSuiAddress() from @mysten/sui/utils.success: true, the payment is irreversible.Reference Implementation
The official TypeScript implementation: