Pular para o conteúdo principal

A marketplace payment, start to finish

Every step below is real, executable TypeScript — no invented IDs, no illustrative pseudocode — re-verified live 2026-08-31 against the Network Execution Engine. A buyer pays into a marketplace; executeSettlement() computes the Fee and Split and builds the payout's SigningRequest itself; the marketplace signs it with its own execution wallet. Ishtaran never sees a private key.

The mechanism

Three parties, one rule: the private key that authorizes the payout is generated and used inside the marketplace's own backend, and never crosses the wire. Ishtaran verifies the signature and relays the transaction — it cannot forge one.

MARKETPLACEholds the keyISHTARAN APIverifies, never signsBLOCKCHAINsimulated1. signUp(org, email, pass)org + app + Sandbox env + API key — one call2. wallets.register(xpub only)wallet generated locally, step before this3. seller claims invitation → own AccountHolder session4. accounts.create(buyer) + authorize5. transactions.create(payer, seller)6. deposits.createPaymentIntent()real deposit address — "the payment ID"7. buyer's deposit lands (simulated), confirms→ Transaction auto-reserves once funded8. settlements.executeSettlement()Fee/Split computed AND a real signingRequestId — no separate "create" call9. signingRequests.get(signingRequestId) → hash per legsign(hash) — LOCAL, private key used once10. submitSignedTransaction(sig) → verified → broadcast
Dashed arrows are responses; solid arrows are requests. The private key is used exactly once, inside the marketplace's own process, at step 9. Not pictured (happens once, right after step 3): registering an ExecutionDestination for the seller and the marketplace's own commission Account, and a NetworkCostPayerAccount to cover real network execution cost — see the code below.

Step by step

Code is TypeScript here; the same calls exist in Java, Python, and Go with each language's own idiom — see the full, runnable version linked at the end of each SDK's install section below.

1. Self-service signup

One call provisions the Organization, a default Application, its Sandbox Environment, and a first API Key — no manual dashboard step.

const owner = IshtaranClient.create({ environment: Environment.Sandbox });
const signup = await owner.auth.signUp('Ishtaran Marketplace Demo', 'owner@example.com', '••••••••');

2. The marketplace's own execution wallet

Mnemonic and private key are generated in this process and never serialized anywhere. Only the extended public key is sent to Ishtaran.

const wallet = walletModule.generate(); // 24-word mnemonic, local only
const registered = await client.wallets.register(
applicationId, networkId, DerivationScheme.TRON_BIP44_HARDENED_ACCOUNT,
wallet.wallet.accountExtendedPublicKey, idempotencyKey,
);

3. Seller claims their own identity

The marketplace issues an invitation; the seller — a separate session, never authenticated as the marketplace — claims it and gets their own AccountHolder login. Authorizing an Account for an Application (next step) requires the marketplace's Member session, not the API Key — a real gap this flow found in the docs, now fixed.

const invite = await client.accounts.createAccountHolderInvitation(organizationId, 'seller-01');
// -- delivered out-of-band (email/link) to the seller --
const claim = await sellerClient.accountHolders.signUpAndClaimInvitation(
invite.plainTextToken, 'seller@example.com', '••••••••',
);

4. Register where every beneficiary gets paid, and who pays for network execution

Before the first real Settlement on this AssetNetwork: an ExecutionDestination for the seller (their own external wallet) and for the marketplace's own commission Account, and a NetworkCostPayerAccount to cover real network execution cost — the marketplace's own commission Account again, a real business decision (it pays network cost out of its own commission). Skip either and executeSettlement() in step 8 fails before it builds anything.

await client.executionDestinations.register(organizationId, sellerAccountId, assetNetworkId, sellerOwnAddress);
await client.executionDestinations.register(organizationId, marketplaceRevenueAccountId, assetNetworkId, marketplaceRevenueAddress);
await client.networkCostPayerAccounts.register(organizationId, assetNetworkId, marketplaceRevenueAccountId);

5. Transaction + Payment Intent

Two non-payer Participants (seller, marketplace) require an explicit Split — a single implicit 100% only applies with exactly one beneficiary (BR-SPL-004). workflowVersionId is null here — genuinely optional, this flow's own application decides when to settle.

const txn = await client.transactions.create(organizationId, applicationId, environmentId, null, assetNetworkId, '1000', [payer, seller, marketplace]);
const intent = await client.deposits.createPaymentIntent(organizationId, txn.transactionId, assetNetworkId, '1000');

6. Deposit lands, reservation is automatic

Once the deposit is confirmed, the Transaction moves to Reserved on its own — no explicit reserve() call needed in the common path.

await client.sandbox.simulateDeposit(environmentId, depositAddress, assetNetworkId, '1000');
await client.sandbox.simulateConfirmation(environmentId, observedAddressId, 1, true);
// poll transactions.getState(...) -- CREATED -> RESERVED

7. Settlement builds its own SigningRequest — no separate call needed

executeSettlement() computes the Fee/Split AND builds a real SigningRequest itself, under SelfCustody — one ExecutionLeg per beneficiary, each addressed via the ExecutionDestination registered in step 4. This is not a two-step process — there is no separate signingRequests.create(...) call for a Settlement's payout; fetch the full Settlement to read the signingRequestId it already produced.

const executed = await client.settlements.executeSettlement(txn.transactionId);
const settlement = await client.settlements.get(executed.settlementId);
// settlement.signingRequestId is already populated -- nothing left to "request"

8. Sign locally, submit, verify

The private key touches exactly one line, in this process. Ishtaran rejects a tampered hash outright (SIGNED_TRANSACTION_MISMATCH) and only broadcasts once every leg is verified — the first submission alone never triggers it.

const signingRequest = await client.signingRequests.get(settlement.signingRequestId!);
for (const leg of signingRequest.legs) {
const signature = wallet.signer.sign(signingRequest.derivationReference, hexToBytes(leg.canonicalHash));
await client.signingRequests.submitSignedTransaction(
settlement.signingRequestId!, leg.executionLegId, leg.canonicalHash, bytesToHex(signature),
);
}

Both legs reach Broadcast with a real (simulated) reference, automatically, on the last signature — no separate "broadcast" call. In Production, the only thing that changes is what's behind the broadcast port — the signing protocol above is identical. Once each leg's confirmation is simulated and the Settlement reaches Completed, payout.getPayableSummary(...) reflects what was actually paid — never ledger.getBalance().available, since both beneficiaries' ExecutionDestinations are external wallets (see Self-Custody and Transaction, Settlement, Split, and Refund).

Run it yourself

The full runnable version of this exact flow — same calls, same order — ships as example 14 in the TypeScript SDK, re-verified live 2026-08-31:

See Self-Custody for the full signing-protocol detail, and SDKs for installation.