# Token sale offering flows

Source: https://docs.settlemint.com/docs/api-reference/offerings/token-sale-offering-flows
Create, configure, activate, buy, finalize, and settle a token sale offering through DALP APIs, SDK methods, and CLI commands.



# Token sale offering flows [#token-sale-offering-flows]

A token sale offering runs a primary sale for an asset: investors buy tokens with one or more payment currencies, and the issuer settles funds and tokens after the sale closes. Use this flow to create a sale, configure its controls, activate it, accept purchases, finalize accounting, and handle withdrawals or refunds.

DALP exposes token sales through the API, SDK, and CLI. The public API and SDK share the same sale model, so you can create and operate a sale programmatically and reconcile it through the platform UI or CLI.

A token sale is a system add-on. Your DALP system must have the token sale add-on factory available before you create a sale.

## When to use this flow [#when-to-use-this-flow]

Use a token sale offering when you run a primary distribution for an asset and need on-chain controls for pricing, purchase limits, presale access, vesting, and post-sale settlement.

The flow has three phases:

* **Configure** — create the sale, then set payment currencies, purchase limits, soft cap, terms, presale whitelist, and vesting before you activate it.
* **Sell** — activate the sale and accept purchases. Pause and resume the sale while it is live if you need to.
* **Settle** — end and finalize the sale, then withdraw funds and tokens, or let investors claim refunds when the sale fails its soft cap.

Do not use this page as a substitute for the generated contract. Use the [API Reference](/docs/api-reference/reference/openapi) for exact request and response fields, and the [CLI Command Reference](/docs/developers/cli/command-reference) for command syntax.

## Prerequisites [#prerequisites]

* Your DALP system has the token sale add-on factory available.
* You have authenticated and configured a client. See [API integration getting started](/docs/api-reference/reference/getting-started).
* You send acting context with each request through the wallet verification your client attaches. The SDK and CLI examples below assume a configured `walletVerification`.
* The caller holds the operator role required for the action. Configuration, activation, and settlement are issuer-side actions; buying and refund claims are investor-side actions.

## Amount and time units [#amount-and-time-units]

Amount fields use raw base units. Scale the sale token caps, purchase limits, soft cap, and minimum token amounts to the sale token decimals, and scale the buy amount to the decimals of the payment currency you send. Price ratios are scaled by 1e18. Timestamps for sale start, presale end, and vesting are Unix seconds.

## Create a sale [#create-a-sale]

Create a sale with the token address, a future sale start time, a duration in seconds, and a hard cap. You can supply payment currencies, purchase limits, a soft cap, presale settings, and vesting in the same request, or add them later with the configuration operations below.

```ts fixture=dalp-client
// saleStart must be a future chain timestamp. Derive it from the current time
// with a generous buffer so it stays ahead even on test chains.
const saleStart = String(Math.floor(Date.now() / 1000) + 86400);

const created = await client.addons.tokenSale.create({
  body: {
    tokenAddress: "0xTOKEN",
    saleStart,
    saleDuration: 2592000,
    hardCap: "1000000000000000000000000",
    paymentCurrencies: [{ currency: "0xUSDC", priceRatio: "1000000000000000000" }],
    walletVerification,
  },
});

if (!("data" in created)) {
  throw new Error(`Sale creation is still processing: ${created.statusUrl}`);
}

const saleAddress = created.data.tokenSaleId;
```

Sale creation runs on-chain through the system factory. By default the request returns `202 Accepted` with a `statusUrl` that tracks the queued transaction. Poll that URL until the operation reaches a terminal state, or send `Prefer: wait=N` (RFC 7240) to wait synchronously. The SDK waits synchronously by default, so the example above resolves to the created sale in most cases.

To run a presale, set the `presale` fields at create time. The presale end time, discount multiplier, and per-address cap are fixed during creation; you can only update the whitelist after that.

```ts fixture=dalp-client
// Both saleStart and the presale endTime must be future chain timestamps, with the presale ending after the sale opens.
// Use a generous buffer so the values stay ahead even on test chains.
const presaleSaleStart = String(Math.floor(Date.now() / 1000) + 86400);
const presaleEndTime = String(Math.floor(Date.now() / 1000) + 86400 + 604800);

const createdWithPresale = await client.addons.tokenSale.create({
  body: {
    tokenAddress: "0xTOKEN",
    saleStart: presaleSaleStart,
    saleDuration: 2592000,
    hardCap: "1000000000000000000000000",
    presale: {
      endTime: presaleEndTime,
      discountMultiplier: "800000000000000000",
      maxPerAddress: "100000000000000000000",
      whitelist: ["0xBUYER1", "0xBUYER2"],
    },
    walletVerification,
  },
});
```

## Configure sale controls [#configure-sale-controls]

Configure controls while the sale is in setup, before you activate it. Each operation runs on-chain and follows the same queued-transaction model as create.

| Operation                | SDK method                                    | What it sets                                               |
| ------------------------ | --------------------------------------------- | ---------------------------------------------------------- |
| Add payment currency     | `addons.tokenSale.addPaymentCurrency`         | An accepted ERC-20 payment currency and its price ratio.   |
| Remove payment currency  | `addons.tokenSale.removePaymentCurrency`      | Removes an accepted payment currency.                      |
| Set purchase limits      | `addons.tokenSale.setPurchaseLimits`          | Minimum and maximum purchase amounts per investor.         |
| Set soft cap             | `addons.tokenSale.setSoftCap`                 | The soft cap below which the sale is treated as failed.    |
| Set terms hash           | `addons.tokenSale.setTermsHash`               | The terms hash investors acknowledge before buying.        |
| Configure vesting        | `addons.tokenSale.configureVesting`           | Vesting start, duration, and cliff for purchased tokens.   |
| Add to presale list      | `addons.tokenSale.addToPresaleWhitelist`      | Wallet addresses allowed to buy during the presale window. |
| Remove from presale list | `addons.tokenSale.removeFromPresaleWhitelist` | Removes wallet addresses from the presale whitelist.       |

The presale end time, discount multiplier, and per-address cap are set at create time and cannot be changed later. The whitelist can be updated after creation.

Activation requires the sale contract to hold at least the `hardCap` amount of sale tokens. Mint or transfer the full token inventory to the sale address before you activate; otherwise activation reverts with an insufficient balance error.

Set the purchase limits and soft cap against the sale address you created:

```ts fixture=dalp-client group=token-sale-config
const saleAddress = "0xSALE";

await client.addons.tokenSale.setPurchaseLimits({
  body: {
    saleAddress,
    minPurchase: "100000000000000000000",
    maxPurchase: "10000000000000000000000",
    walletVerification,
  },
});

await client.addons.tokenSale.setSoftCap({
  body: {
    saleAddress,
    softCap: "250000000000000000000000",
    walletVerification,
  },
});
```

## Activate and buy [#activate-and-buy]

Before you activate the sale, fund the sale contract with at least the hard cap amount of sale tokens. `activateSale` checks the contract's token balance and reverts with `InsufficientTokenBalance` if it is below the hard cap. Transfer or mint the sale inventory to the sale address after creation and before activation.

Activate the sale to open it for purchases. After activation, an investor buys tokens by sending a payment currency amount and a minimum token amount that protects against slippage.

```ts fixture=dalp-client group=token-sale-buy
const saleAddress = "0xSALE";

await client.addons.tokenSale.activate({
  body: { saleAddress, walletVerification },
});
```

Activation changes the sale status, but purchases are still rejected until the chain time reaches the configured `saleStartTime`. If you created the sale with a future start time, poll `currentTime` to check the chain clock before you open purchases.

```ts fixture=dalp-client
const now = await client.addons.tokenSale.currentTime({ query: {} });
```

```ts fixture=dalp-client group=token-sale-buy
await client.addons.tokenSale.buy({
  body: {
    saleAddress,
    currency: "0xUSDC",
    amount: "1000000000000000000",
    minTokenAmount: "950000000000000000",
    walletVerification,
  },
});
```

If the sale sets a terms hash, an investor acknowledges the terms before buying:

```ts fixture=dalp-client
const saleAddress = "0xSALE";

await client.addons.tokenSale.acknowledgeTerms({
  body: { saleAddress, walletVerification },
});
```

Pause and resume the sale while it is live when you need to hold purchases:

```ts fixture=dalp-client
const saleAddress = "0xSALE";

await client.addons.tokenSale.pauseSale({
  body: { saleAddress, walletVerification },
});

await client.addons.tokenSale.unpauseSale({
  body: { saleAddress, walletVerification },
});
```

## Monitor a sale [#monitor-a-sale]

Read sale state through the list and read endpoints. Use list for collections and read when you already know the sale address.

```ts fixture=dalp-client group=token-sale-monitor
const saleAddress = "0xSALE";

const sales = await client.addons.tokenSale.list({
  query: {
    sortBy: "createdAt",
    sortDirection: "desc",
    filters: [{ id: "systemAddon", operator: "eq", value: "0xSALEADDON" }],
  },
});

const detail = await client.addons.tokenSale.read({
  params: { saleAddress },
});
```

List supports pagination and sorting by sale start time, sale end time, creation time, and total sold. The response `status` field reflects the sale phase — for example, setup, presale, public sale, paused, ended, success, or failed. DALP computes that status per request against the active chain's clock, so a presale advances to the public-sale phase once its presale end time is crossed. Because status is computed at read time, it is not a filterable or facetable field; group or filter by status on the response payload.

Scale and verify against the active chain. List and read are scoped to the active chain resolved from your system context; DALP does not aggregate sales across chains in one response.

When you need the chain timestamp the sale uses for scheduling, read it from the current-time endpoint:

```ts fixture=dalp-client
const now = await client.addons.tokenSale.currentTime({ query: {} });
```

For authenticated callers without admin, token manager, or sale admin roles, the read endpoint resolves the caller's effective buyer address and filters `tokenSalePurchases` to that buyer. An investor-scoped integration using this endpoint for full purchase reconciliation would undercount activity; call the read endpoint with an operator-scoped caller when you need all purchases for the sale.

## Finalize and settle [#finalize-and-settle]

End the sale window, then finalize it to settle accounting. Finalizing determines whether the sale met its soft cap.

```ts fixture=dalp-client group=token-sale-settle
const saleAddress = "0xSALE";

await client.addons.tokenSale.end({
  body: { saleAddress, walletVerification },
});

await client.addons.tokenSale.finalize({
  body: { saleAddress, walletVerification },
});
```

After a successful sale, the issuer withdraws collected funds for each payment currency and withdraws any unsold tokens.

```ts fixture=dalp-client
const saleAddress = "0xSALE";

await client.addons.tokenSale.withdrawFunds({
  body: { saleAddress, currency: "0xUSDC", recipient: "0xRECIPIENT", walletVerification },
});

await client.addons.tokenSale.withdrawUnsoldTokens({
  body: { saleAddress, recipient: "0xRECIPIENT", walletVerification },
});
```

## Investor claims [#investor-claims]

When a sale fails its soft cap, an investor claims a refund per payment currency they paid in:

```ts fixture=dalp-client
const saleAddress = "0xSALE";

await client.addons.tokenSale.claimRefund({
  body: { saleAddress, currency: "0xUSDC", walletVerification: buyerWalletVerification },
});
```

After a successful sale, investors withdraw their purchased tokens once the sale is claimable. The contract sends tokens to the caller's address, so the investor must call this with their own wallet verification.

```ts fixture=dalp-client
const saleAddress = "0xSALE";

await client.addons.tokenSale.withdrawTokens({
  body: { saleAddress, walletVerification: buyerWalletVerification },
});
```

## CLI coverage [#cli-coverage]

The DALP CLI exposes the same flow under `dalp token-sales`. Create the sale first, configure controls before activation, then activate, sell, and settle.

```bash
dalp token-sales list
dalp token-sales read 0xSALE
dalp token-sales create \
  --token-address 0xTOKEN \
  --sale-start 2026-06-01T09:00:00.000Z \
  --sale-duration 604800 \
  --hard-cap 1000000000000000000000000
dalp token-sales add-payment-currency --address 0xSALE --currency 0xUSDC --price-ratio 1000000000000000000
dalp token-sales set-purchase-limits --address 0xSALE --min-purchase 100000000000000000000 --max-purchase 10000000000000000000000
dalp token-sales set-soft-cap --address 0xSALE --soft-cap 250000000000000000000000
dalp token-sales activate 0xSALE
dalp token-sales buy --address 0xSALE --currency 0xUSDC --amount 1000000000000000000 --min-token-amount 950000000000000000
dalp token-sales pause 0xSALE
dalp token-sales unpause 0xSALE
dalp token-sales end 0xSALE
dalp token-sales finalize 0xSALE
dalp token-sales withdraw-funds --address 0xSALE --currency 0xUSDC --recipient 0xRECIPIENT
dalp token-sales withdraw-unsold-tokens --address 0xSALE --recipient 0xRECIPIENT
dalp token-sales withdraw-tokens 0xSALE
dalp token-sales claim-refund --address 0xSALE --currency 0xUSDC
```

The CLI also covers vesting, presale whitelist, terms hash, and payment-currency management. Use the API or SDK for scenarios the CLI flags do not cover.

## Related references [#related-references]

* [API integration getting started](/docs/api-reference/reference/getting-started)
* [API Reference](/docs/api-reference/reference/openapi)
* [CLI Command Reference](/docs/developers/cli/command-reference)
* [XvP settlement flows](/docs/api-reference/settlement/xvp-settlement-flows)
