Getting Started as a Merchant
This guide walks you through registering as a DolphinPay merchant and accepting native Circle USDC payments on Sui testnet.
What is a DolphinPay merchant?
A merchant is any address that registers on-chain to receive payments. As a merchant you get:
- USDC settlement — payments in native Circle USDC (6 decimals), enforced on-chain
- No fees — the platform fee is 0 bps by policy, and merchants have no fee configuration; you receive the full payment amount
- Capability-based control — a
MerchantCapobject authorizes all changes to your merchant - Receiving address control — set where your USDC settles
- Status control — activate or deactivate your merchant at any time
There are no on-chain refunds (per ADR-004): once a payment executes, funds transfer to your receiving address with finality. Handle any make-goods off-chain.
Prerequisites
- ✅ Sui wallet on testnet
- ✅ Testnet SUI for gas: https://faucet.sui.io
- ✅ Package ID and AdminConfig ID from
DEPLOYMENT.md(the source of truth for IDs)
Your customers will need testnet USDC: https://faucet.circle.com (select Sui testnet).
The official DolphinPay Sui Testnet deployment is live and ready for the
native Circle USDC flow documented below. Obtain the current Package ID and
AdminConfig ID only from DEPLOYMENT.md — do not use IDs from anywhere else.
Alternatively, you can publish your own package with the
Testnet Deployment guide.
Step 1: Register your merchant
Using the frontend
- Open the DolphinPay app (deployed on Cloudflare Workers, or run
bun run devinfrontend/) - Connect your wallet (testnet)
- Go to the merchant page and register with a name and description
- Confirm the transaction — you receive a shared
Merchantobject and an ownedMerchantCap
Using the SDK
import { createClient } from '@dolphinpay/sdk';
const client = createClient({
network: 'testnet',
packageId: '<PACKAGE_ID>', // from DEPLOYMENT.md
adminConfigId: '<ADMIN_CONFIG_ID>', // from DEPLOYMENT.md
});
const txb = client.merchant.buildRegisterMerchant({
name: 'My Store',
description: 'Online store selling digital products',
});
// Sign and execute with your wallet
const result = await signAndExecuteTransaction({ transaction: txb });
console.log('Merchant registered:', result.digest);
Find your merchant objects
// One page of MerchantCaps owned by your address (each points at its Merchant object).
// The limit is 1–50 per page (default 50); more caps are reachable via the cursor.
const page = await client.merchant.getMerchantCaps('0xYOUR_ADDRESS', { limit: 50 });
for (const cap of page.caps) {
console.log(cap.objectId, '→', cap.merchantId);
}
if (page.pageInfo.hasNextPage) {
const nextPage = await client.merchant.getMerchantCaps('0xYOUR_ADDRESS', {
cursor: page.pageInfo.endCursor,
});
}
// If you hold exactly one MerchantCap, this returns your sole merchant.
// If you hold multiple caps, it throws unless you pass an explicit
// merchantId selector: getMerchantByOwner('0xYOUR_ADDRESS', '0xMERCHANT_ID').
const merchant = await client.merchant.getMerchantByOwner('0xYOUR_ADDRESS');
Step 2: Set your USDC receiving address
Add USDC support and point it at the address where funds should settle. All merchant mutations require both your Merchant object ID and your MerchantCap ID.
import { USDC_TYPES } from '@dolphinpay/sdk';
const txb = client.merchant.buildAddSupportedCurrency({
merchantObjectId: '0xYOUR_MERCHANT_OBJECT_ID',
merchantCapId: '0xYOUR_MERCHANT_CAP_ID',
currencyType: USDC_TYPES.testnet,
receivingAddress: '0xYOUR_RECEIVING_ADDRESS',
});
To change the address later:
const txb = client.merchant.buildSetReceivingAddress({
merchantObjectId: '0xYOUR_MERCHANT_OBJECT_ID',
merchantCapId: '0xYOUR_MERCHANT_CAP_ID',
currencyType: USDC_TYPES.testnet,
receivingAddress: '0xYOUR_NEW_ADDRESS',
});
USDC is the only currency the platform accepts — the contract aborts on any other coin type. There is no multi-currency support.
Step 3: Create payment requests
import { usdcToUnits, USDC_TYPES } from '@dolphinpay/sdk';
// Validated against your merchant record (must be active)
const txb = client.payment.buildCreatePaymentWithMerchant({
merchantObjectId: '0xYOUR_MERCHANT_OBJECT_ID',
amount: usdcToUnits('25'), // '25' USDC (decimal string) → base-unit string
currencyType: USDC_TYPES.testnet,
description: 'Order #12345',
expirySeconds: 3600, // payment link valid for 1 hour
});
const result = await signAndExecuteTransaction({ transaction: txb });
The Payment is created as a shared object, so any payer can execute it. Share the checkout link (/pay/[paymentId] in the frontend) with your customer.
There is also buildCreatePayment({ merchant: '0xADDRESS', ... }), which pays directly to an address without merchant validation.
Step 4: Customer executes the payment
The customer pays with a USDC coin from their wallet:
const txb = client.payment.buildExecutePaymentWithMerchant(
{
paymentId: '0xPAYMENT_OBJECT_ID',
merchantObjectId: '0xYOUR_MERCHANT_OBJECT_ID',
coinObjectId: '0xCUSTOMER_USDC_COIN_ID',
amount: usdcToUnits('25'), // exact amount is split from the coin in-transaction
},
USDC_TYPES.testnet
);
await customerSignAndExecuteTransaction({ transaction: txb });
On execution the contract validates the coin type against AdminConfig, checks the amount and expiry, and transfers the full amount to your receiving address (platform fee is 0 bps).
Pending payments can be cancelled before execution:
const txb = client.payment.buildCancelPayment({
paymentId: '0xPAYMENT_OBJECT_ID',
reason: 'Order cancelled by customer',
});
Step 5: Track your payments
Query state over gRPC (the SDK's only transport — no JSON-RPC, no websockets):
// Payment status
const { payment } = await client.payment.getPayment('0xPAYMENT_OBJECT_ID');
console.log(payment.status, payment.amount); // amount in USDC base units
// Merchant record
const { merchant } = await client.merchant.getMerchant('0xYOUR_MERCHANT_OBJECT_ID');
console.log(merchant.isActive);
The contract emits payment creation/completion/cancellation and merchant lifecycle events; use client.events to query payment history and merchant statistics. PaymentFailed and PaymentExpired are defined but are not emitted by current contract paths.
Managing your merchant
// Update name/description
const txb1 = client.merchant.buildUpdateMerchantInfo({
merchantObjectId: '0xYOUR_MERCHANT_OBJECT_ID',
merchantCapId: '0xYOUR_MERCHANT_CAP_ID',
name: 'My Store (renamed)',
description: 'Updated description',
});
// Deactivate (stops new merchant-validated payments) / reactivate
const txb2 = client.merchant.buildToggleMerchantStatus({
merchantObjectId: '0xYOUR_MERCHANT_OBJECT_ID',
merchantCapId: '0xYOUR_MERCHANT_CAP_ID',
active: false,
});
Security best practices
- Protect your
MerchantCap— whoever owns it controls your merchant. Keep the owning wallet secure and never transfer the cap unintentionally. Its object ID is public metadata, not a signing credential. - Wallets sign, servers don't — the SDK only builds transactions; never handle private keys in application code.
- Verify receiving addresses before updating them; funds transfer with finality and there are no on-chain refunds.
- Use dry runs — every builder has a
dryRun*counterpart (e.g.client.merchant.dryRunRegisterMerchant(params, sender)) to test without gas. - Don't hard-code IDs — read the Package ID and AdminConfig ID from configuration backed by
DEPLOYMENT.md.
Troubleshooting
Payment execution aborts
- The coin must be testnet native Circle USDC (
USDC_TYPES.testnet); any other type is rejected on-chain - The coin value must match the payment amount — pass
amountso the SDK splits it exactly - The payment may have expired or been cancelled
"Unauthorized merchant operation"
- Check you passed the
MerchantCapthat matches theMerchantobject
Merchant-validated creation fails
- Ensure the merchant is active (
client.merchant.isMerchantActive(...))
Next steps
- Basic Payment Example — complete React integration
- Quick Start — SDK setup recap
- Testnet Deployment — run your own package