npm package: @puul/typescript-sdk
Description: Official TypeScript SDK for the Puul Partner API — integrate prediction markets into your platform
Latest version: 1.6.0

# @puul/typescript-sdk

Official TypeScript SDK for the **Puul Partner API** — integrate prediction markets into your platform with type safety and zero configuration.

## Features

- 🔐 **Auto-authentication** — OAuth2 token management handled for you
- 📦 **Zero runtime dependencies** — uses native `fetch` (Node 18+)
- 🎯 **Fully typed** — complete TypeScript definitions for all API shapes
- 🪝 **Webhook helpers** — signature verification & typed event parsing
- ⚡ **Dual output** — ESM + CommonJS builds

## Installation

```bash
npm install @puul/typescript-sdk
```

## Quick Start

```typescript
import { PuulPartner } from '@puul/typescript-sdk';

const puul = new PuulPartner({
  clientId: 'pk_live_abc123',
  clientSecret: 'sk_live_secret789',
});

// List live markets
const markets = await puul.markets.list();
console.log(markets[0].question);
// → "Will Bitcoin reach $100k by March?"

// Place a prediction
const prediction = await puul.predictions.place({
  marketId: markets[0].id,
  outcomeId: markets[0].outcomes[0].id,
  stakeAmount: 100000,  // ₦1,000 (minor units)
  stakeCurrency: 'NGN',
  idempotencyKey: `pred_${Date.now()}`,
});
```

## API Reference

### Authentication

The SDK automatically manages OAuth2 tokens. You never need to call the auth endpoint directly.

```typescript
const puul = new PuulPartner({
  clientId: 'pk_live_abc123',
  clientSecret: 'sk_live_secret789',
  baseUrl: 'https://api.joinpuul.com/api/v1', // optional
  timeoutMs: 30000, // optional
});
```

### User Linking

```typescript
// 1. Create a link token (server-side)
const linkToken = await puul.sessions.createLinkToken({
  externalUserId: 'your-user-id-123',
  email: 'user@example.com',     // optional
  countryCode: 'NG',             // optional
});

// 2. Exchange link token for a session
const session = await puul.sessions.create(linkToken.linkToken);
// session.access_token is scoped to the linked user
```

### Markets

```typescript
// All live markets
const markets = await puul.markets.list();

// Filter by country
const ngMarkets = await puul.markets.list(['NG', 'GH']);
```

### Predictions

```typescript
// Optional: Lock in odds with a quote (10s expiry)
const quote = await puul.predictions.createQuote({
  marketId: 'market-uuid',
  outcomeId: 'outcome-uuid',
  stakeAmount: 50000,
  stakeCurrency: 'NGN',
});

// Place prediction (with or without quote)
const prediction = await puul.predictions.place({
  marketId: 'market-uuid',
  outcomeId: 'outcome-uuid',
  stakeAmount: 50000,
  stakeCurrency: 'NGN',
  idempotencyKey: 'unique-key',
  quoteId: quote.quoteId, // optional
});

// Get prediction details
const details = await puul.predictions.get(prediction.id);

// List user predictions
const history = await puul.predictions.list({ status: 'OPEN', limit: 20 });
```

### Pending Predictions (JIT Funding)

```typescript
// Create a pending prediction — returns bank payment details
const pending = await puul.predictions.createPending({
  marketId: 'market-uuid',
  outcomeId: 'outcome-uuid',
  stakeAmount: 500000,
  stakeCurrency: 'NGN',
  userExternalId: 'your-user-id',
  idempotencyKey: 'unique-key',
});
// pending.paymentReference — use this in the bank transfer

// Check status
const status = await puul.predictions.getPending(pending.id);
// status.status: 'pending' → 'funded' → 'placed'
```

### Wallet

```typescript
const balance = await puul.wallet.getBalance();
const omnibus = await puul.wallet.getOmnibusBalance('USDC');

await puul.wallet.deposit({
  amount: 5000,         // $50.00 in minor units
  currency: 'USDC',
  idempotency_key: 'dep_unique_123',
});

// Withdraw from omnibus to configured bank/crypto destination
const withdrawal = await puul.wallet.withdraw({
  amount: 500000,       // $5,000 in minor units
  currency: 'USDC',
  method: 'bank',       // or 'crypto'
});
// withdrawal.withdrawalId, withdrawal.status
```

> **Auto-Settlement:** If your `payout_method` is configured (via the admin dashboard),
> revenue share is automatically paid out after each market settlement. You don't need
> to call `withdraw()` manually — it happens at settlement time.

### Webhook Verification

```typescript
import { verifyWebhookSignature, parseWebhookEvent } from '@puul/typescript-sdk';

app.post('/webhooks/puul', express.raw({ type: '*/*' }), (req, res) => {
  const isValid = verifyWebhookSignature(
    req.body.toString(),
    req.headers['x-puul-signature'] as string,
    process.env.PUUL_WEBHOOK_SECRET!,
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = parseWebhookEvent(JSON.parse(req.body.toString()));

  switch (event.event) {
    case 'prediction.settled':
      console.log('Prediction settled:', event.data);
      break;
    case 'prediction.voided':
      console.log('Prediction voided:', event.data);
      // Handle refund — user gets full stake back
      break;
    case 'deposit.confirmed':
      console.log('Deposit confirmed:', event.data);
      break;
    case 'withdrawal.completed':
      console.log('Withdrawal completed:', event.data);
      break;
  }

  res.status(200).send('OK');
});
```

## Error Handling

All API errors throw a `PuulError` with structured details:

```typescript
import { PuulError } from '@puul/typescript-sdk';

try {
  await puul.predictions.place(params);
} catch (error) {
  if (error instanceof PuulError) {
    console.error(error.code);       // 'INSUFFICIENT_BALANCE'
    console.error(error.message);    // 'Insufficient wallet balance'
    console.error(error.statusCode); // 400
    console.error(error.requestId);  // 'req_abc123'
    console.error(error.retryable);  // false
  }
}
```

## Supported Currencies

| Code | Currency |
|------|----------|
| `NGN` | Nigerian Naira |
| `USDC` | USD Coin |
| `USDT` | Tether |
| `KES` | Kenyan Shilling |
| `GHS` | Ghanaian Cedi |
| `ZAR` | South African Rand |

## License

MIT