Skip to main content

Installation

Install the packages you need:
npm install @keystoneos/react

1. Create a Session Token (Backend)

Session tokens are scoped, short-lived JWTs that your frontend uses to call the KeyStone API. Your backend creates them using M2M credentials.
// your-backend/routes/keystone-session.ts
import { KeystoneClient } from '@keystoneos/sdk';
import { createSessionToken } from '@keystoneos/node';

const keystone = new KeystoneClient({
  clientId: process.env.KEYSTONE_CLIENT_ID,
  clientSecret: process.env.KEYSTONE_CLIENT_SECRET,
  environment: 'production',
});

app.post('/api/keystone/session', async (req, res) => {
  const session = await createSessionToken(keystone, {
    scopes: ['settlements:read', 'settlements:write'],
    metadata: { userId: req.user.id },
  });
  res.json({ token: session.sessionToken });
});

2. Set Up the Provider (Frontend)

Wrap your app (or the relevant section) with KeystoneProvider:
import { KeystoneProvider } from '@keystoneos/react';

function App() {
  const [token, setToken] = useState<string | null>(null);

  useEffect(() => {
    fetch('/api/keystone/session', { method: 'POST' })
      .then(r => r.json())
      .then(d => setToken(d.token));
  }, []);

  if (!token) return <Loading />;

  return (
    <KeystoneProvider
      sessionToken={token}
      environment="production"
      onTokenExpired={async () => {
        const res = await fetch('/api/keystone/session', { method: 'POST' });
        const data = await res.json();
        return data.token;
      }}
    >
      <YourApp />
    </KeystoneProvider>
  );
}

3. Use Hooks

Now you can use any hook inside the provider:
import { useSettlements, getStateInfo } from '@keystoneos/react';

function SettlementDashboard() {
  const { settlements, total, isLoading } = useSettlements();

  if (isLoading) return <Spinner />;

  return (
    <table>
      <thead>
        <tr><th>Reference</th><th>State</th><th>Created</th></tr>
      </thead>
      <tbody>
        {settlements.map(s => {
          const state = getStateInfo(s.state);
          return (
            <tr key={s.id}>
              <td>{s.external_reference}</td>
              <td>{state.label}</td>
              <td>{new Date(s.created_at).toLocaleDateString()}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

4. Submit an Instruction

Create a settlement by submitting your side of the trade:
import { useSubmitInstruction } from '@keystoneos/react';

function TradeForm() {
  const { submit, result, isSubmitting, error } = useSubmitInstruction();

  const handleSubmit = async (formData) => {
    const instruction = await submit({
      role: 'seller',
      party: {
        external_reference: formData.accountId,
        name: formData.accountName,
        wallet_address: formData.walletAddress,
      },
      legs: [{
        instrument_id: formData.instrumentId,
        quantity: formData.quantity,
        direction: 'deliver',
        chain_id: formData.chainId,
      }],
      templateSlug: 'dvp-bilateral',
      timeoutAt: new Date(Date.now() + 86400000).toISOString(),
    });

    if (instruction.status === 'matched') {
      // Counterparty already submitted - settlement created
      router.push(`/settlements/${instruction.settlementId}`);
    }
  };

  if (result && result.status === 'pending_match') {
    return (
      <div>
        <p>Waiting for counterparty. Share this trade reference:</p>
        <code>{result.tradeReference}</code>
      </div>
    );
  }

  return <YourFormComponent onSubmit={handleSubmit} loading={isSubmitting} error={error} />;
}

5. Handle Deposits (Tier 2)

For custody wallet integration, provide action delegates:
<KeystoneProvider
  sessionToken={token}
  actionDelegates={{
    onDepositRequired: async (leg, depositInfo) => {
      // depositInfo contains: calldata, escrowAddress, chainId, tokenAddress, amount
      const result = await fetch('/api/deposit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          calldata: depositInfo.calldata,
          escrowAddress: depositInfo.escrowAddress,
          chainId: depositInfo.chainId,
        }),
      }).then(r => r.json());

      return { txHash: result.txHash };
    },
  }}
>
Your backend receives the calldata and submits it through your custody provider:
// your-backend/routes/deposit.ts
app.post('/api/deposit', async (req, res) => {
  const { calldata, escrowAddress, chainId } = req.body;

  // Submit to Fireblocks, BitGo, or your signing infrastructure
  const txHash = await fireblocksClient.submitTransaction({
    to: escrowAddress,
    data: calldata,
    chainId,
  });

  res.json({ txHash });
});

6. Handle Webhooks (Backend)

Set up webhook processing with signature verification:
import { webhookMiddleware } from '@keystoneos/node';
import express from 'express';

app.post(
  '/webhooks/keystone',
  express.raw({ type: 'application/json' }),
  webhookMiddleware({ secret: process.env.WEBHOOK_SECRET }),
  (req, res) => {
    const { event, data } = req.keystoneEvent;

    switch (event) {
      case 'settlement.state.compliance_cleared':
        console.log(`Settlement ${data.settlement_id}: ${data.to_state}`);
        // Update your OMS, notify traders, etc.
        break;
      case 'settlement.state.finalized':
        console.log(`Settlement finalized: ${data.settlement_id}`);
        break;
    }

    res.sendStatus(200);
  },
);

Next: Hooks Reference

Explore the full hooks API.