Non-Custodial Design
AinePay deterministically generates a user payment address from the merchant's collect address. This address is a blockchain contract address whose logic ensures that any funds sent to it can only be swept to the merchant's collect address, and anyone can trigger the sweep. Therefore, user funds do not need to be custodied on any platform β math and the blockchain guarantee the fund flow.
Non-Custodial Address Architecture
- Collect address: the merchant-controlled address where funds are ultimately swept. The merchant must configure this in the merchant dashboard.
- User address: the payment address shown to the user. It is a contract address deterministically derived from the merchant's collect address. The contract logic ensures that any funds sent to this address can only be swept to the merchant's collect address, and anyone can trigger the sweep.
- ForwarderImpl: the contract template for user addresses. Every user address contract has the same logic as this address.
- ForwarderFactory: deploys user addresses according to the ForwarderImpl template.
How the non-custodial flow works
- A merchant configures one or more active collect addresses in the merchant dashboard.
- AinePay creates a unique user payment address for the merchant's customer.
- The customer pays to this dedicated user payment address, allowing AinePay to identify which user made the deposit.
- The deposit is recorded in the AinePay account ledger for the user's payment order.
- When the user address balance reaches the auto-sweep threshold and the merchant status is
ACTIVE, AinePay can automatically sweep funds to the merchant's collect address (the contract guarantees no one can sweep funds to any other address). In the current configuration, the sweep threshold is 1 USD.
This design serves two business goals simultaneously. First, each user gets a unique payment address, so transfers can be correctly attributed and credited to the right user. Second, funds are designed to flow only to the merchant's preconfigured collect address once they reach the threshold.
Even if AinePay becomes unavailable, the merchant can still sweep funds from user addresses to their own collect address without relying on anyone. In the sweep operation, AinePay has no more privilege over the contract than anyone else.
CREATE2 deterministic address model
The user payment address is computed using the CREATE2 model. In the current implementation, the address is deterministically derived from ForwarderFactory, ForwarderImpl, merchantId, userId, the merchant's collect address, version, and chainId.
address = keccak256(0xff ++ factory ++ salt ++ keccak256(init_code))[12:]
salt: keccak256(abi.encode(keccak256(merchantId), keccak256(userId), version, destination, chainId))
This is the unique identifier used to derive the user payment address. It is built from the merchant, the user, the forwarder version, the collect address, and the chain ID. If any of these inputs changes, the derived address will be different.
init_code: the minimal proxy creation code pointing to ForwarderImpl. It defines what kind of contract will be deployed and which implementation it will point to.
Reference docs
How to verify
Merchants can use the following example code to independently verify whether a user address assigned by AinePay matches the expected result, confirming that any funds sent to that address will ultimately be swept to their own collect address:
import { ethers } from "ethers";
function predictForwarderAddress(
factory,
implementation,
merchantId,
userId,
destination,
version,
chainId
) {
const impl = implementation.toLowerCase().replace(/^0x/, "");
const initCodeHash = ethers.keccak256(
"0x3d602d80600a3d3981f3" +
"363d3d373d3d3d363d73" +
impl +
"5af43d82803e903d91602b57fd5bf3"
);
const salt = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
["bytes32", "bytes32", "uint256", "address", "uint256"],
[
ethers.keccak256(ethers.toUtf8Bytes(merchantId)),
ethers.keccak256(ethers.toUtf8Bytes(userId)),
BigInt(version),
destination,
BigInt(chainId)
]
)
);
const packed = ethers.keccak256(
ethers.concat(["0xff", factory, salt, initCodeHash])
);
return ethers.getAddress(ethers.dataSlice(packed, 12));
}
const FACTORY = "0x06559ab75cd906e2ecd9c3e91459eea558e2ec1b";
const IMPLEMENTATION = "0x42eb2a5b755551d5f386f2c79807abd438341557";
const ASSIGNED_USER_ADDRESS = "<ASSIGNED_USER_ADDRESS>";
const merchantId = "20001";
const userId = "U_90001";
const destination = "<DESTINATION_ADDRESS>";
const version = 1;
const chainId = 1;
const predicted = predictForwarderAddress(
FACTORY,
IMPLEMENTATION,
merchantId,
userId,
destination,
version,
chainId
);
const matched =
ethers.getAddress(predicted) === ethers.getAddress(ASSIGNED_USER_ADDRESS);
console.log({ predicted, matched });Contract addresses
- ForwarderFactory:
0x06559ab75cd906e2ecd9c3e91459eea558e2ec1b - ForwarderImpl:
0x42eb2a5b755551d5f386f2c79807abd438341557 - Contract implementation GitHub repository: https://github.com/ainepay/ainepay-contract
Dedicated deployment
AinePay can deploy dedicated contracts for an individual merchant when needed. For this setup, contact support@ainepay.com.