Dividends

How Dinari calculates and distributes dividends to dShare holders, and how to reconcile them in your integration.

Dividends

Holders of any dShare token backed by a dividend-issuing security receive dividends. Cash dividends are the supported, fully automated dividend type at Dinari. They are detected, calculated, and distributed end-to-end without manual intervention.


How it works at Dinari

sequenceDiagram
    autonumber
    participant I as Issuer
    participant D as Dinari
    participant C as Chain
    participant P as Partner
    participant U as End user
 
    I->>D: Declares dividend<br/>(record, ex, pay dates)
    D->>D: Ingests and classifies<br/>as cash_dividend
    P->>D: GET /market_data/stocks/{id}/dividends
    D-->>P: Announcement:<br/>cash_amount, ex_dividend_date, pay_date
    P->>U: Surface upcoming dividend
 
    I->>D: Fiat payment settles<br/>from the underlying
    D->>D: Calculate per-holder amount<br/>(minimum $0.10)
    D->>C: Distribute USD+ to dShare holders<br/>(or dShare into wrapped dShares)
 
    P->>D: GET /accounts/{id}/dividend_payments
    D-->>P: DividendPayment records
    P->>U: Credit user and notify

Qualification

To be eligible for a dividend, all of the following must be true:

  • The wallet is registered with an account
  • The account is registered with an entity
  • The entity is qualified with a valid KYC

Distribution

Once the fiat payment from the underlying is confirmed, distribution runs shortly after. The amount per holder is calculated, and the form it arrives in depends on how the dShares are held:

Held typeReceive dividend as
dShareUSD+
Wrapped dShareUnderlying dShare, deposited into the wrapped dShare

Supported chains

Chain nameChain ID
Mainnet1
Base8453
Arbitrum42161
Avalanche C-Chain43114
Plume98866

Dividend types

Announcement data distinguishes two types:

TypeMeaning
CDDividends that have been paid and/or are expected to be paid
on consistent schedules
SCSpecial Cash dividends that have been paid that are infrequent or unusual, and/or can not be expected to occur in the future

Both are cash distributions and follow the same path.


Integration guide to dividends

Dividends require no on-chain action from you. Dinari takes the snapshot, calculates entitlements, and distributes them. Your integration is read-only so you are only responsible for confirming a user qualifies before the snapshot, then reconcile what was paid.

To integrate dividends, complete three steps:

  1. Find the dividend -fetch announcements and identify the ex-dividend and pay dates.
  2. Reconcile the payment - after the pay date, list what was paid and confirm the cash balance moved.

Step 1: Find the dividend

Initialize the client

import Dinari from '@dinari/api-sdk';
 
const client = new Dinari({
  apiKeyID: process.env['DINARI_API_KEY_ID'], // This is the default and can be omitted
  apiSecretKey: process.env['DINARI_API_SECRET_KEY'], // This is the default and can be omitted
  environment: 'sandbox', // defaults to 'production'
});
 
const accountID = 'your-account-id';
const entityID = 'your-entity-id';
const stockID = 'stock-id-here';
import os
from dinari_api_sdk import Dinari
 
client = Dinari(
    api_key_id=os.environ.get("DINARI_API_KEY_ID"),  # This is the default and can be omitted
    api_secret_key=os.environ.get("DINARI_API_SECRET_KEY"),  # This is the default and can be omitted
    environment="sandbox", # defaults to "production"
)
 
ACCOUNT_ID = "account_xxx"  # Replace with the actual Account ID
ENTITY_ID = "entity_xxx"    # Replace with the actual Entity ID
STOCK_ID = "stock_xxx"      # Replace with the actual Stock ID

Fetch announced dividends and select the next one

Announcements describe what the underlying issuer declared — use them for dates and estimates, not as a record of payment. Sort by ex-dividend date and take the next one that has not yet passed.

const dividends = await client.v2.marketData.stocks.retrieveDividends(stockID);
const today = new Date().toISOString().slice(0, 10);
 
const next = dividends
  .filter((d) => d.ex_dividend_date >= today)
  .sort((a, b) => a.ex_dividend_date.localeCompare(b.ex_dividend_date))[0];
 
if (!next) {
  console.log('No upcoming dividend announced for this stock.');
} else {
  const kind = next.dividend_type === 'SC' ? 'special' : 'regular';
  console.log(
    `${next.ticker}: ${next.cash_amount} ${next.currency} (${kind}) — ` +
    `record ${next.record_date}, ex-date ${next.ex_dividend_date}, pays ${next.pay_date}`
  );
}
from datetime import date
 
dividends = client.v2.market_data.stocks.retrieve_dividends(
    stock_id=STOCK_ID  # Replace with the actual Stock ID
)
today = date.today().isoformat()
 
upcoming = sorted(
    (d for d in dividends if d.ex_dividend_date >= today),
    key=lambda d: d.ex_dividend_date,
)
next_dividend = upcoming[0] if upcoming else None
 
if next_dividend is None:
    print("No upcoming dividend announced for this stock.")
else:
    kind = "special" if next_dividend.dividend_type == "SC" else "regular"
    print(f"{next_dividend.ticker}: {next_dividend.cash_amount} "
          f"{next_dividend.currency} ({kind}) — "
          f"record {next_dividend.record_date}, "
          f"ex-date {next_dividend.ex_dividend_date}, "
          f"pays {next_dividend.pay_date}")

Step 2: Check eligibility

Confirm the holding and estimate the entitlement

Balances are returned per chain, so sum across them rather than reading the first match. Distributions below $0.10 are not paid, so check that threshold too.

const portfolio = await client.v2.accounts.getPortfolio(accountID);
 
const totalHeld = portfolio.assets
  .filter((b) => b.stock_id === stockID)
  .reduce((sum, b) => sum + Number(b.amount), 0);
 
if (totalHeld === 0) {
  console.log('Not eligible: no dShares held for this stock.');
}
 
const estimated = totalHeld * Number(next.cash_amount);
 
if (estimated > 0 && estimated < 0.1) {
  console.log(
    `Estimated entitlement is ${estimated.toFixed(4)} ${next.currency}, ` +
    `below the $0.10 minimum. No payment will be made.`
  );
}
portfolio = client.v2.accounts.get_portfolio(
    account_id=ACCOUNT_ID  # Replace with the actual Account ID
)
 
total_held = sum(
    float(b.amount) for b in portfolio.assets if b.stock_id == STOCK_ID
)
 
if total_held == 0:
    print("Not eligible: no dShares held for this stock.")
 
estimated = total_held * float(next_dividend.cash_amount)
 
if 0 < estimated < 0.10:
    print(f"Estimated entitlement is {estimated:.4f} "
          f"{next_dividend.currency}, below the $0.10 minimum. "
          f"No payment will be made.")

Confirm the account is qualified to receive

A holding alone is not enough: the wallet must be connected and the entity must hold a valid KYC. This catches accounts that lapsed after onboarding.

const wallet = await client.v2.accounts.getWallet(accountID);
if (!wallet) {
  console.log('Not eligible: no wallet connected to this account.');
}
 
// The entity behind the account must be KYC-qualified.
const kyc = await client.v2.entities.kyc.retrieve(entityID);
if (kyc.status !== 'PASS') {
  console.log(`Not eligible: entity KYC status is ${kyc.status}.`);
}
wallet = client.v2.accounts.get_wallet(
    account_id=ACCOUNT_ID  # Replace with the actual Account ID
)
if wallet is None:
    print("Not eligible: no wallet connected to this account.")
 
# The entity behind the account must be KYC-qualified.
kyc = client.v2.entities.kyc.retrieve(
    entity_id=ENTITY_ID  # Replace with the actual Entity ID
)
if kyc.status != "PASS":
    print(f"Not eligible: entity KYC status is {kyc.status}.")

Step 3: Reconcile the payment

List payments for the window

start_date is inclusive, end_date is exclusive, both required and in US Eastern time. Filter by stock_id for a single dividend.

const payments = await client.v2.accounts.getDividendPayments(accountID, {
  start_date: '2026-01-01',
  end_date: '2026-02-01',
  stock_id: stockID, // optional
});
 
for (const p of payments.data ?? payments) {
  console.log(`${p.payment_date}: ${p.amount} ${p.currency} from ${p.stock_id}`);
}
payments = client.v2.accounts.get_dividend_payments(
    account_id=ACCOUNT_ID,   # Replace with the actual Account ID
    start_date="2026-01-01",  # Inclusive, US Eastern time
    end_date="2026-02-01",    # Exclusive, US Eastern time
    stock_id=STOCK_ID,        # Optional
)
 
for p in payments:
    print(f"{p.payment_date}: {p.amount} {p.currency} from {p.stock_id}")

Page through larger windows

Results are cursor-paginated with page_size capped at 10, so a wide date range spans several pages. Follow the cursor — skipping this is the most common cause of a reconciliation that silently misses payments.

async function allDividendPayments(accountID: string, startDate: string, endDate: string) {
  const results = [];
  let cursor: string | undefined = undefined;
 
  do {
    const page = await client.v2.accounts.getDividendPayments(accountID, {
      start_date: startDate,
      end_date: endDate,
      limit: 100,
      order: 'asc',
      next: cursor,
    });
 
    results.push(...(page.data ?? page));
    cursor = page.pagination_metadata?.next;
  } while (cursor);
 
  return results;
}
def all_dividend_payments(account_id, start_date, end_date):
    results = []
    cursor = None
 
    while True:
        page = client.v2.accounts.get_dividend_payments(
            account_id=account_id,
            start_date=start_date,
            end_date=end_date,
            limit=100,
            order="asc",
            next=cursor,
        )
 
        results.extend(page.data)
        cursor = page.pagination_metadata.next
        if not cursor:
            break
 
    return results

Confirm the cash balance moved

dShare holders are paid in USD+. Reading the cash balance closes the loop and catches a payment record with no matching on-chain credit.

const cash = await client.v2.accounts.getCash(accountID);
 
for (const c of cash) {
  console.log(`${c.currency}: ${c.amount}`);
}
cash = client.v2.accounts.get_cash(
    account_id=ACCOUNT_ID  # Replace with the actual Account ID
)
 
for c in cash:
    print(f"{c.currency}: {c.amount}")
⚠️

Wrapped dShare holders will see no cash movement

Wrapped holders receive the underlying dShare deposited into the wrapped position rather than a USD+ credit. Check the portfolio, not the cash balance, for those accounts.

Complete reference script

This is the complete, uninterrupted script for checking eligibility and reconciling a dividend:

import Dinari from '@dinari/api-sdk';
 
const client = new Dinari({
  apiKeyID: process.env['DINARI_API_KEY_ID'], // This is the default and can be omitted
  apiSecretKey: process.env['DINARI_API_SECRET_KEY'], // This is the default and can be omitted
  environment: 'sandbox', // defaults to 'production'
});
 
const accountID = 'your-account-id';
const entityID = 'your-entity-id';
const stockID = 'stock-id-here';
 
function addDays(isoDate: string, days: number): string {
  const d = new Date(isoDate);
  d.setDate(d.getDate() + days);
  return d.toISOString().slice(0, 10);
}
 
async function main() {
  // --- Step 1: Find the dividend ---
  const dividends = await client.v2.marketData.stocks.retrieveDividends(stockID);
  const today = new Date().toISOString().slice(0, 10);
 
  const next = dividends
    .filter((d) => d.ex_dividend_date >= today)
    .sort((a, b) => a.ex_dividend_date.localeCompare(b.ex_dividend_date))[0];
 
  if (!next) {
    console.log('No upcoming dividend announced for this stock.');
    return;
  }
 
  console.log(
    `Next dividend: ${next.cash_amount} ${next.currency} per share, ` +
    `ex-date ${next.ex_dividend_date}, pays ${next.pay_date}`
  );
 
  // --- Step 2: Check eligibility, before 4:00 AM ET on the ex-date ---
  const portfolio = await client.v2.accounts.getPortfolio(accountID);
  const totalHeld = portfolio.assets
    .filter((b) => b.stock_id === stockID)
    .reduce((sum, b) => sum + Number(b.amount), 0);
 
  const reasons: string[] = [];
 
  if (totalHeld === 0) {
    reasons.push('no dShares held for this stock');
  }
 
  const estimated = totalHeld * Number(next.cash_amount);
  if (estimated > 0 && estimated < 0.1) {
    reasons.push(`estimated entitlement ${estimated.toFixed(4)} is below the $0.10 minimum`);
  }
 
  const wallet = await client.v2.accounts.getWallet(accountID);
  if (!wallet) {
    reasons.push('no wallet connected to the account');
  }
 
  const kyc = await client.v2.entities.kyc.retrieve(entityID);
  if (kyc.status !== 'PASS') {
    reasons.push(`entity KYC status is ${kyc.status}`);
  }
 
  if (reasons.length) {
    console.log(`Not eligible: ${reasons.join('; ')}`);
  } else {
    console.log(`Eligible. Estimated entitlement: ${estimated.toFixed(4)} ${next.currency}`);
  }
 
  // --- Step 3: Reconcile, after the pay date ---
  const payments = [];
  let cursor: string | undefined = undefined;
 
  do {
    const page = await client.v2.accounts.getDividendPayments(accountID, {
      start_date: next.pay_date,
      end_date: addDays(next.pay_date, 7),
      stock_id: stockID,
      limit: 100,
      order: 'asc',
      next: cursor,
    });
 
    payments.push(...(page.data ?? page));
    cursor = page.pagination_metadata?.next;
  } while (cursor);
 
  for (const p of payments) {
    console.log(`Paid ${p.amount} ${p.currency} on ${p.payment_date}`);
  }
 
  // Close the loop against the on-chain cash balance.
  const cash = await client.v2.accounts.getCash(accountID);
  console.log('Cash balances:', cash);
}
 
main();
import os
from datetime import date, timedelta
from dinari_api_sdk import Dinari
 
client = Dinari(
    api_key_id=os.environ.get("DINARI_API_KEY_ID"),  # This is the default and can be omitted
    api_secret_key=os.environ.get("DINARI_API_SECRET_KEY"),  # This is the default and can be omitted
    environment="sandbox", # defaults to "production"
)
 
ACCOUNT_ID = "account_xxx"  # Replace with the actual Account ID
ENTITY_ID = "entity_xxx"    # Replace with the actual Entity ID
STOCK_ID = "stock_xxx"      # Replace with the actual Stock ID
 
# --- Step 1: Find the dividend ---
dividends = client.v2.market_data.stocks.retrieve_dividends(stock_id=STOCK_ID)
today = date.today().isoformat()
 
upcoming = sorted(
    (d for d in dividends if d.ex_dividend_date >= today),
    key=lambda d: d.ex_dividend_date,
)
 
if not upcoming:
    print("No upcoming dividend announced for this stock.")
    raise SystemExit
 
next_dividend = upcoming[0]
print(f"Next dividend: {next_dividend.cash_amount} {next_dividend.currency} "
      f"per share, ex-date {next_dividend.ex_dividend_date}, "
      f"pays {next_dividend.pay_date}")
 
# --- Step 2: Check eligibility, before 4:00 AM ET on the ex-date ---
portfolio = client.v2.accounts.get_portfolio(account_id=ACCOUNT_ID)
total_held = sum(
    float(b.amount) for b in portfolio.assets if b.stock_id == STOCK_ID
)
 
reasons = []
 
if total_held == 0:
    reasons.append("no dShares held for this stock")
 
estimated = total_held * float(next_dividend.cash_amount)
if 0 < estimated < 0.10:
    reasons.append(f"estimated entitlement {estimated:.4f} is below the $0.10 minimum")
 
wallet = client.v2.accounts.get_wallet(account_id=ACCOUNT_ID)
if wallet is None:
    reasons.append("no wallet connected to the account")
 
kyc = client.v2.entities.kyc.retrieve(entity_id=ENTITY_ID)
if kyc.status != "PASS":
    reasons.append(f"entity KYC status is {kyc.status}")
 
if reasons:
    print("Not eligible: " + "; ".join(reasons))
else:
    print(f"Eligible. Estimated entitlement: {estimated:.4f} {next_dividend.currency}")
 
# --- Step 3: Reconcile, after the pay date ---
pay_date = date.fromisoformat(next_dividend.pay_date)
payments = []
cursor = None
 
while True:
    page = client.v2.accounts.get_dividend_payments(
        account_id=ACCOUNT_ID,
        start_date=pay_date.isoformat(),
        end_date=(pay_date + timedelta(days=7)).isoformat(),
        stock_id=STOCK_ID,
        limit=100,
        order="asc",
        next=cursor,
    )
 
    payments.extend(page.data)
    cursor = page.pagination_metadata.next
    if not cursor:
        break
 
for p in payments:
    print(f"Paid {p.amount} {p.currency} on {p.payment_date}")
 
# Close the loop against the on-chain cash balance.
cash = client.v2.accounts.get_cash(account_id=ACCOUNT_ID)
print("Cash balances:", cash)

Did this page help you?