Skip to content

Guide โ€” Acquirer Registration

Register as an Acquirer

An Acquirer is any registered participant who distributes the Stablecoin Stack payment service and earns a percentage of the processing fee on every payment they refer. Commission is distributed automatically by the Settlement Contract โ€” in the same atomic transaction as the payment itself. No invoicing. No reconciliation cycles. No trust relationship required with the processor.

This guide covers the full registration flow: constructing and signing the BuyAcquiringPackRequest, submitting it, retrieving your Acquirer ID, and embedding it in payment flows.


What registration gives you

When a payment includes your Acquirer ID in its ref field, the Settlement Contract:

  1. Computes your commission as a percentage of the principal.
  2. Credits it to your wallet's internal balance โ€” atomically with the payment.
  3. Emits a CommissionGenerated event you can track via the Event Explorer.

Your commission accumulates on-chain. Withdraw at any time. No billing period. No float.


Prerequisites

  • A wallet with enough stablecoin to pay acquiringPrice (the one-time registration fee โ€” fetch the current value from the processor before starting)
  • A local stack (see Run Locally) or access to a live processor's gateway
  • The TypeScript SDK: npm install @stablecoin-stack/wallet-sdk ethers

Step 1 โ€” Fetch registration parameters

import { ethers } from 'ethers';
import SettlementContractABI from '@stablecoin-stack/abis/SettlementContract.json';

const provider  = new ethers.JsonRpcProvider(RPC_URL);
const contract  = new ethers.Contract(SETTLEMENT_CONTRACT_ADDRESS, SettlementContractABI, provider);

// Current registration fee (in token units)
const acquiringPrice = await contract.acquiringPrice();
console.log(`Registration fee: $${ethers.formatUnits(acquiringPrice, 6)}`);

// Maximum fee percentage you are allowed to set
const maxAcquiringFee = await contract.maxAcquiringFee();
console.log(`Max acquiring fee: ${maxAcquiringFee} (processor-defined units)`);

// Tokens accepted for the registration payment
const allowedTokens = await walletGateway.getAcquiringAllowedTokens();
console.log('Accepted tokens:', allowedTokens);

Pick a token from allowedTokens for the registration payment. This does not have to be the same token used in the payments you will later acquire.


Step 2 โ€” Decide your acquiring fee percentage

Your acquiring fee percentage must be at or below maxAcquiringFee. The units are processor-defined โ€” check the processor's documentation for the exact interpretation (e.g. basis points, hundredths of a percent, etc.).

// Example: if 100 units = 1%, setting 150 = 1.5%
const desiredFeePercent = 150;

if (desiredFeePercent > maxAcquiringFee) {
  throw new Error(`Fee ${desiredFeePercent} exceeds maximum ${maxAcquiringFee}`);
}

You can update your fee after registration, but you cannot exceed maxAcquiringFee at any point.


Step 3 โ€” Fetch the nonce

const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const payerAddress = await signer.getAddress();

// The wallet being registered may differ from the payer
// (sponsored registration is supported โ€” see note below)
const acquiringWalletAddress = payerAddress;  // or a different address

const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_PERMIT_ABI, provider);
const nonce = await tokenContract.nonces(payerAddress);

Sponsored registration

The payer of the fee and the wallet being registered do not need to be the same address. A company can pay the fee on behalf of a partner's wallet. Set payerAddress to the address paying, and acquiringWalletAddress to the address being registered.


Step 4 โ€” Build PermitParams

The permit must be signed for exactly acquiringPrice โ€” no more, no less.

const deadline = Math.floor(Date.now() / 1000) + 900;  // 15 minutes from now

const permitParams = {
  owner:    payerAddress,
  spender:  SETTLEMENT_CONTRACT_ADDRESS,
  value:    acquiringPrice.toString(),  // MUST equal acquiringPrice exactly
  nonce:    nonce.toString(),
  deadline,
};

Step 5 โ€” Sign the Permit (Signature 1)

const tokenDomain = {
  name:              await tokenContract.name(),
  version:           '1',
  chainId:           CHAIN_ID,
  verifyingContract: TOKEN_ADDRESS,
};

const permitTypes = {
  Permit: [
    { name: 'owner',    type: 'address' },
    { name: 'spender',  type: 'address' },
    { name: 'value',    type: 'uint256' },
    { name: 'nonce',    type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
};

const permitSigRaw = await signer.signTypedData(tokenDomain, permitTypes, permitParams);
const { v: v1, r: r1, s: s1 } = ethers.Signature.from(permitSigRaw);
const permitHash = ethers.TypedDataEncoder.hash(tokenDomain, permitTypes, permitParams);

const permitSig = {
  hash: permitHash,
  v:    v1 < 27 ? v1 + 27 : v1,
  r:    r1,
  s:    s1,
};

Step 6 โ€” Build BuyAcquiringPackPermitParams

const buyAcquiringPackParams = {
  token:        TOKEN_ADDRESS,
  feeValue:     acquiringPrice.toString(),  // MUST equal permitParams.value
  acquiring:    acquiringWalletAddress,     // wallet to register
  permitParams,
};

Both feeValue and permitParams.value must equal the current acquiringPrice. The Settlement Contract verifies this on-chain and reverts if they differ.


Step 7 โ€” Sign the operation (Signature 2)

const settlementDomain = {
  name:              'SettlementContract',
  version:           '1',
  chainId:           CHAIN_ID,
  verifyingContract: SETTLEMENT_CONTRACT_ADDRESS,
};

const buyAcquiringPackTypes = {
  BuyAcquiringPackPermitParams: [
    { name: 'token',        type: 'address' },
    { name: 'feeValue',     type: 'uint256' },
    { name: 'acquiring',    type: 'address' },
    { name: 'permitParams', type: 'PermitParams' },
  ],
  PermitParams: [
    { name: 'owner',    type: 'address' },
    { name: 'spender',  type: 'address' },
    { name: 'value',    type: 'uint256' },
    { name: 'nonce',    type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
};

const bindingSigRaw = await signer.signTypedData(
  settlementDomain, buyAcquiringPackTypes, buyAcquiringPackParams
);
const { v: v2, r: r2, s: s2 } = ethers.Signature.from(bindingSigRaw);
const bindingHash = ethers.TypedDataEncoder.hash(
  settlementDomain, buyAcquiringPackTypes, buyAcquiringPackParams
);

const payWithPermitSig = {
  hash: bindingHash,
  v:    v2 < 27 ? v2 + 27 : v2,
  r:    r2,
  s:    s2,
};

Step 8 โ€” Submit the registration request

const buyAcquiringPackRequest = {
  buyAcquiringPackParams,
  payWithPermitSig,
  permitSig,
};

const response = await fetch(`${WALLET_GATEWAY_URL}/api/submit-acquiring`, {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body:    JSON.stringify(buyAcquiringPackRequest),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(`Rejected: ${error.category} โ€” ${error.message}`);
}

const { submissionId } = await response.json();
console.log('Registration submitted:', submissionId);

Step 9 โ€” Retrieve your Acquirer ID

After the transaction confirms on-chain, the Settlement Contract emits an AcquirerCreated event containing your Acquirer ID.

import { ExplorerClient } from '@stablecoin-stack/explorer-sdk';

const explorer = new ExplorerClient({ baseUrl: EXPLORER_URL });

// Option A: watch for the event in real time
await explorer.connect();
explorer.onAcquirerCreated({ wallet: acquiringWalletAddress }, (event) => {
  console.log('Registered!');
  console.log('Acquirer ID:', event.acquirerId);
  console.log('Fee percent:', event.feePercent);
  explorer.disconnect();
});

// Option B: look up after confirmation
const acquirerId = await contract.getAcquiringWallet(acquiringWalletAddress);
// Returns the Zero-UUID if not registered, or your Acquirer ID once confirmed

Store your Acquirer ID. You will distribute it to payers and embed it in your distribution tooling. It is a bytes16 value โ€” 16 bytes, formatted as a 32-character hex string with a 0x prefix.


Step 10 โ€” Embed your Acquirer ID in payments

Your Acquirer ID earns commission only when it is present in the ref field of a payment. The ref field is 32 bytes: the first 16 bytes are the Order Reference, the last 16 bytes are the Acquirer ID.

// When a payer submits a payment through your distribution channel,
// your acquirer ID must be embedded in the ref field.

const ref = '0x'
  + orderReference.replace('0x', '')   // 32 hex chars (16 bytes) โ€” from the merchant's session
  + YOUR_ACQUIRER_ID.replace('0x', ''); // 32 hex chars (16 bytes) โ€” your registered ID

If you are distributing the payment service via a wallet application, a POS terminal, or a platform integration, embed your Acquirer ID automatically when constructing the ref field. Payers and merchants do not need to know it exists.


Step 11 โ€” Track your commissions

import { ExplorerClient } from '@stablecoin-stack/explorer-sdk';

const explorer = new ExplorerClient({ baseUrl: EXPLORER_URL });

// Historical commissions
const commissions = await explorer.getCommissions({
  acquirerId: YOUR_ACQUIRER_ID,
  fromTimestamp: startOfMonth,
  toTimestamp:   endOfMonth,
});

const total = commissions.reduce((sum, c) => sum + BigInt(c.amount), 0n);
console.log(`Monthly commission: $${ethers.formatUnits(total, 6)}`);

// Real-time commission stream
await explorer.connect();
explorer.onCommission({ acquirerId: YOUR_ACQUIRER_ID }, (event) => {
  console.log(`Commission earned: $${ethers.formatUnits(event.amount, 6)}`);
});

Step 12 โ€” Withdraw your balance

Commissions accumulate as an internal balance in the Settlement Contract. Withdraw to your wallet at any time:

const balance = await contract.getBalances(TOKEN_ADDRESS, [acquiringWalletAddress]);
console.log(`Balance: $${ethers.formatUnits(balance[0], 6)}`);

const acquiringSigner = new ethers.Wallet(ACQUIRING_WALLET_PRIVATE_KEY, provider);
await contract.connect(acquiringSigner).withdraw(TOKEN_ADDRESS, balance[0]);

Common mistakes

Mistake Result Fix
feeValue โ‰  permitParams.value SEMANTIC_ERROR rejection Both must equal current acquiringPrice exactly
feePercent > maxAcquiringFee On-chain revert Fetch maxAcquiringFee first
Registering an already-registered wallet On-chain revert Each wallet registers once only
Using a token not in acquiringAllowedTokens On-chain revert Fetch the allowed list first
Wrong v value (0 or 1) CRYPTOGRAPHIC_ERROR Normalise: if v < 27, add 27
Not embedding Acquirer ID in ref No commission earned Always embed in the ref field

Next steps