Syncing Onchain Subscription Events to an Internal Ledger
Learn how to sync onchain USDC subscription billing events from Base, Ethereum, and Polygon to your internal ledger, with real examples and reconciliation tips.

Recurring subscription billing in USDC gives your SaaS business onchain transparency and control. But to keep your accounting clean, you need to sync every onchain event, invoice, attempt, settlement, failure, to your internal ledger. This lets you track revenue, customer status, and handle edge cases like partial payments or retries.
The Core Events: What to Sync
Onchain recurring USDC billing generates a series of events. At minimum, you will need to track:
- Invoice created: The intent to bill a customer for a given period and amount.
- Payment attempt: An onchain transaction broadcast to collect the amount, using an approved cap.
- Payment settled: The transaction is confirmed and the funds move from the customer wallet to your address.
- Payment failed: The attempt did not settle, due to insufficient funds, cap exceeded, or a rejected transaction.
- Cap changes: If the customer increases, decreases, or revokes a cap, you may need to adjust future billing logic.
Most teams will want this reflected in an internal ledger. The ledger is your source of truth for revenue, customer liability, and reporting. It is common to treat the ledger as append-only, with every event logged and immutable.
Mapping Onchain Events to Ledger Entries
Each event maps to a row or document in your ledger system. The following is a typical pattern:
invoice_id: Unique identifier, matching the onchain invoice or subscription cycle.customer_id: Your internal customer identifier.event_type: Enum or string (e.g., INVOICE_CREATED, PAYMENT_ATTEMPTED, PAYMENT_SETTLED, PAYMENT_FAILED).amount: In USDC, matching the onchain value.chain: ‘ethereum’, ‘base’, or ‘polygon’.tx_hash: For onchain events, the transaction hash. Blank for offchain events like invoice creation.timestamp: When the event occurred (UTC is standard).meta: Freeform JSON for extra context (e.g., error message, cap value, wallet address).
Each time Reclack emits an event, you insert a new row. Never overwrite settled entries. This guarantees a full audit trail.
Example: Ledger Row for a Settled Payment
{
"invoice_id": "inv_20240601a",
"customer_id": "cus_abc123",
"event_type": "PAYMENT_SETTLED",
"amount": "100.00",
"chain": "base",
"tx_hash": "0x5ff0...c2fa",
"timestamp": "2024-06-01T14:22:30Z",
"meta": {
"settlement_address": "0xmerchant...e12",
"customer_wallet": "0xcustomer...45f",
"block_number": 12844000
}
}
Syncing Events: Webhooks, Polling, and Idempotency
There are two main approaches to getting events into your ledger:
- Webhooks: Reclack can send POST requests to your endpoint when an event occurs. This is real-time and requires your endpoint to be reliable and idempotent (safe to retry).
- Polling: If you need extra control, you can poll Reclack’s API for recent events or directly index the blockchain for relevant transfers and approvals, then map them to your invoice logic.
Most teams start with webhooks for real-time sync. Ensure your endpoint:
- Authenticates requests (HMAC or bearer token)
- Writes events idempotently (by tx_hash or event ID)
- Handles out-of-order delivery, since blockchain finality is not always time-ordered
Idempotency Example (Python Pseudocode)
def handle_event(event):
if ledger.has_entry(event["tx_hash"]):
return # Already processed
ledger.insert(event)
This pattern prevents duplicate revenue entries if you receive the same event more than once.
Handling Edge Cases: Partial Payments, Failures, and Cap Changes
Partial Payments
A customer may only have enough USDC for part of a billing period. In this case, the onchain payment settles for less than the invoice amount. Your ledger should record both the amount attempted and the amount settled. You may need to carry the outstanding balance to the next period or trigger a service downgrade.
Failed Payments
A failed payment attempt (insufficient funds, cap exceeded, or user rejection) should not be logged as revenue. Instead, log it as a PAYMENT_FAILED event, with details in the meta field. This keeps your revenue numbers clean and lets you trigger reminders or dunning workflows.
Cap Changes
If a customer lowers or revokes their recurring approval, record this as a CAP_CHANGED or CAP_REVOKED event. This helps you anticipate failed collections before they happen and gives your support team context.
Worked Example: A Monthly SaaS Subscription
Suppose you charge $50 USDC per month per seat. Customer A has 2 seats. Their wallet has $120 USDC. The billing cycle runs as follows:
- Invoice created
invoice_id: inv_20240601aamount: 100.00
- Payment attempt
- On June 1, you trigger an onchain collection for $100.
- Payment settled
- The transaction settles, $100 USDC moves from customer to merchant. Ledger entry logs
PAYMENT_SETTLEDwithtx_hash.
- The transaction settles, $100 USDC moves from customer to merchant. Ledger entry logs
- Invoice closed
- No outstanding balance, subscription continues.
The next month, customer forgets to top up, so wallet has $60 USDC. You bill $100 again:
- Invoice created
invoice_id: inv_20240701aamount: 100.00
- Payment attempt
- On July 1, trigger collection for $100.
- Partial settlement
- Only $60 settles. Ledger logs
PAYMENT_SETTLEDfor $60, andPAYMENT_FAILEDfor the remaining $40. Outstanding balance is $40.
- Only $60 settles. Ledger logs
- Follow-up
- Automated reminder sent to customer. Next billing cycle, you can attempt to collect the outstanding $40 with the new invoice, or handle it as a separate charge.
Reconciling Your Ledger with Onchain Data
To keep your accounting accurate, reconcile your ledger with the blockchain:
- Periodically fetch all relevant transfer events to your merchant address.
- Match each
tx_hashto a ledger entry. - Flag any onchain payments not yet in the ledger as exceptions for review.
- For USDC on Base, Ethereum or Polygon, use each chain’s block explorer or API to pull ERC-20 Transfer events.
This process catches missed webhook events or manual payments sent outside your billing platform.
Sample SQL Query: Check for Missing Settlements
SELECT t.tx_hash, t.amount, t.block_time
FROM onchain_transfers t
LEFT JOIN ledger l ON t.tx_hash = l.tx_hash
WHERE l.tx_hash IS NULL
AND t.to_address = '0xmerchant...e12'
AND t.token = 'USDC'
ORDER BY t.block_time DESC;
Rolling Up Revenue, Refunds, and Subscription Status
With every relevant event in your ledger, you can: - Roll up settled payments by customer and period for revenue recognition - Track outstanding balances by invoice - Generate customer statements - Flag accounts for dunning or offboarding if payments repeatedly fail - Drive dashboards and ops reporting from the same source of truth
How We Handle Ledger Sync at Reclack
We designed Reclack so you can sync every onchain billing event, invoice, attempt, settlement, cap change, directly to your internal ledger. We never hold funds and never ask for private keys. Our webhooks are idempotent, signed, and include all the context you need to match events to invoices and customer records. You can learn more about our approach to billing event sync at reclack.com.


