The logic for upgrading customers lives within Stripe. You will first need to upgrade the customer within Stripe, and then Loop will ingest the updated and finalized invoice based on the upgrade that is trigged. When a customer is upgraded, there will likely be a prorated amount, where the customer is charged a percentage of a subscription’s cost to reflect partial use. Stripe will calculate this amount based on the rules you've configured in Stripe.


Charging immediately for prorated amounts

When you direct Stripe to bill immediately, it will create a draft that will not be finalized for 1 hour and the due date will be the end of the business day. You can, however, change the due date and finalization time. You can change the due_date on the invoice to the current timestamp with this method and force an invoice to finalize immediately with this method. Also, best practice is to set the due_date to now + 2 seconds to account for any processing time on Stripe's side (invoices cannot be due in the past). Be sure to keep automatic_collection:false so Loop can collect on the payment.

Once an invoice is finalized, Loop will immediately attempt to charge the customer. Stripe will mark the invoice as 'due' until it is paid onchain. This could take several minutes depending on the chain.


Payment Method Issues

There are two reasons that an invoice will remain open and unpaid

IssueResolution
Low balanceCustomer needs to increase their token balance
Low authorizationCustomer needs to increase their authorization

Loop will send the customer a "Missed Payment" email 5 minutes after the payment is due but not paid. Included in this email is an indication of why the payment was missed (e.g. low balance or low authorization) and a link to the Loop customer portal where the customer can increase their authorization.

Additionally, a late payment webhook 10 minutes after the payment is due but not paid.

Heading off payment issues

To avoid payment issues, you can query the wallet's balance and allowance and provide resolutions directly on your frontend.

Query wallets

You can retrieve from Loop the payment wallet, network, and token by calling the GET Agreements endpoint and passing in the externalSubscriptionId.

Low balance

You can query a wallet's balance before you upgrade a customer to ensure they have the correct amount.

Low authorization

You can query a wallet's authorization amount before you upgrade a customer to ensure they have enough. You can do this, by querying the allowance function of the payment token's smart contract on any EVM chain and the delegate amount on Solana (directions for this below)

function allowance(address owner, address spender) external view returns (uint256)
InputDescriptionWhere to find
Token contractThis is the address of the ERC-20 token contractNetworks & tokens page
SpenderThis is the address of your Loop smart contractDeveloper page on your dashboard or the GET Entities endpoint

Sample Ethers.js code

const { ethers } = require("ethers");

// Set up your provider (e.g., Infura, Alchemy, etc.)
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID");

// Token contract address
const tokenAddress = '0xTokenAddressHere';

// Spender address
const spenderAddress = '0xSpenderAddressHere';

// Wallet address (owner of the tokens)
const walletAddress = '0xYourWalletAddressHere';

// ERC-20 ABI with allowance function
const tokenABI = [
  "function allowance(address owner, address spender) view returns (uint256)"
];

// Create a contract instance
const tokenContract = new ethers.Contract(tokenAddress, tokenABI, provider);

// Get the allowance
async function checkAllowance() {
  try {
    const allowance = await tokenContract.allowance(walletAddress, spenderAddress);
    console.log(`Allowance: ${ethers.utils.formatUnits(allowance, <token decimals>)} tokens`);
  } catch (error) {
    console.error('Error:', error);
  }
}

checkAllowance();

Sample code on Solana

import {
  getAccount,
  getAssociatedTokenAddress,
  Connection
} from "@solana/spl-token";

import {
  PublicKey,
} from "@solana/web3.js";

const provider = new Connection(`${rpcUrl}/${providerApiKey}`);

const tokenAccount = await getAssociatedTokenAddress(
    new PublicKey(tokenAddress),
    new PublicKey(walletAddress)
);

const accountInfo = await getAccount(
    provider,
    tokenAccount
);

const delegatedAmount = acountInfo.delegatedAmount;

Resolutions

To increase the approval amount, you can construct the transaction yourself for the customer to increase their authorization on your site or you can direct them to the Loop customer portal where an end payer can manage their authorization and see the token balance of their payment token. When a subscription is updated in Stripe, that update will be reflected immediately on the Loop customer portal.

Increasing the authorization

You can call the approve function to increase the authorization. This function must be signed by the wallet that is setting this allowance.

function approve(address spender, uint256 amount) external returns (bool);

On Solana, the equivalent of the approve function in Ethereum is done via the approve instruction in the SPL Token Program. You authorize an account (the spender) to transfer a certain amount of tokens from your token account.

const {
  Connection,
  PublicKey,
  Keypair,
  Transaction,
  sendAndConfirmTransaction,
} = require('@solana/web3.js');

const { Token, TOKEN_PROGRAM_ID } = require('@solana/spl-token');

async function increaseTokenApproval() {
  // Set up your connection to Solana's mainnet
  const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');

  // Your wallet's keypair (Replace with your actual keypair)
  const wallet = Keypair.fromSecretKey(new Uint8Array([/* your private key array */]));

  // Token mint address for the SPL Token (replace with the token you want to use)
  const tokenMintAddress = new PublicKey('YourTokenMintAddressHere');

  // The token account where the tokens are stored
  const fromTokenAccount = new PublicKey('YourTokenAccountAddressHere');

  // Spender account (the account you're authorizing to spend tokens)
  const spenderAddress = new PublicKey('SpenderAccountAddressHere');

  // New approval amount (in token's smallest unit, e.g., 1000 tokens for a token with 9 decimals)
  const amountToApprove = 1000 * Math.pow(10, 9);  // For tokens with 9 decimals

  // Create the Token object for the mint address
  const token = new Token(connection, tokenMintAddress, TOKEN_PROGRAM_ID, wallet);

  // Create the approve instruction
  const approveIx = Token.createApproveInstruction(
    TOKEN_PROGRAM_ID, // SPL Token Program
    fromTokenAccount, // Source token account
    spenderAddress,   // Spender account
    wallet.publicKey, // Owner of the token account
    [],               // Multisig signers (if any)
    amountToApprove   // Amount to approve
  );

  // Create the transaction
  const transaction = new Transaction().add(approveIx);

  // Send the transaction
  try {
    const signature = await sendAndConfirmTransaction(connection, transaction, [wallet]);
    console.log('Transaction confirmed with signature:', signature);
  } catch (error) {
    console.error('Error sending transaction:', error);
  }
}

increaseTokenApproval();