How Equillar Ensures Payment Capacity for Investment Contracts

Published: (January 10, 2026 at 05:21 AM EST)
6 min read
Source: Dev.to

Source: Dev.to

One of the most critical aspects of any investment platform is ensuring that investors receive their payments reliably and on time. At Equillar, we have implemented an automated system that constantly verifies the payment capacity of each active investment contract. In this article, we will explore how this mechanism works internally.

Why is it important to verify payment capacity?

Imagine you have an active investment contract with multiple investors waiting for their monthly returns. The contract might be functioning correctly, receiving funds and processing operations, but what happens if, at some point, there aren’t enough funds in the reserve to cover the next payments?

To avoid these situations as much as possible, we have opted for the following approach: we try to detect these problems before they occur, allowing organizations to take corrective action in time.

The verification flow: a three‑phase architecture

Our payment‑capacity verification system operates in three phases, using an architecture based on commands, asynchronous messages, and concrete services.

Phase 1: Identifying contracts to verify

It all starts with a scheduled command that runs periodically:

php bin/console app:contract:check-payment-availability

This command (CheckContractPaymentAvailabilityCommand) is the entry point of our verification system. Its logic is simple and focused:

  • Selects eligible contracts – Not all contracts need constant verification. The command only searches for those in operational states:

    • ACTIVE – Active contracts receiving investments
    • FUNDS_REACHED – Contracts that reached their funding goal
    • PAUSED – Contracts temporarily paused but still having payment obligations
  • Creates a verification record – For each selected contract, the system creates a ContractPaymentAvailability entity. This record acts as a verification “ticket” that stores:

    • The contract to verify
    • When the request was created
    • The process status (pending, processed, failed)
    • The verification results
  • Queues the work – Instead of processing everything synchronously (which could block the system for minutes), the command queues each verification as an asynchronous message through Symfony Messenger.

This design is important because it allows the command to finish quickly, while the heavy work is processed in the background. If you have 50 active contracts, the command simply creates 50 “tickets” and queues them, returning control in seconds.

Phase 2: Asynchronous processing

Once the messages are in the queue, Symfony Messenger workers come into play. Each message of type CheckContractPaymentAvailabilityMessage is processed by its corresponding handler (CheckContractPaymentAvailabilityMessageHandler).

The handler acts as a minimalist coordinator:

public function __invoke(CheckContractPaymentAvailabilityMessage $message): void
{
    $contractPaymentAvailability = $this->contractPaymentAvailabilityStorage
        ->getById($message->contractPaymentAvailabilityId);

    try {
        $this->contractCheckPaymentAvailabilityService
            ->checkContractAvailability($contractPaymentAvailability);
    } catch (ContractExecutionFailedException $e) {
        // The exception is handled silently because the state 
        // has already been recorded in the database
    }
}

This pattern of “delegating to the specialized service” is a good practice: the handler only deals with infrastructure (retrieving data, capturing errors), while the business logic lives in the service.

Phase 3: Blockchain interaction

This is where the most interesting part of the process occurs. The ContractCheckPaymentAvailabilityService is the core of our verification system, and it interacts directly with smart contracts on the Stellar blockchain.

Step‑by‑step process

  1. Smart‑contract invocation

    The service calls a specific smart‑contract function (check_reserve_balance) that is deployed on the blockchain:

    $trxResponse = $this->checkContractPaymentAvailabilityOperation
        ->checkContractPaymentAvailability($contractPaymentAvailability);

    This function performs on‑chain calculations: it checks how many investors there are, what the next pending payments are, how much is in the reserve fund, and calculates if there’s a deficit.

    The code for this function in the contract (written in Rust for Soroban) is as follows:

    pub fn check_reserve_balance(env: Env) -> Result {
        require_admin(&env);
    
        let claims_map: Map = get_claims_map_or_new(&env);
        let project_balances: ContractBalances = get_balances_or_new(&env);
        let mut min_funds: i128 = 0;
    
        for (_addr, next_claim) in claims_map.iter() {
            if next_claim.is_claim_next(&env) {
                min_funds += next_claim.amount_to_pay;
            }
        }
    
        if min_funds > 0 {
            if project_balances.reserve   
  2. Result processing

    (The original text cuts off here; continue with the description of how the service interprets the returned value, updates the ContractPaymentAvailability entity, and triggers any necessary alerts or corrective actions.)

Smart Contract Reserve‑Fund Verification Workflow

The smart‑contract response arrives in XDR format (a binary format used in Stellar). We decode it with the Soneso PHP Stellar SDK:

$trxResult = $this->scContractResultBuilder
    ->getResultDataFromTransactionResponse($trxResponse);

$requiredFunds = $this->i128Handler
    ->fromI128ToPhpFloat(
        $trxResult->getLo(),
        $trxResult->getHi(),
        $contractPaymentAvailability->getContract()
            ->getToken()
            ->getDecimals()
    );

The $requiredFunds value is crucial:

  • 0 – everything is fine.
  • > 0 – indicates exactly how much money is missing from the reserve fund to cover the next payments.

3. Transaction Recording

Each blockchain interaction is recorded:

$contractTransaction = $this->contractTransactionEntityTransformer
    ->fromSuccessfulTransaction(
        $contractPaymentAvailability->getContract()->getAddress(),
        ContractNames::INVESTMENT->value,
        ContractFunctions::check_reserve_balance->name,
        [$trxResult],
        $trxResponse->getTxHash(),
        $trxResponse->getCreatedAt()
    );

The resulting ContractTransaction provides:

  • Complete traceability – we can audit each verification.
  • Transaction hash – verifiable on Stellar.
  • Exact timestamp – we know when it occurred.
  • Result – what the smart contract returned.

4. State Update

If a deficit is detected ($requiredFunds > 0), the system blocks the contract:

if ($requiredFunds > 0) {
    $contract = $contractPaymentAvailability->getContract();
    $this->contractEntityTransformer->updateContractAsBlocked($contract);
    $this->persistor->persist($contract);
}

The contract moves to BLOCKED state, which:

  • Alerts administrators.
  • Shows in the UI that the contract needs attention.
  • Records the exact amount that must be added to the reserve fund.

UI Warning

In assets/react/controllers/Contract/ViewContract.tsx the warning is displayed as:

{query.data.requiredReserveFunds && query.data.requiredReserveFunds > 0 ? (
    
        ⚠️ The contract requires adding {formatCurrencyFromValueAndTokenContract(
            query.data.requiredReserveFunds,
            query.data.tokenContract
        )} to the reserve fund to ensure investor payments.
    
) : (
    
        ✓ The contract has capacity to serve payments.
    
)}

Future work: add email notifications to alert administrators as soon as a deficit is detected.

5. Error Handling

If something fails (network down, smart‑contract error, etc.), we record it in a controlled manner:

catch (TransactionExceptionInterface $ex) {
    $contractTransaction = $this->contractTransactionEntityTransformer
        ->fromFailedTransaction(
            $contractPaymentAvailability->getContract()->getAddress(),
            ContractNames::INVESTMENT->value,
            ContractFunctions::check_reserve_balance->name,
            $ex
        );

    // Mark the verification as failed
    $this->contractPaymentAvailabilityTransformer
        ->updateContractPaymentAvalabilityAsFailed(
            $contractPaymentAvailability,
            $contractTransaction
        );
}

The ContractPaymentAvailability Entity – Historical Record

Each verification is persisted in the database via the ContractPaymentAvailability entity:

class ContractPaymentAvailability
{
    private ?int $id;
    private ?Contract $contract;                     // Which contract was verified?
    private ?float $requiredFunds;                  // Missing amount (if any)
    private ?ContractTransaction $contractTransaction; // Associated blockchain transaction
    private ?\DateTimeImmutable $checkedAt;         // When it was verified
    private ?\DateTimeImmutable $createdAt;         // When it was requested
    private ?string $status;                        // PENDING, PROCESSED, FAILED
}

This structure enables us to:

  • View the full verification history of a contract.
  • Identify patterns (e.g., frequent fund shortages).
  • Measure processing times.
  • Generate reports and metrics.

Advantages of This Architecture

  1. Scalability – Asynchronous processing lets us verify many contracts without impacting the main application. Each verification runs in its own context.
  2. Resilience – A failure in one verification does not affect others; we can retry failed jobs later.
  3. Transparency – Interacting directly with on‑chain smart contracts makes calculations verifiable and immutable. Anyone can audit the logic.
  4. Separation of Concerns
    • Command – identifies work.
    • Handler – deals with the messaging infrastructure.
    • Service – contains business logic.
    • Entities – model the data.

Each component has a clear purpose and can evolve independently.

Conclusion

We have demonstrated a hybrid off‑chain/on‑chain solution that:

  • Continuously monitors reserve‑fund health.
  • Records every verification on the blockchain and in our database.
  • Reacts automatically when a deficit is detected, blocking the contract and notifying stakeholders.

This approach delivers scalability, resilience, transparency, and a clean separation of concerns, laying a solid foundation for future enhancements such as automated email alerts and richer reporting.

Off‑Chain & On‑Chain Interaction

The system combines Symfony with smart‑contract interaction to check the payment capacity of investment contracts.

  • Off‑chain component – handles scheduling, queueing, and state management.
  • On‑chain component – performs the actual financial calculations on the Stellar blockchain.

This approach lets us leverage the best of both worlds:

  • Flexibility & familiarity of a traditional PHP framework for business logic.
  • Transparency & immutability of blockchain for critical financial verifications.

What’s Next?

In future articles we will explore other aspects of the Equillar platform, such as:

  • Payment processing
  • Reserve‑fund management
  • Integration with the Stellar network

Stay tuned!

Technical note
This article describes the internal functioning of the system at the time of writing. Implementation details may evolve over time, but the fundamental architecture principles remain.

Back to Blog

Related posts

Read more »

토스의 새로운 얼굴 만들기

!https://static.toss.im/ipd-tcs/toss_core/live/78215702-c6f2-4af4-b44c-35cfd4c4fa62/tech-persona-01.png 토스에서 이런 얼굴을 마주한 적이 있나요? 안내 문구나 고객센터처럼 사용자와 직접 마주하는 화면에는...