Placing dShare Orders with Alchemy

Ordering with Alchemy Account Kit Smart Wallet

This guide shows how to place a dShare order using the Dinari Partner SDK (@dinari/api-sdk) where an Alchemy Account Kit smart contract wallet signs the order intent.

How it works

When the funder is an Account Kit smart wallet, you must use the externally-signed path. The flow is:

  1. Create an order request and get back the EIP-712 permit to sign.
  2. Sign the permit with the smart wallet (produces an ERC-1271 contract-account signature).
  3. Submit the signed permit — Dinari-sponsored or self-sponsored.
  4. Poll the order request until it reaches a terminal state.
🚧

NOTE: Smart-wallet signatures are not EOA signatures

A standard ERC-2612 permit() recovers the signer with ecrecover, which only accepts EOA signatures and will reject a smart-contract wallet. A contract-wallet (ERC-1271) signature is only honored if the verifying contract supports ERC-1271 as Uniswap Permit2 does. Confirm your payment path validates ERC-1271/Permit2 (not raw ERC-2612) before going to mainnet.

Prerequisites

To place an order, an Entity needs:

  • A valid KYC on the Entity.
  • A valid Account under the Entity.
  • Enough payment-token balance in the smart wallet to cover the order plus fees.

For this integration you also need:

  • A Dinari partner account with an API Key ID and API Secret Key (partners.dinari.com). Start in sandbox.
  • An Alchemy API key (dashboard.alchemy.com), and optionally a Gas Manager policy ID for gas sponsorship.
  • The smart wallet's owner private key, held server-side in a secret store as a best practice. This is the signer/owner of the account — not the account address.

Step 1 — Initialize the Dinari client

import Dinari from "@dinari/api-sdk";

const dinari = new Dinari({
  apiKeyID: process.env.DINARI_API_KEY_ID,
  apiSecretKey: process.env.DINARI_API_SECRET_KEY,
  environment: "sandbox", // defaults to "production"
});

Step 2 — Create the headless smart wallet client

This is the server-side signer client (not the React hooks path). The smart account is a Modular Account V2 whose owner is a LocalAccountSigner built from a private key.

import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import { LocalAccountSigner } from "@aa-sdk/core";
import { alchemy, arbitrumSepolia } from "@account-kit/infra";

const owner = LocalAccountSigner.privateKeyToAccountSigner(
  process.env.SMART_WALLET_OWNER_PRIVATE_KEY as `0x${string}`,
);

const smartAccountClient = await createModularAccountV2Client({
  mode: "default",
  chain: arbitrumSepolia,
  transport: alchemy({ apiKey: process.env.ALCHEMY_API_KEY! }),
  signer: owner,
});

// The smart contract account address (not the owner EOA):
const smartWalletAddress = smartAccountClient.account.address;

signTypedData on this client produces a signature validatable via ERC-1271. If the account is not yet deployed, the signature is wrapped per ERC-6492.

Step 3 — Deploy the smart account

A Modular Account V2 deploys on its first user operation. On-chain ERC-1271 validation requires the account to be deployed (unless the verifier supports ERC-6492), so deploy it before signing permits.

const isDeployed = await smartAccountClient.account.isAccountDeployed();

if (!isDeployed) {
  const deployOp = await smartAccountClient.sendUserOperation({
    uo: { target: smartAccountClient.account.address, data: "0x", value: 0n },
  });
  await smartAccountClient.waitForUserOperationTransaction({ hash: deployOp.hash });
}
🚧

Deployment needs gas

The deploy user operation costs gas. Fund the account with native gas or attach an Alchemy Gas Manager policy to the client.

Step 4 — Complete KYC and link your wallet (one-time)

These steps run once per user. KYC requires the user to complete a hosted flow, so it cannot be fully automated server-side.

// Entity + KYC
const entity = await dinari.v2.entities.create({ name: "Jane Doe" });
const kyc = await dinari.v2.entities.kyc.createManagedCheck(entity.id);
console.log("Send the user to:", kyc.embed_url);

// Account
const account = await dinari.v2.entities.accounts.create(entity.id);
const accountId = account.id;

// Link the smart wallet (signed with the wallet, ERC-1271)
const nonceResp = await dinari.v2.accounts.wallet.external.getNonce(accountId, {
  wallet_address: smartWalletAddress,
});

const linkSignature = await smartAccountClient.signMessage({
  message: nonceResp.message,
});

await dinari.v2.accounts.wallet.external.connect(accountId, {
  chain_id: "eip155:421614",
  nonce: nonceResp.nonce,
  signature: linkSignature,
  wallet_address: smartWalletAddress,
});
📘

Linking also uses an ERC-1271 signature

The wallet-linking nonce is signed by the smart wallet, so the account must be deployed (Step 3) for the signature to validate.

Step 5 — Select the stock and order parameters

const stocks = await dinari.v2.marketData.stocks.list();
const stock = stocks.find((s) => s.symbol === "AAPL");
if (!stock) throw new Error("Stock not found");
const stockId = stock.id;
FieldMeaning
stock_idDinari UUID of the dShare (from marketData.stocks.list()).
order_side"BUY" or "SELL".
order_type"MARKET" or "LIMIT".
order_tifTime-in-force, e.g. "DAY".
payment_tokenPayment-token ERC-20 address (e.g. USDC on the target chain).
payment_token_quantityFor a market buy, the notional amount of payment token to spend.
chain_idCAIP-2 chain id, e.g. "eip155:421614".
📘

Amount fields by order type

Market buys use payment_token_quantity (notional). Market sells and limit orders use asset_quantity (number of dShares); limit orders also take limit_price. Fees are added on top of the notional.

Step 6 — Create the order permit

const permitResponse = await dinari.v2.accounts.orderRequests.eip155.createPermit(
  accountId,
  {
    chain_id: "eip155:421614",
    order_side: "BUY",
    order_type: "MARKET",
    order_tif: "DAY",
    stock_id: stockId,
    payment_token: process.env.PAYMENT_TOKEN_ADDRESS!,
    payment_token_quantity: 10.0, // notional to spend
  },
);

const orderRequestId = permitResponse.order_request_id;
const permit = permitResponse.permit; // { domain, types, primaryType, message }

The response carries an order_request_id and a permit object shaped exactly like an EIP-712 TypedDataDefinition.

Step 7 — Sign the permit with the smart wallet

const permitSignature = await smartAccountClient.signTypedData({
  typedData: {
    domain: permit.domain,
    types: permit.types,
    primaryType: permit.primaryType,
    message: permit.message,
  },
});

Step 8 — Submit the signed order

Choose one of two submission modes.

Dinari-sponsored — Dinari pays gas and submits on-chain:

const result = await dinari.v2.accounts.orderRequests.eip155.submit(accountId, {
  order_request_id: orderRequestId,
  permit_signature: permitSignature,
});

Self-sponsored — you send the transaction from the smart wallet:

const tx = await dinari.v2.accounts.orderRequests.eip155.createPermitTransaction(
  accountId,
  { order_request_id: orderRequestId, permit_signature: permitSignature },
);

const op = await smartAccountClient.sendUserOperation({
  uo: {
    target: tx.contract_address as `0x${string}`,
    data: tx.data as `0x${string}`,
    value: BigInt(tx.value ?? 0),
  },
});

const txHash = await smartAccountClient.waitForUserOperationTransaction({
  hash: op.hash,
});

Step 9 — Poll order status

const orderRequest = await dinari.v2.accounts.orderRequests.retrieve(
  orderRequestId,
  { account_id: accountId },
);
console.log("Status:", orderRequest.status);

Two status concepts exist:

  • OrderRequestStatus — the request status (pre-brokerage), on the OrderRequest.
  • BrokerageOrderStatus — the resulting on-chain/brokerage Order, via dinari.v2.accounts.orders.retrieve(orderId, { account_id }).

Poll the order request to a terminal state, then track the resulting order and fulfillments.

Common errors

SymptomLikely causeFix
Permit signature rejected on submitPayment path uses raw ERC-2612 (ecrecover), which rejects contract-wallet signaturesConfirm Dinari's verifier supports ERC-1271 / Permit2 for your token and chain
isValidSignature revertsSmart account not deployedDeploy the account first (Step 3)
Wallet-linking failsAccount not deployed when linking signature was producedDeploy, then link
Insufficient fundsPayment-token balance below notional + feesFund the smart wallet; budget for fees on top of the notional
User operation fails in self-sponsored modeNo native gas and no paymasterFund the wallet or attach a Gas Manager policy