Powered by Dodo Payments
Treasury payment rails — HMAC-signed invoice webhooks, slot-rate pre-warm, same-slot Solana settlement.
How it works
Webhook
invoice.paid
HMAC verify
5-step gate
Pre-warm
slot-rate formula
Tx
SPL transfer · same slot
Webhook payload schema
Dodo signs the canonicalized body with HMAC-SHA256 using the shared secret. Atlas rejects any payload that fails the 5-step verification or replays an already-seen invoice_id.
#[derive(Deserialize)]
pub struct DodoWebhookPayload {
pub event: String, // "invoice.paid"
pub invoice_id: String, // "inv_8c2..."
pub amount: u64, // base units
pub mint: Pubkey, // PUSD / USDC mint
pub destination: Pubkey, // treasury ATA
pub memo: Option<String>,
pub signed_at: i64, // unix seconds — replay window anchor
pub signature: String, // HMAC-SHA256 hex of canonicalized body
}5-step verification
- 1
Canonicalize JSON
Sort keys, drop whitespace. Hash input is the canonical byte string, not the wire body.
- 2
HMAC-SHA256 compute
Compute HMAC-SHA256(secret, canonical_body) once. Reused for verify + audit log.
- 3
Constant-time compare
Use subtle::ConstantTimeEq against the supplied hex signature — never == on bytes.
- 4
10-minute replay window
Reject if now() - signed_at > 600s. Clock skew tolerance ±60s baked in.
- 5
Idempotency check
Dedup by invoice_id in a 24h LRU. Duplicate webhooks accept silently — no double-settle.
Slot-rate pre-warm formula
The pre-warm target is the floor of the buffer ratio plus the slot-rate term — the yield Atlas expects to capture before the obligation deadline. This keeps the rebalance window open even when the idle buffer alone is short.
prewarm_target_pct = min( 100, ⌊ (idle_buffer ÷ obligation) × 100 ⌋
+ slot_rate_bps ÷ 100 )
slot_rate_bps = EMA₆₄(yield_bps_per_slot) × slots_until_deadlineEMA₆₄— 64-slot exponential moving average of realised yield bps per slot.slots_until_deadline— slots betweennow()and the payout's settlement target.- Both terms are unit-checked; floors prevent off-by-one over-warming.
6 failure modes
| Mode | Outcome | Reason |
|---|---|---|
| bad signature | reject | constant-time compare failed |
| stale signed_at | reject | outside 10-minute replay window |
| duplicate invoice_id | accept · no-op | idempotent settlement |
| forbidden mint | reject | mint not in vault allowlist |
| insufficient liquidity | defensive pre-warm | pre-warm capped at idle buffer cap |
| Solana RPC degraded | retry · exponential | resubmits same canonical body |