Payroll quickstart
Run stablecoin payroll for a team, a DAO, or a payroll provider — multi-recipient, private by default, Squads-compatible approvals.
What you get
- A roster of employees and contractors, each with their preferred payment wallet and tax profile.
- Batches that group payments by pay period, with approval gates for amounts above the configured threshold.
- Private settlement — recipients see incoming funds; the public ledger doesn’t see who got paid or how much.
- Tax filings — 1099 / W-2-ready summaries pulled from the same batch data merchants use for their books.
Roster
Add an employee through the SDK or the dashboard. The roster lives at
/dashboard/treasury/payroll and surfaces filtering by employment
type, tax jurisdiction, and active status.
const ada = await ninja.payroll.employees.create({
payeeType: 'EMPLOYEE', // or 'CONTRACTOR'
displayName: 'Ada Lovelace',
primaryWallet: 'Ada1QmZ…', // base58 Solana pubkey
defaultCurrency: 'USDC',
taxJurisdiction: 'US-CA',
});Contractors and international payees use the same shape — the
taxJurisdiction flag controls which filings get generated at
period end.
Pay periods
Create a batch, add line items, freeze it, approve it:
const batch = await ninja.payroll.batches.create({
description: 'Pay period 2026-05-A',
scheduledFor: '2026-05-15',
});
await ninja.payroll.batches.bulkAddPayments(batch.id, {
entries: [
{ employeeId: ada.id, amount: 5_000_00, currency: 'USDC' }, // $5,000
{ employeeId: bob.id, amount: 4_200_00, currency: 'USDC' },
{ employeeId: cdr.id, amount: 3_800_00, currency: 'USDC' },
],
});
await ninja.payroll.batches.freeze(batch.id); // lock the rosterFrozen batches over the approval threshold route through a Squads-compatible multisig approval. The dashboard renders the approval state inline, and the API exposes the threshold via the organization’s payroll-policy settings.
Settlement
On approval, the router program executes
split_to_n_recipients — one on-chain transaction settling every
line item — and each recipient’s deposit lands as an Umbra UTXO they
can claim from their wallet. Recipients see funds; everyone else
sees nothing.
const settled = await ninja.payroll.batches.settle(batch.id);
settled.routerTxSig; // Solana tx — public, but doesn't reveal recipients
settled.umbraBatchId; // shielded settlement referenceThe same Umbra primitives that protect merchant settlement also protect payroll — there’s no separate “private payroll” code path.
Tax filings
At period end, generate filings for every recipient who hit the reporting threshold:
const filings = await ninja.payroll.taxFilings.generate({
year: 2026,
});
for (const filing of filings.filings) {
// filing.formType === '1099-NEC' | 'W-2' | …
// filing.netCompensation, filing.withheld, filing.taxJurisdiction
}The dashboard exposes the same data at
/dashboard/treasury/payroll/tax, with CSV export and per-recipient
drill-downs.
Errors worth handling
| Error code | Meaning |
|---|---|
batch_frozen | The batch has been frozen and can’t accept new line items. |
approval_required | The batch exceeds the threshold and is waiting on multisig approval. |
recipient_wallet_missing | An employee was added without a primaryWallet. Update the roster before re-trying. |
umbra_registration_required | The recipient hasn’t completed Umbra registration. The dashboard shows them as “Pending claim” until they do. |
Next steps
- Walk the dashboard:
pnpm -F dashboard devand visit/dashboard/treasury/payroll. - Read
docs/integrations/connect.mdif you’re building a payroll-as-a-service product on top of NinjaPay. - Wire the Squads approval webhook to your treasury Slack channel
with the existing webhook event catalogue at
docs/integrations/webhooks.md.