Integration lessons
May 17, 2026 · View on GitHub
A running tally of the gotchas we hit bringing this sidecar up against Divigent on Base mainnet, in the hope that the next integrator skips them.
SDK behavior
MIN_DEPOSIT is $10 on-chain, errors are opaque
Divigent's router enforces a hard minimum at the contract level. As of
SDK 1.0.2 it surfaces as Divigent contract revert: InvalidAmount(). Read
it before smoke testing:
await publicClient.readContract({
address: '0xE958A89c2CCa697d4896990685800cc1D5AF2A01',
abi: [{ name: 'MIN_DEPOSIT', type: 'function', stateMutability: 'view',
inputs: [], outputs: [{ type: 'uint256' }] }],
functionName: 'MIN_DEPOSIT',
}); // → 10000000n (\$10)
A typed MinDepositError is planned for v1.1.
depositIdle silently no-ops below the threshold
Returns undefined whether the sweep landed below MIN_DEPOSIT or whether
there was simply nothing above the reserve floor. Indistinguishable for
dashboards. v1.1 will add a typed result.
Wallet must be initialized once before any deposit
ensureInitializedAndWait() on boot is idempotent (returns undefined
when already initialized). Without it, the first deposit reverts with
NotAuthorised().
Base USDC quirks
depositWithPermitAndWait could fail on permit fallback (≤ 1.0.1)
Resolved in 1.0.2. Our /deposit route still uses explicit
usdcAllowance check → approveUsdc(amount) → depositAndWait because:
- Combined with the pre-approval below, the
if (allowance < amount)branch almost never fires — so the explicit path is single-tx in practice. - The behavior is more predictable than relying on permit-fallback paths inside the SDK.
Pre-approve the router with a generous allowance
Without it, depositIdle() approves exactly the deposit amount, then the
router's deposit() reverts with transfer amount exceeds allowance on the
exact-equal-allowance edge case. The next sweep cycle succeeds because the
failed approve leaves allowance in place — but you get one sweep-failure
event per cycle. Run scripts/approve-router.mjs 10000 once at setup time
and every subsequent sweep is a single tx with zero failure events.
Divigent confirmed in 1.0.2 that pre-approval is the recommended pattern for seller-side sidecars, even though their fallback bug is fixed.
Operations
Public Base RPC will rate-limit you
At ~60 RPC reads/hour (sweep + snapshot tickers + open routes), the public
mainnet.base.org starts returning errors. Use Alchemy (10M/mo free tier
is plenty), QuickNode, or Coinbase Developer Platform.
macOS port 7000 is owned by Control Center
This sidecar defaults to SIDECAR_PORT=7100. macOS Sonoma+ binds 7000 to
AirPlay Receiver. lsof -nP -iTCP:7000 -sTCP:LISTEN will show ControlCe.
Express can't serialize bigint by default
The Divigent SDK returns USDC amounts as bigint, but JSON.stringify
throws on them. The canonical workaround is at the top of src/index.ts:
(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () {
return this.toString();
};
Time Machine backs up your .env by default
Excluded the file explicitly:
sudo tmutil addexclusion /path/to/.env
Architecture
Split-process sellers can't use attachToResourceServer
Divigent's docs recommend attachToResourceServer(server, config) for the
seller side. That requires a TS-resident x402ResourceServer instance. If
your resource server lives in Python (FastAPI), Go, Rust, or anywhere else,
the canonical fallback is divigent.depositIdle({ wallet, minIdleThreshold, onIdleDeposit, ... }) on a ticker. That's exactly what this sidecar does.
Ticker overlap on slow RPC
A sweep tx can take 10–20 seconds (preview + simulate + broadcast + wait).
At a 5-minute interval that's normally fine, but a chain stall plus the
snapshot ticker firing concurrently can put two RPC writes in flight. Guard
each ticker with a busy flag (or chain promises).
Graceful shutdown matters under launchd / systemd
SIGTERM during an in-flight sweep tx → the tx broadcasts and mines, but the
SDK callback never fires → your backend never records the deposit event.
This sidecar drains in-flight work for up to 30s before process.exit(0).
Bearer auth even on localhost
The sidecar binds 127.0.0.1 by default, but local malware reaches
localhost trivially. SIDECAR_API_TOKEN is required for /deposit,
/withdraw, and /sweep. Compared via crypto.timingSafeEqual.
Token-bucket rate limit on open routes
/balance and /preview/withdraw each make an RPC call. Without a limit,
a same-host process can exhaust your Alchemy quota. Global 60/min bucket
in src/routes.ts.
Things that worked well (no fix needed, just noting)
ensureInitializedAndWaitidempotency — safe to call on every boot.IdleDepositContextpayload includes everything a dashboard needs:walletBalance,reserveFloor,idleAmount,txHash,dedupeKey. No transformations required to display it.previewWithdrawNetcorrectly converts target USDC → shares, including all of Divigent's internal math. Don't try to compute it yourself.Positionfield accrual —accruedYieldticks up live without polling any extra endpoint.- viem typing across the SDK boundary is clean — no
anycasts needed in well-written integration code.