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 type | Receive dividend as |
|---|---|
| dShare | USD+ |
| Wrapped dShare | Underlying dShare, deposited into the wrapped dShare |
Supported chains
| Chain name | Chain ID |
|---|---|
| Mainnet | 1 |
| Base | 8453 |
| Arbitrum | 42161 |
| Avalanche C-Chain | 43114 |
| Plume | 98866 |
Dividend types
Announcement data distinguishes two types:
| Type | Meaning |
|---|---|
CD | Dividends that have been paid and/or are expected to be paid on consistent schedules |
SC | Special 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:
- Find the dividend -fetch announcements and identify the ex-dividend and pay dates.
- 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 IDFetch 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 resultsConfirm 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 movementWrapped 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)Updated about 2 hours ago
