See also: API Reference Examples — executable TypeScript walkthroughs.
The Business
Karma Automotive sells luxury electric vehicles. Customers place prepayment deposits when ordering a vehicle, with the full balance due before delivery. If the vehicle is not delivered within the agreed timeframe (e.g. 6 months), the customer is entitled to a full refund of their deposit.Why Oak?
Karma Automotive needs:- Time-constrained escrow — funds are locked with a hard deadline; if delivery doesn’t happen by the deadline, the buyer is automatically protected
- Structured payments — line items for base price, options packages, and delivery fees
- Automatic expiry protection — after the deadline + claim delay, expired funds can be swept back to the buyer
- Transparent fee tracking — dealer fees, protocol fees, all visible on-chain
- Trust for high-value transactions — a $50,000+ vehicle deposit requires stronger guarantees than a traditional wire transfer
Oak Contract Used
| Contract | Purpose |
|---|---|
| CampaignInfoFactory | Creates the CampaignInfo contract that holds NFT receipts and the accepted token list |
| TreasuryFactory | Deploys the TimeConstrainedPaymentTreasury clone linked to the CampaignInfo |
| TimeConstrainedPaymentTreasury | Identical to PaymentTreasury in its SDK interface (both use oak.paymentTreasury()), but enforces launch-time and deadline constraints on-chain. After the deadline passes and the claim delay expires, claimExpiredFunds becomes callable |
Multi-token support
Like PaymentTreasury, the time-constrained variant is multi-token:paymentToken must be accepted for the campaign, and all pending / confirmed / fee accounting is per token address in that token’s decimals. The Karma example uses USDT only as a familiar stablecoin; deposits and claimNonGoalLineItems can use any accepted ERC-20 from the campaign whitelist. Whitelist source: GlobalParams (getTokensForCurrency / owner addTokenToCurrency); per-campaign cache: campaign.getAcceptedTokens().
Roles
| Role | Who | On-Chain Functions |
|---|---|---|
| Platform Admin | Karma’s ordering system | createPayment, createPaymentBatch, confirmPayment, confirmPaymentBatch, cancelPayment, claimRefund(paymentId, address) (non-NFT), claimExpiredFunds, claimNonGoalLineItems, pauseTreasury, unpauseTreasury, cancelTreasury |
| Platform Admin or Campaign Owner | Karma or dealer | withdraw, cancelTreasury |
| Buyer | Vehicle customer | ERC-20 approve, processCryptoPayment, claimRefundSelf(paymentId) (NFT payments) |
| Dealer (Campaign Owner) | Karma dealership | Receives funds after withdraw |
| Protocol Admin | Oak protocol | Receives protocol fees (via disburseFees) |
| Any caller | Anyone | disburseFees, all read functions (getPaymentData, getRaisedAmount, getExpectedAmount, paused, etc.) |
Note on time constraints: Unlike the standard PaymentTreasury,createPayment,createPaymentBatch,processCryptoPayment,cancelPayment,confirmPayment, andconfirmPaymentBatchmust be called while the current time is withinlaunchTime…deadline + bufferTime(perTimestampChecker).claimRefund,claimRefundSelf,claimExpiredFunds,disburseFees,withdraw, andclaimNonGoalLineItemsrequire the current time to be afterlaunchTime(they use_checkTimeIsGreater()).
Integration Flow
Step 1: Create a CampaignInfo contract
Role: Any caller — createCampaign is permissionless.
Before deploying a TimeConstrainedPaymentTreasury, Karma needs a CampaignInfo contract. This holds NFT receipts for crypto payments and defines the accepted token list.
Step 2: Deploy the TimeConstrainedPaymentTreasury
Role: Any caller — deploy on TreasuryFactory is permissionless (the implementation must have been registered and approved during platform onboarding).
Karma deploys a TimeConstrainedPaymentTreasury linked to the CampaignInfo from Step 1. The time constraints (launch time and deadline) are enforced on-chain by the contract.
Step 3: Customer orders a vehicle — two independent payment flows
James orders a Karma GS-6 electric sedan with the Performance Package. The total prepayment is $52,500 broken down into line items. Karma supports two payment methods — they are not sequential steps:Flow A: Off-chain / fiat payment (createPayment)
Role: Platform Admin — only the platform admin can create payment records. Must be called within the time window (Karma’s system creates a payment record on-chain. ThelaunchTimetodeadline + bufferTime).
createPayment transaction does not pull ERC-20 from the buyer’s wallet — it records the order and pending accounting. James pays through off-chain rails (wire transfer, dealership financing, etc.). Before confirmPayment, the treasury must hold enough of the payment token on-chain (for example the platform deposits stablecoins after settlement). The contract checks the treasury balance when confirming.
createPayment, fund the treasury with the agreed token amount before calling confirmPayment (operational path is product-specific).
Confirm after delivery (platform admin)
Role: Platform Admin — confirmPayment must still be within the launch…deadline+buffer window.
The GS-6 is manufactured and delivered to James. After the treasury holds the required tokens and Karma verifies delivery (still within the time window):
Flow B: On-chain crypto payment (processCryptoPayment)
Role: Any caller — processCryptoPayment is permissionless, but the buyer must first approve the treasury to transfer their ERC-20 tokens. Must be called within the time window.
This is a standalone operation — it creates the payment record AND transfers ERC-20 tokens in a single transaction. It does not require or complete a prior createPayment call. An NFT is minted to James as proof of payment.
James transfers the full prepayment amount. Before the treasury can pull funds, James must grant an ERC-20 allowance:
Step 4: Monitor the order status
Role: Any caller — all read functions are public.Karma’s dashboard tracks the prepayment and treasury health.
Step 5 (Success): Vehicle delivered — disburse and withdraw
Any caller fordisburseFees(afterlaunchTime). Platform Admin or Campaign Owner forwithdraw(afterlaunchTime). For Flow A, you already calledconfirmPaymentunder Step 3 after delivery; for Flow B, the payment was confirmed whenprocessCryptoPaymentran—do not callconfirmPaymenthere.
Step 5 (Failure): Claim window after deadline — platform sweeps expired funds
Role: Platform Admin — only the platform admin can callIf the vehicle is not delivered and funds remain in the treasury past the campaign deadline plus the configured claim delay, Karma’s backend can sweep idle balances on-chain. The contract transfers swept amounts to the platform admin and protocol admin addresses (seeclaimExpiredFunds. Callable only aftercampaignDeadline + platformClaimDelay, and only afterlaunchTime(time-constrained variant).
ExpiredFundsClaimed). Consumer-facing refunds to James are then handled by Karma’s policy and ops (off-chain settlement or a follow-on transfer), not by a single claimExpiredFunds transfer directly to the buyer wallet in the base contract logic.
Alternative: Refunds before or after the claim window
A) Cancel unconfirmed off-chain payment (beforeconfirmPayment):
Role: Platform Admin for cancelPayment (within the launch…deadline+buffer window). This clears pending accounting only; it does not automatically return ERC-20 already sent to the treasury—handle recovery operationally if you deposited before cancelling.
Role: Platform Admin forclaimRefund(paymentId, refundAddress)(afterlaunchTime). This refunds a confirmed payment where no NFT was minted. The contract verifies the payment is confirmed and hastokenId == 0.
Role: Any caller (NFT owner) —Before callingclaimRefundSelf(paymentId)looks up the current NFT owner, burns the NFT, and sends the refundable amount to that owner (afterlaunchTime). No priorcancelPaymentis needed — crypto payments cannot be cancelled viacancelPayment(they are auto-confirmed on creation).
claimRefundSelf, the NFT owner must approve the treasury to manage the NFT. All pledge NFTs live on the CampaignInfo contract (not the treasury itself), so approval uses the CampaignInfo SDK entity:
Claim non-goal line items
Role: Platform Admin — only the platform admin can claim non-goal line items (after launchTime).
If the prepayment used line items that do not count toward the campaign goal (e.g., processing fees), the platform admin can claim accumulated non-goal balances per token.
Batch operations
Role: Platform Admin — batch create and confirm are platform-admin-only (within the launch…deadline+buffer window).
Pause, unpause, or cancel the treasury
Pause / unpause:Role: Platform Admin — only the platform admin can pause and unpause.
Role: Platform Admin or Campaign Owner — either party can cancel (same override pattern as PaymentTreasury).
Architecture Diagram
Karma Automotive Prepayment FlowKey Takeaways
- ERC-20 approval is required — James must
approvethe treasury beforeprocessCryptoPaymentcan pull tokens createPaymentpath — fund the treasury beforeconfirmPayment(createPaymentdoes not pull from the buyer)processCryptoPaymentpath — do not callconfirmPaymentafterward; usedisburseFees/withdrawwhen appropriate- Multi-token — use any accepted
paymentTokenfor the campaign; balances and sweeps are per ERC-20 - Time gates are enforced on-chain — create/confirm/cancel/pay paths must occur within
launchTime…deadline + bufferTime; refunds, fee disbursement, withdrawal, non-goal claims, and expired sweeps require time afterlaunchTime - Same SDK interface as PaymentTreasury —
oak.paymentTreasury()works for both; behavior differs in the deployed contract bytecode claimExpiredFunds()is platform-admin-only and only afterdeadline + platformClaimDelay; on-chain recipients are the platform and protocol admins — align customer refunds with your product policy- Role-based access — matches PaymentTreasury for admin-only writes;
withdrawis platform admin or campaign owner;disburseFeesis permissionless - Three cancellation/refund paths —
cancelPaymentdeletes unconfirmed off-chain records (no on-chain refund);claimRefund(paymentId, address)refunds confirmed non-NFT payments (platform admin);claimRefundSelf(paymentId)refunds crypto/NFT payments directly (NFT owner, no prior cancel needed; requires prior ERC-721 approval on CampaignInfo) - High-value transactions benefit from deterministic rules instead of informal wire holds
- Line items provide a clear audit trail (base price vs. options vs. delivery)
- Batch, pause, cancel, and
claimNonGoalLineItemsbehave like PaymentTreasury but inherit the same time checks fromTimeConstrainedPaymentTreasury