DEV Community

U Ledger
U Ledger

Posted on

Preventing Double-Counted USDT Transfers in a Multi-Wallet Ledger

A multi-wallet USDT ledger has a classification problem that is easy to miss: the same internal movement appears as an outgoing transfer in one owned wallet and an incoming transfer in another.

If those wallet histories are aggregated without an ownership model, the principal may be counted as both an expense and income.

This post describes a small, deterministic data model for handling that case across TRON and BSC.

1. Keep network identity in every key

An address string is not a complete identity.

TRON addresses and EVM addresses use different formats, while the same 0x... address can exist on BSC, Ethereum, Polygon, Base, or another EVM network.

Use a network-qualified key:

function addressKey(network, address) {
  return `${network.toLowerCase()}:${address.toLowerCase()}`;
}
Enter fullscreen mode Exit fullscreen mode

The same rule should apply to transaction identifiers:

function transferKey(network, txHash, logIndex = 0) {
  return `${network.toLowerCase()}:${txHash.toLowerCase()}:${logIndex}`;
}
Enter fullscreen mode Exit fullscreen mode

Including a log index is useful on EVM networks when one transaction emits multiple token-transfer events.

2. Maintain an explicit ownership registry

Do not infer ownership from recent activity or transfer direction.

const owned = new Map([
  ["tron:T_SAMPLE_OPERATIONS", { name: "TRON operations" }],
  ["tron:T_SAMPLE_PAYMENTS", { name: "TRON payments" }],
  ["bsc:0xsampleoperations", { name: "BSC operations" }],
  ["bsc:0xsampletreasury", { name: "BSC treasury" }],
]);
Enter fullscreen mode Exit fullscreen mode

In production, normalize addresses according to the network before adding them to the registry.

The ownership list should be user-controlled because public blockchain data cannot reliably explain the business relationship behind an address.

3. Classify principal movement before calculating totals

A minimal transfer record might look like this:

{
  network,
  txHash,
  logIndex,
  timestamp,
  tokenContract,
  from,
  to,
  amount,
  fee,
  confirmations
}
Enter fullscreen mode Exit fullscreen mode

Then classification becomes deterministic:

function classifyTransfer(tx, owned) {
  const fromOwned = owned.has(addressKey(tx.network, tx.from));
  const toOwned = owned.has(addressKey(tx.network, tx.to));

  if (fromOwned && toOwned) return "internal";
  if (fromOwned) return "outgoing";
  if (toOwned) return "incoming";
  return "external";
}
Enter fullscreen mode Exit fullscreen mode

Only incoming and outgoing principal movements should contribute to ordinary receipt and payment totals.

An internal movement should remain visible in the ledger for traceability, but its principal should not be added to revenue or expense.

4. Treat network fees separately

The principal transfer and the network fee are different accounting events.

For an internal transfer:

  • principal movement: internal, excluded from ordinary income/expense
  • network fee: possible operating cost, retained separately

Combining both into one signed amount makes later reconciliation harder.

5. Separate blockchain facts from business meaning

The blockchain can confirm:

  • timestamp
  • network
  • token contract
  • sender and recipient
  • amount
  • transaction hash
  • confirmation state

It cannot confirm whether the transfer represents a customer payment, supplier payment, refund, treasury movement, or test.

Keep business metadata in a separate layer:

{
  expectedAmount,
  counterpartyId,
  invoiceId,
  note,
  status: "reconciled" | "still_due" | "overpaid" | "review"
}
Enter fullscreen mode Exit fullscreen mode

This prevents a label correction from rewriting the underlying public transaction record.

6. Start with a bounded range

A safer onboarding sequence is:

  1. add one owned public address
  2. read a recent bounded period
  3. validate the network and token contract
  4. label known counterparties
  5. add the remaining owned wallets
  6. reclassify owned-to-owned transfers
  7. compare expected and actual payments
  8. import older history only when needed

This makes failures easier to isolate. A zero result might otherwise mean the wrong network, the wrong token contract, an incomplete provider response, or simply no activity.

7. Do not request wallet authority for reconciliation

This workflow uses public transaction data. It does not require a wallet connection, seed phrase, private key, or signing permission.

I built an interactive sample of the multi-wallet deduplication flow using example wallets and transfers. It can be reviewed without registration or a real address:

Open the no-sign-up multi-wallet example

Disclosure: I built U Ledger. The link is a product guide and sample workflow, not a trading, custody, exchange, or investment service.

Top comments (0)