Skip to Content
Devnet open · mainnet by application — these docs ship with the pre-alpha release.
IntegrationsPayroll quickstart

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 roster

Frozen 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 reference

The 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 codeMeaning
batch_frozenThe batch has been frozen and can’t accept new line items.
approval_requiredThe batch exceeds the threshold and is waiting on multisig approval.
recipient_wallet_missingAn employee was added without a primaryWallet. Update the roster before re-trying.
umbra_registration_requiredThe recipient hasn’t completed Umbra registration. The dashboard shows them as “Pending claim” until they do.

Next steps

  • Walk the dashboard: pnpm -F dashboard dev and visit /dashboard/treasury/payroll.
  • Read docs/integrations/connect.md if 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.